├── .gitignore ├── MIT-LICENSE ├── Makefile ├── README.markdown ├── bower.json ├── keymaster.js ├── package.json ├── test.html └── test ├── evidence.js └── keymaster.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | keymaster.min.js -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2013 Thomas Fuchs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # To run this, you'll need to install UglifyJS: 2 | # npm install uglify-js@1 -g 3 | default: 4 | uglifyjs -o keymaster.min.js keymaster.js 5 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # keymaster.js 2 | 3 | Keymaster is a simple micro-library for defining and 4 | dispatching keyboard shortcuts in web applications. 5 | 6 | It has no dependencies. 7 | 8 | *It’s a work in progress (e.g. beta), so spare me your nerdrage and instead 9 | contribute! Patches are welcome, but they are not guaranteed to make 10 | it in.* 11 | 12 | ## Usage 13 | 14 | Include `keymaster.js` in your web app*, by loading it as usual: 15 | 16 | ```html 17 | 18 | ``` 19 | 20 | Keymaster has no dependencies and can be used completely standalone. 21 | It should not interfere with any JavaScript libraries or frameworks. 22 | 23 | _*Preferably use a minified version that fits your workflow. You can 24 | run `make` to have UglifyJS (if you have it installed) create a 25 | `keymaster.min.js` file for you._ 26 | 27 | ## Defining shortcuts 28 | 29 | One global method is exposed, `key` which defines shortcuts when 30 | called directly. 31 | 32 | ```javascript 33 | // define short of 'a' 34 | key('a', function(){ alert('you pressed a!') }); 35 | 36 | // returning false stops the event and prevents default browser events 37 | key('ctrl+r', function(){ alert('stopped reload!'); return false }); 38 | 39 | // multiple shortcuts that do the same thing 40 | key('⌘+r, ctrl+r', function(){ }); 41 | ``` 42 | 43 | The handler method is called with two arguments set, the keydown `event` fired, and 44 | an object containing, among others, the following two properties: 45 | 46 | `shortcut`: a string that contains the shortcut used, e.g. `ctrl+r` 47 | `scope`: a string describing the scope (or `all`) 48 | 49 | ```javascript 50 | key('⌘+r, ctrl+r', function(event, handler){ 51 | console.log(handler.shortcut, handler.scope); 52 | }); 53 | 54 | // "ctrl+r", "all" 55 | ``` 56 | 57 | 58 | ## Supported keys 59 | 60 | Keymaster understands the following modifiers: 61 | `⇧`, `shift`, `option`, `⌥`, `alt`, `ctrl`, `control`, `command`, and `⌘`. 62 | 63 | The following special keys can be used for shortcuts: 64 | `backspace`, `tab`, `clear`, `enter`, `return`, `esc`, `escape`, `space`, 65 | `up`, `down`, `left`, `right`, `home`, `end`, `pageup`, `pagedown`, `del`, `delete` 66 | and `f1` through `f19`. 67 | 68 | 69 | ## Modifier key queries 70 | 71 | At any point in time (even in code other than key shortcut handlers), 72 | you can query the `key` object for the state of any keys. This 73 | allows easy implementation of things like shift+click handlers. For example, 74 | `key.shift` is `true` if the shift key is currently pressed. 75 | 76 | ```javascript 77 | if(key.shift) alert('shift is pressed, OMGZ!'); 78 | ``` 79 | 80 | 81 | ## Other key queries 82 | 83 | At any point in time (even in code other than key shortcut handlers), 84 | you can query the `key` object for the state of any key. This 85 | is very helpful for game development using a game loop. For example, 86 | `key.isPressed(77)` is `true` if the M key is currently pressed. 87 | 88 | ```javascript 89 | if(key.isPressed("M")) alert('M key is pressed, can ya believe it!?'); 90 | if(key.isPressed(77)) alert('M key is pressed, can ya believe it!?'); 91 | ``` 92 | 93 | You can also get these as an array using... 94 | ```javascript 95 | key.getPressedKeyCodes() // returns an array of key codes currently pressed 96 | ``` 97 | 98 | 99 | ## Scopes 100 | 101 | If you want to reuse the same shortcut for separate areas in your single page app, 102 | Keymaster supports switching between scopes. Use the `key.setScope` method to set scope. 103 | 104 | ```javascript 105 | // define shortcuts with a scope 106 | key('o, enter', 'issues', function(){ /* do something */ }); 107 | key('o, enter', 'files', function(){ /* do something else */ }); 108 | 109 | // set the scope (only 'all' and 'issues' shortcuts will be honored) 110 | key.setScope('issues'); // default scope is 'all' 111 | 112 | // remove all events that are set in 'issues' scope 113 | key.deleteScope('issues'); 114 | 115 | ``` 116 | 117 | 118 | ## Filter key presses 119 | 120 | By default, when an `INPUT`, `SELECT` or `TEXTAREA` element is focused, Keymaster doesn't process any shortcuts. 121 | 122 | You can change this by overwriting `key.filter` with a new function. This function is called before 123 | Keymaster processes shortcuts, with the keydown event as argument. 124 | 125 | If your function returns false, then the no shortcuts will be processed. 126 | 127 | Here's the default implementation for reference: 128 | 129 | ```javascript 130 | function filter(event){ 131 | var tagName = (event.target || event.srcElement).tagName; 132 | return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); 133 | } 134 | ``` 135 | 136 | If you only want _some_ shortcuts to work while in an input element, you can change the scope in the 137 | `key.filter` function. Here's an example implementation, setting the scope to either `'input'` or `'other'`. 138 | Don't forget to return `true` so the any shortcuts get processed. 139 | 140 | ```javascript 141 | key.filter = function(event){ 142 | var tagName = (event.target || event.srcElement).tagName; 143 | key.setScope(/^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other'); 144 | return true; 145 | } 146 | ``` 147 | 148 | However a more robust way to handle this is to use proper 149 | focus and blur event handlers on your input element, and change scopes there as you see fit. 150 | 151 | 152 | ## noConflict mode 153 | 154 | You can call ```key.noConflict``` to remove the ```key``` function from global scope and restore whatever ```key``` was defined to before Keymaster was loaded. Calling ```key.noConflict``` will return the Keymaster ```key``` function. 155 | 156 | ```javascript 157 | var k = key.noConflict(); 158 | k('a', function() { /* ... */ }); 159 | 160 | key() 161 | // --> TypeError: 'undefined' is not a function 162 | ``` 163 | 164 | 165 | ## Unbinding shortcuts 166 | 167 | Similar to defining shortcuts, they can be unbound using `key.unbind`. 168 | 169 | ```javascript 170 | // unbind 'a' handler 171 | key.unbind('a'); 172 | 173 | // unbind a key only for a single scope 174 | // when no scope is specified it defaults to the current scope (key.getScope()) 175 | key.unbind('o, enter', 'issues'); 176 | key.unbind('o, enter', 'files'); 177 | ``` 178 | 179 | 180 | ## Notes 181 | 182 | Keymaster should work with any browser that fires `keyup` and `keydown` events, 183 | and is tested with IE (6+), Safari, Firefox and Chrome. 184 | 185 | See [http://madrobby.github.com/keymaster/](http://madrobby.github.com/keymaster/) for a live demo. 186 | 187 | 188 | ## CoffeeScript 189 | 190 | If you're using CoffeeScript, configuring key shortcuts couldn't be simpler: 191 | 192 | ```coffeescript 193 | key 'a', -> alert('you pressed a!') 194 | 195 | key '⌘+r, ctrl+r', -> 196 | alert 'stopped reload!' 197 | off 198 | 199 | key 'o, enter', 'issues', -> 200 | whatevs() 201 | 202 | alert 'shift is pressed, OMGZ!' if key.shift 203 | ``` 204 | 205 | 206 | ## Contributing 207 | 208 | To contribute, please fork Keymaster, add your patch and tests for it (in the `test/` folder) and 209 | submit a pull request. 210 | 211 | ## TODOs 212 | 213 | * Finish test suite 214 | 215 | Keymaster is (c) 2011-2013 Thomas Fuchs and may be freely distributed under the MIT license. 216 | See the `MIT-LICENSE` file. 217 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keymaster", 3 | "main": "keymaster.js", 4 | "version": "1.6.3", 5 | "homepage": "https://github.com/madrobby/keymaster", 6 | "authors": [ 7 | "Thomas Fuchs (http://mir.aculo.us)" 8 | ], 9 | "description": "library for defining and dispatching keyboard shortcuts", 10 | "moduleType": [ 11 | "es6", 12 | "globals" 13 | ], 14 | "keywords": [ 15 | "key", 16 | "keyboard", 17 | "shortcuts" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "test", 25 | "tests" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /keymaster.js: -------------------------------------------------------------------------------- 1 | // keymaster.js 2 | // (c) 2011-2013 Thomas Fuchs 3 | // keymaster.js may be freely distributed under the MIT license. 4 | 5 | ;(function(global){ 6 | var k, 7 | _handlers = {}, 8 | _mods = { 16: false, 18: false, 17: false, 91: false }, 9 | _scope = 'all', 10 | // modifier keys 11 | _MODIFIERS = { 12 | '⇧': 16, shift: 16, 13 | '⌥': 18, alt: 18, option: 18, 14 | '⌃': 17, ctrl: 17, control: 17, 15 | '⌘': 91, command: 91 16 | }, 17 | // special keys 18 | _MAP = { 19 | backspace: 8, tab: 9, clear: 12, 20 | enter: 13, 'return': 13, 21 | esc: 27, escape: 27, space: 32, 22 | left: 37, up: 38, 23 | right: 39, down: 40, 24 | del: 46, 'delete': 46, 25 | home: 36, end: 35, 26 | pageup: 33, pagedown: 34, 27 | ',': 188, '.': 190, '/': 191, 28 | '`': 192, '-': 189, '=': 187, 29 | ';': 186, '\'': 222, 30 | '[': 219, ']': 221, '\\': 220 31 | }, 32 | code = function(x){ 33 | return _MAP[x] || x.toUpperCase().charCodeAt(0); 34 | }, 35 | _downKeys = []; 36 | 37 | for(k=1;k<20;k++) _MAP['f'+k] = 111+k; 38 | 39 | // IE doesn't support Array#indexOf, so have a simple replacement 40 | function index(array, item){ 41 | var i = array.length; 42 | while(i--) if(array[i]===item) return i; 43 | return -1; 44 | } 45 | 46 | // for comparing mods before unassignment 47 | function compareArray(a1, a2) { 48 | if (a1.length != a2.length) return false; 49 | for (var i = 0; i < a1.length; i++) { 50 | if (a1[i] !== a2[i]) return false; 51 | } 52 | return true; 53 | } 54 | 55 | var modifierMap = { 56 | 16:'shiftKey', 57 | 18:'altKey', 58 | 17:'ctrlKey', 59 | 91:'metaKey' 60 | }; 61 | function updateModifierKey(event) { 62 | for(k in _mods) _mods[k] = event[modifierMap[k]]; 63 | }; 64 | 65 | // handle keydown event 66 | function dispatch(event) { 67 | var key, handler, k, i, modifiersMatch, scope; 68 | key = event.keyCode; 69 | 70 | if (index(_downKeys, key) == -1) { 71 | _downKeys.push(key); 72 | } 73 | 74 | // if a modifier key, set the key. property to true and return 75 | if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko 76 | if(key in _mods) { 77 | _mods[key] = true; 78 | // 'assignKey' from inside this closure is exported to window.key 79 | for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true; 80 | return; 81 | } 82 | updateModifierKey(event); 83 | 84 | // see if we need to ignore the keypress (filter() can can be overridden) 85 | // by default ignore key presses if a select, textarea, or input is focused 86 | if(!assignKey.filter.call(this, event)) return; 87 | 88 | // abort if no potentially matching shortcuts found 89 | if (!(key in _handlers)) return; 90 | 91 | scope = getScope(); 92 | 93 | // for each potential shortcut 94 | for (i = 0; i < _handlers[key].length; i++) { 95 | handler = _handlers[key][i]; 96 | 97 | // see if it's in the current scope 98 | if(handler.scope == scope || handler.scope == 'all'){ 99 | // check if modifiers match if any 100 | modifiersMatch = handler.mods.length > 0; 101 | for(k in _mods) 102 | if((!_mods[k] && index(handler.mods, +k) > -1) || 103 | (_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false; 104 | // call the handler and stop the event if neccessary 105 | if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){ 106 | if(handler.method(event, handler)===false){ 107 | if(event.preventDefault) event.preventDefault(); 108 | else event.returnValue = false; 109 | if(event.stopPropagation) event.stopPropagation(); 110 | if(event.cancelBubble) event.cancelBubble = true; 111 | } 112 | } 113 | } 114 | } 115 | }; 116 | 117 | // unset modifier keys on keyup 118 | function clearModifier(event){ 119 | var key = event.keyCode, k, 120 | i = index(_downKeys, key); 121 | 122 | // remove key from _downKeys 123 | if (i >= 0) { 124 | _downKeys.splice(i, 1); 125 | } 126 | 127 | if(key == 93 || key == 224) key = 91; 128 | if(key in _mods) { 129 | _mods[key] = false; 130 | for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false; 131 | } 132 | }; 133 | 134 | function resetModifiers() { 135 | for(k in _mods) _mods[k] = false; 136 | for(k in _MODIFIERS) assignKey[k] = false; 137 | }; 138 | 139 | // parse and assign shortcut 140 | function assignKey(key, scope, method){ 141 | var keys, mods; 142 | keys = getKeys(key); 143 | if (method === undefined) { 144 | method = scope; 145 | scope = 'all'; 146 | } 147 | 148 | // for each shortcut 149 | for (var i = 0; i < keys.length; i++) { 150 | // set modifier keys if any 151 | mods = []; 152 | key = keys[i].split('+'); 153 | if (key.length > 1){ 154 | mods = getMods(key); 155 | key = [key[key.length-1]]; 156 | } 157 | // convert to keycode and... 158 | key = key[0] 159 | key = code(key); 160 | // ...store handler 161 | if (!(key in _handlers)) _handlers[key] = []; 162 | _handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods }); 163 | } 164 | }; 165 | 166 | // unbind all handlers for given key in current scope 167 | function unbindKey(key, scope) { 168 | var multipleKeys, keys, 169 | mods = [], 170 | i, j, obj; 171 | 172 | multipleKeys = getKeys(key); 173 | 174 | for (j = 0; j < multipleKeys.length; j++) { 175 | keys = multipleKeys[j].split('+'); 176 | 177 | if (keys.length > 1) { 178 | mods = getMods(keys); 179 | } 180 | 181 | key = keys[keys.length - 1]; 182 | key = code(key); 183 | 184 | if (scope === undefined) { 185 | scope = getScope(); 186 | } 187 | if (!_handlers[key]) { 188 | return; 189 | } 190 | for (i = 0; i < _handlers[key].length; i++) { 191 | obj = _handlers[key][i]; 192 | // only clear handlers if correct scope and mods match 193 | if (obj.scope === scope && compareArray(obj.mods, mods)) { 194 | _handlers[key][i] = {}; 195 | } 196 | } 197 | } 198 | }; 199 | 200 | // Returns true if the key with code 'keyCode' is currently down 201 | // Converts strings into key codes. 202 | function isPressed(keyCode) { 203 | if (typeof(keyCode)=='string') { 204 | keyCode = code(keyCode); 205 | } 206 | return index(_downKeys, keyCode) != -1; 207 | } 208 | 209 | function getPressedKeyCodes() { 210 | return _downKeys.slice(0); 211 | } 212 | 213 | function filter(event){ 214 | var tagName = (event.target || event.srcElement).tagName; 215 | // ignore keypressed in any elements that support keyboard data input 216 | return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); 217 | } 218 | 219 | // initialize key. to false 220 | for(k in _MODIFIERS) assignKey[k] = false; 221 | 222 | // set current scope (default 'all') 223 | function setScope(scope){ _scope = scope || 'all' }; 224 | function getScope(){ return _scope || 'all' }; 225 | 226 | // delete all handlers for a given scope 227 | function deleteScope(scope){ 228 | var key, handlers, i; 229 | 230 | for (key in _handlers) { 231 | handlers = _handlers[key]; 232 | for (i = 0; i < handlers.length; ) { 233 | if (handlers[i].scope === scope) handlers.splice(i, 1); 234 | else i++; 235 | } 236 | } 237 | }; 238 | 239 | // abstract key logic for assign and unassign 240 | function getKeys(key) { 241 | var keys; 242 | key = key.replace(/\s/g, ''); 243 | keys = key.split(','); 244 | if ((keys[keys.length - 1]) == '') { 245 | keys[keys.length - 2] += ','; 246 | } 247 | return keys; 248 | } 249 | 250 | // abstract mods logic for assign and unassign 251 | function getMods(key) { 252 | var mods = key.slice(0, key.length - 1); 253 | for (var mi = 0; mi < mods.length; mi++) 254 | mods[mi] = _MODIFIERS[mods[mi]]; 255 | return mods; 256 | } 257 | 258 | // cross-browser events 259 | function addEvent(object, event, method) { 260 | if (object.addEventListener) 261 | object.addEventListener(event, method, false); 262 | else if(object.attachEvent) 263 | object.attachEvent('on'+event, function(){ method(window.event) }); 264 | }; 265 | 266 | // set the handlers globally on document 267 | addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48 268 | addEvent(document, 'keyup', clearModifier); 269 | 270 | // reset modifiers to false whenever the window is (re)focused. 271 | addEvent(window, 'focus', resetModifiers); 272 | 273 | // store previously defined key 274 | var previousKey = global.key; 275 | 276 | // restore previously defined key and return reference to our key object 277 | function noConflict() { 278 | var k = global.key; 279 | global.key = previousKey; 280 | return k; 281 | } 282 | 283 | // set window.key and window.key.set/get/deleteScope, and the default filter 284 | global.key = assignKey; 285 | global.key.setScope = setScope; 286 | global.key.getScope = getScope; 287 | global.key.deleteScope = deleteScope; 288 | global.key.filter = filter; 289 | global.key.isPressed = isPressed; 290 | global.key.getPressedKeyCodes = getPressedKeyCodes; 291 | global.key.noConflict = noConflict; 292 | global.key.unbind = unbindKey; 293 | 294 | if(typeof module !== 'undefined') module.exports = assignKey; 295 | 296 | })(this); 297 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keymaster", 3 | "description": "library for defining and dispatching keyboard shortcuts", 4 | "version": "1.6.3", 5 | "author": "Thomas Fuchs (http://mir.aculo.us)", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/madrobby/keymaster" 9 | }, 10 | "main": "./keymaster.js", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | The Keymaster 4 | 5 | 6 | 7 | 8 | 9 |

10 | The Keymaster 11 |

12 | 13 | 14 | 15 | 16 | 17 |
    18 |
  1. Press 'c'. Nothing should be logged on console.
  2. 19 |
  3. Press 'o' or Enter or Cursor ←. Console should log function call.
  4. 20 |
  5. Press 'i'. Switches scope to 'issues'.
  6. 21 |
  7. Press 'c'. Console should log function call.
  8. 22 |
  9. Press 'o' or Enter or Cursor ←. Console should log function call.
  10. 23 |
  11. Press and hold 'm'. Console should log a message every second.
  12. 24 |
  13. Every second console should log a message listing all the currently pressed keycodes.
  14. 25 |
26 | 27 |

28 | At any time, try pressing ⌘+right, shift+left or ctrl+shift+alt+d. 29 |

30 | 31 |

32 | When a input, a select or a textarea element is focused, key inputs should be ignored. 33 |

34 | 35 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /test/evidence.js: -------------------------------------------------------------------------------- 1 | /* evidence.js, version 0.6 2 | * 3 | * Copyright (c) 2009 Tobie Langel (http://tobielangel.com) 4 | * 5 | * evidence.js is freely distributable under the terms of an MIT-style license. 6 | *--------------------------------------------------------------------------*/ 7 | 8 | (function(global) { 9 | var originalEvidence = global.Evidence, 10 | originalOnload = global.onload; 11 | 12 | function Evidence() { 13 | TestCase.extend.apply(TestCase, arguments); 14 | } 15 | 16 | function noConflict() { 17 | global.Evidence = originalEvidence; 18 | return Evidence; 19 | } 20 | 21 | Evidence.noConflict = noConflict; 22 | Evidence.VERSION = '0.6'; 23 | 24 | var FILE_REGEXP = /.*?\/(\w+\.html)(.*)/; 25 | 26 | function getNameFromFile() { 27 | return (global.location || '').toString().replace(FILE_REGEXP, '$1'); 28 | } 29 | 30 | function chain(subclass, superclass) { 31 | function Subclass() {} 32 | Subclass.prototype = superclass.prototype; 33 | subclass.prototype = new Subclass(); 34 | subclass.prototype.constructor = subclass; 35 | return subclass; 36 | } 37 | 38 | function defer(block, context) { 39 | if ('setTimeout' in global) { 40 | window.setTimeout(function() { 41 | block.call(context); 42 | }, 10); 43 | } else { 44 | block.call(context); 45 | } 46 | } 47 | function AssertionSkippedError(message) { 48 | this.message = message; 49 | } 50 | 51 | AssertionSkippedError.displayName = 'AssertionSkippedError'; 52 | 53 | (function(p) { 54 | p.name = 'AssertionSkippedError'; 55 | })(AssertionSkippedError.prototype); 56 | Evidence.AssertionSkippedError = AssertionSkippedError; 57 | function AssertionFailedError(message, template, args) { 58 | this.message = message; 59 | this.template = template || ''; 60 | this.args = args; 61 | } 62 | 63 | AssertionFailedError.displayName = 'AssertionFailedError'; 64 | 65 | (function(p) { 66 | p.name = 'AssertionFailedError'; 67 | })(AssertionFailedError.prototype); 68 | Evidence.AssertionFailedError = AssertionFailedError; 69 | function AssertionMessage(message, template, args) { 70 | this.message = message.replace(/%/g, '%%'); 71 | this.template = template || ''; 72 | this.args = args; 73 | } 74 | 75 | AssertionMessage.displayName = 'AssertionMessage'; 76 | 77 | (function(p) { 78 | function toString() { 79 | return UI.printf(this.message + this.template, this.args); 80 | } 81 | p.toString = toString; 82 | })(AssertionMessage.prototype); 83 | Evidence.AssertionMessage = AssertionMessage; 84 | 85 | var Assertions = (function() { 86 | function _assertExpression(expression, message, template) { 87 | /*for (var i=0; i < 100000; i++) { 88 | (function(){})() 89 | }*/ 90 | if (expression) { 91 | this.addAssertion(); 92 | } else { 93 | var args = Array.prototype.slice.call(arguments, 3); 94 | throw new AssertionFailedError(message, template, args); 95 | } 96 | } 97 | 98 | function skip(message) { 99 | throw new AssertionSkippedError(message || 'Skipped!'); 100 | } 101 | 102 | function fail(message) { 103 | this._assertExpression(false, message || 'Flunked!'); 104 | } 105 | 106 | function assert(test, message) { 107 | this._assertExpression( 108 | !!test, 109 | message || 'Failed assertion.', 110 | 'Expected %o to evaluate to true.', test 111 | ); 112 | } 113 | 114 | function refute(test, message) { 115 | this._assertExpression( 116 | !test, 117 | message || 'Failed refutation.', 118 | 'Expected %o to evaluate to false.', test 119 | ); 120 | } 121 | 122 | function assertTrue(test, message) { 123 | this._assertExpression( 124 | (test === true), 125 | message || 'Failed assertion.', 126 | 'Expected %o to be true.', test 127 | ); 128 | } 129 | 130 | function refuteTrue(test, message) { 131 | this._assertExpression( 132 | (test !== true), 133 | message || 'Failed refutation.', 134 | 'Expected %o to not be true.', test 135 | ); 136 | } 137 | 138 | function assertNull(test, message) { 139 | this._assertExpression( 140 | (test === null), 141 | message || 'Failed assertion.', 142 | 'Expected %o to be null.', test 143 | ); 144 | } 145 | 146 | function refuteNull(test, message) { 147 | this._assertExpression( 148 | (test !== null), 149 | message || 'Failed refutation.', 150 | 'Expected %o to not be null.', test 151 | ); 152 | } 153 | 154 | function assertUndefined(test, message) { 155 | this._assertExpression( 156 | (typeof test === 'undefined'), 157 | message || 'Failed assertion.', 158 | 'Expected %o to be undefined.', test 159 | ); 160 | } 161 | 162 | function refuteUndefined(test, message) { 163 | this._assertExpression( 164 | (typeof test !== 'undefined'), 165 | message || 'Failed refutation.', 166 | 'Expected %o to not be undefined.', test 167 | ); 168 | } 169 | 170 | function assertFalse(test, message) { 171 | this._assertExpression( 172 | (test === false), 173 | message || 'Failed assertion.', 174 | 'Expected %o to be false.', test 175 | ); 176 | } 177 | 178 | function refuteFalse(test, message) { 179 | this._assertExpression( 180 | (test !== false), 181 | message || 'Failed refutation.', 182 | 'Expected %o to not be false.', test 183 | ); 184 | } 185 | 186 | function assertEqual(expected, actual, message) { 187 | this._assertExpression( 188 | (expected == actual), 189 | message || 'Failed assertion.', 190 | 'Expected %o to be == to %o.', actual, expected 191 | ); 192 | } 193 | 194 | function refuteEqual(expected, actual, message) { 195 | this._assertExpression( 196 | (expected != actual), 197 | message || 'Failed refutation.', 198 | 'Expected %o to be != to %o.', actual, expected 199 | ); 200 | } 201 | 202 | function assertIdentical(expected, actual, message) { 203 | this._assertExpression( 204 | (expected === actual), 205 | message || 'Failed assertion.', 206 | 'Expected %o to be === to %o.', actual, expected 207 | ); 208 | } 209 | 210 | function refuteIdentical(expected, actual, message) { 211 | this._assertExpression( 212 | (expected !== actual), 213 | message || 'Failed refutation.', 214 | 'Expected %o to be !== to %o.', actual, expected 215 | ); 216 | } 217 | 218 | function assertIn(property, object, message) { 219 | this._assertExpression( 220 | (property in object), 221 | message || 'Failed assertion.', 222 | 'Expected "%s" to be a property of %o.', property, object 223 | ); 224 | } 225 | 226 | function refuteIn(property, object, message) { 227 | this._assertExpression( 228 | !(property in object), 229 | message || 'Failed refutation.', 230 | 'Expected "%s" to not be a property of %o.', property, object 231 | ); 232 | } 233 | 234 | return { 235 | _assertExpression: _assertExpression, 236 | skip: skip, 237 | assert: assert, 238 | refute: refute, 239 | assertNot: refute, 240 | assertTrue: assertTrue, 241 | assertNull: assertNull, 242 | assertUndefined: assertUndefined, 243 | assertFalse: assertFalse, 244 | assertIdentical: assertIdentical, 245 | refuteIdentical: refuteIdentical, 246 | assertEqual: assertEqual, 247 | refuteEqual: refuteEqual, 248 | assertIn: assertIn, 249 | refuteIn: refuteIn, 250 | fail: fail, 251 | flunk: fail 252 | }; 253 | })(); 254 | Evidence.Assertions = Assertions; 255 | function TestCase(methodName) { 256 | this._methodName = methodName; 257 | this.name = methodName; 258 | } 259 | 260 | (function() { 261 | function extend(name, methods) { 262 | function TestCaseSubclass(methodName) { 263 | TestCase.call(this, methodName); 264 | } 265 | 266 | if (!methods) { 267 | methods = name; 268 | name = getNameFromFile(); 269 | } 270 | 271 | chain(TestCaseSubclass, this); 272 | TestCaseSubclass.displayName = name; 273 | TestCaseSubclass.extend = extend; 274 | 275 | for(var prop in methods) { 276 | TestCaseSubclass.prototype[prop] = methods[prop]; 277 | } 278 | TestCase.subclasses.push(TestCaseSubclass); 279 | return TestCaseSubclass; 280 | } 281 | 282 | function AssertionsMixin() {} 283 | AssertionsMixin.prototype = Assertions; 284 | TestCase.prototype = new AssertionsMixin(); 285 | TestCase.constructor = TestCase; 286 | 287 | TestCase.displayName = 'TestCase'; 288 | TestCase.extend = extend; 289 | TestCase.subclasses = []; 290 | TestCase.defaultTimeout = 10000; 291 | })(); 292 | 293 | (function(p) { 294 | function run(result) { 295 | if (result) { this._result = result; } 296 | try { 297 | if (this._nextAssertions) { 298 | this._result.restartTest(this); 299 | this._nextAssertions(this); 300 | } else { 301 | /*this._globalProperties = objectKeys(global);*/ 302 | this._result.startTest(this); 303 | this.setUp(this); 304 | this[this._methodName](this); 305 | } 306 | } catch(e) { 307 | this._filterException(e); 308 | } finally { 309 | if (this._paused) { 310 | this._result.pauseTest(this); 311 | } else { 312 | try { 313 | this.tearDown(this); 314 | } catch(e) { 315 | this._filterException(e); 316 | } finally { 317 | this._nextAssertions = null; 318 | this._result.stopTest(this); 319 | defer(function() { 320 | this.parent.next(); 321 | }, this); 322 | } 323 | } 324 | } 325 | } 326 | 327 | function _filterException(e) { 328 | var name = e.name; 329 | switch(name) { 330 | case 'AssertionFailedError': 331 | this._result.addFailure(this, e); 332 | break; 333 | case 'AssertionSkippedError': 334 | this._result.addSkip(this, e); 335 | break; 336 | default: 337 | this._result.addError(this, e); 338 | } 339 | } 340 | 341 | function pause(assertions) { 342 | this._paused = true; 343 | var self = this; 344 | if (assertions) { this._nextAssertions = assertions; } 345 | self._timeoutId = global.setTimeout(function() { 346 | self.resume(function() { 347 | self.fail('Test timed out. Testing was not resumed after being paused.'); 348 | }); 349 | }, TestCase.defaultTimeout); 350 | } 351 | 352 | function resume(assertions) { 353 | if (this._paused) { // avoid race conditions 354 | this._paused = false; 355 | global.clearTimeout(this._timeoutId); 356 | if (assertions) { this._nextAssertions = assertions; } 357 | this.run(); 358 | } 359 | } 360 | 361 | function size() { 362 | return 1; 363 | } 364 | 365 | function toString() { 366 | return this.constructor.displayName + '#' + this.name; 367 | } 368 | 369 | function addAssertion() { 370 | this._result.addAssertion(); 371 | } 372 | 373 | p.run = run; 374 | p.addAssertion = addAssertion; 375 | p._filterException = _filterException; 376 | p.pause = pause; 377 | p.resume = resume; 378 | p.size = size; 379 | p.toString = toString; 380 | p.setUp = function() {}; 381 | p.tearDown = function() {}; 382 | })(TestCase.prototype); 383 | Evidence.TestCase = TestCase; 384 | function TestSuite(name, tests) { 385 | this.name = name; 386 | this._tests = []; 387 | if (tests) { 388 | this.push.apply(this, tests); 389 | } 390 | } 391 | 392 | TestSuite.displayName = 'TestSuite'; 393 | 394 | (function(p) { 395 | function run(result) { 396 | this._index = 0; 397 | this._result = result; 398 | result.startSuite(this); 399 | this.next(); 400 | return result; 401 | } 402 | 403 | function next() { 404 | var next = this._tests[this._index]; 405 | if (next) { 406 | this._index++; 407 | next.run(this._result); 408 | } else { 409 | this._result.stopSuite(this); 410 | if (this.parent) { 411 | this.parent.next(); 412 | } else { 413 | this._result.stop(new Date()); 414 | } 415 | } 416 | } 417 | 418 | function push() { 419 | for (var i = 0, length = arguments.length; i < length; i++) { 420 | var test = arguments[i]; 421 | test.parent = this; 422 | this._tests.push(test); 423 | } 424 | } 425 | 426 | function addTest(test) { 427 | test.parent = this; 428 | this._tests.push(test); 429 | } 430 | 431 | function addTests(tests) { 432 | for (var i = 0, length = tests.length; i < length; i++) { 433 | this.addTest(tests[i]); 434 | } 435 | } 436 | 437 | function size() { 438 | var tests = this._tests, 439 | length = tests.length, 440 | sum = 0; 441 | 442 | for (var i = 0; i < length; i++) { 443 | sum += tests[i].size(); 444 | } 445 | return sum; 446 | } 447 | 448 | function isEmpty() { 449 | return this.size() === 0; 450 | } 451 | 452 | function toString() { 453 | return this.name; 454 | } 455 | p.run = run; 456 | p.next = next; 457 | p.push = push; 458 | p.size = size; 459 | p.isEmpty = isEmpty; 460 | p.toString = toString; 461 | })(TestSuite.prototype); 462 | Evidence.TestSuite = TestSuite; 463 | function TestRunner() { 464 | } 465 | 466 | TestRunner.displayName = 'TestRunner'; 467 | 468 | (function(p) { 469 | function run(suite) { 470 | suite.parent = null; 471 | var result = this._makeResult(); 472 | result.start(new Date()); 473 | suite.run(result); 474 | return result; 475 | } 476 | 477 | function _makeResult() { 478 | return new TestResult(); 479 | } 480 | 481 | p.run = run; 482 | p._makeResult = _makeResult; 483 | })(TestRunner.prototype); 484 | Evidence.TestRunner = TestRunner; 485 | function TestLoader() { 486 | } 487 | 488 | TestLoader.displayName = 'TestLoader'; 489 | 490 | (function(p) { 491 | function loadTestsFromTestCase(testcaseClass) { 492 | var suite = new TestSuite(testcaseClass.displayName), 493 | props = this.getTestCaseNames(testcaseClass); 494 | for (var i=0; i < props.length; i++) { 495 | suite.push(new testcaseClass(props[i])); 496 | } 497 | return suite; 498 | } 499 | 500 | function loadTestsFromTestCases(testcases) { 501 | var suite = new TestSuite(getNameFromFile()); 502 | for (var i = 0; i < testcases.length; i++) { 503 | var testcase = testcases[i]; 504 | var subSuite = defaultLoader.loadTestsFromTestCase(testcase); 505 | if (!subSuite.isEmpty()) { suite.push(subSuite); } 506 | } 507 | return suite; 508 | } 509 | 510 | function getTestCaseNames(testcaseClass) { 511 | var results = [], 512 | proto = testcaseClass.prototype, 513 | prefix = this.testMethodPrefix; 514 | 515 | for (var property in proto) { 516 | if (property.indexOf(prefix) === 0) { 517 | results.push(property); 518 | } 519 | } 520 | return results.sort(); 521 | } 522 | 523 | function loadRegisteredTestCases() { 524 | return loadTestsFromTestCases(TestCase.subclasses); 525 | } 526 | 527 | p.loadTestsFromTestCase = loadTestsFromTestCase; 528 | p.loadRegisteredTestCases = loadRegisteredTestCases; 529 | p.loadTestsFromTestCases = loadTestsFromTestCases; 530 | p.testMethodPrefix = 'test'; 531 | p.getTestCaseNames = getTestCaseNames; 532 | 533 | })(TestLoader.prototype); 534 | Evidence.TestLoader = TestLoader; 535 | function AutoRunner() { 536 | if (global.console && global.console.log) { 537 | this.logger = Logger; 538 | } else if (Object.prototype.toString.call(global.environment) === '[object Environment]' && global.print) { 539 | this.logger = CommandLineLogger; 540 | } else { 541 | this.logger = PopupLogger; 542 | } 543 | this.autoRun = true; 544 | this.verbosity = Logger.INFO; 545 | this.runner = ConsoleTestRunner; 546 | } 547 | 548 | (function() { 549 | function run(options) { 550 | var autoRunner = new this(); 551 | options = options || autoRunner.retrieveOptions(); 552 | autoRunner.processOptions(options); 553 | if (autoRunner.autoRun) { autoRunner.run() }; 554 | } 555 | 556 | AutoRunner.run = run; 557 | AutoRunner.displayName = 'AutoRunner'; 558 | AutoRunner.LOGGERS = { 559 | console: Logger, 560 | popup: PopupLogger, 561 | command_line: CommandLineLogger 562 | }; 563 | 564 | AutoRunner.RUNNERS = { 565 | console: ConsoleTestRunner 566 | }; 567 | })(); 568 | 569 | (function(p) { 570 | function run() { 571 | var logger = new this.logger(this.verbosity), 572 | runner = new this.runner(logger), 573 | suite = defaultLoader.loadRegisteredTestCases(); 574 | if (suite._tests.length <= 1) { 575 | suite = suite._tests[0]; 576 | } 577 | return runner.run(suite); 578 | } 579 | 580 | function processQueryString(str) { 581 | var results = {}; 582 | str = (str + '').match(/^(?:[^?#]*\?)([^#]+?)(?:#.*)?$/); 583 | str = str && str[1]; 584 | 585 | if (!str) { return results; } 586 | 587 | var pairs = str.split('&'), 588 | length = pairs.length; 589 | if (!length) { return results; } 590 | 591 | for (var i = 0; i < length; i++) { 592 | var pair = pairs[i].split('='), 593 | key = decodeURIComponent(pair[0]), 594 | value = pair[1]; 595 | value = value ? decodeURIComponent(value) : true; 596 | results[key] = value; 597 | } 598 | return results; 599 | } 600 | 601 | function processArguments(args) { // RHINO 602 | var results = {}; 603 | 604 | for (var i = 0; i < args.length; i++) { 605 | var arg = args[i]; 606 | if (arg.indexOf('-') === 0) { 607 | var value = args[i + 1]; 608 | if (value && value.indexOf('-') !== 0) { 609 | i++; 610 | } else { 611 | value = true; 612 | } 613 | results[arg.substr(1)] = value; 614 | } 615 | } 616 | return results; 617 | } 618 | 619 | function retrieveOptions() { 620 | if (global.location) { 621 | return this.processQueryString(global.location); 622 | } 623 | if (global.arguments) { 624 | return this.processArguments(global.arguments); 625 | } 626 | return {}; 627 | } 628 | 629 | function processOptions(options) { 630 | for(var key in options) { 631 | var value = options[key]; 632 | switch(key) { 633 | case 'timeout': 634 | TestCase.defaultTimeout = global.parseFloat(value) * 1000; 635 | break; 636 | case 'run': 637 | this.autoRun = value === 'false' ? false : true; 638 | break; 639 | case 'logger': 640 | this.logger = AutoRunner.LOGGERS[value]; 641 | break; 642 | case 'verbosity': 643 | var i = global.parseInt(value); 644 | this.verbosity = global.isNaN(i) ? Logger[value] : i; 645 | break; 646 | case 'runner': 647 | this.runner = AutoRunner.RUNNERS[value]; 648 | break; 649 | } 650 | } 651 | } 652 | 653 | p.run = run; 654 | p.processQueryString = processQueryString; 655 | p.processArguments = processArguments; 656 | p.retrieveOptions = retrieveOptions; 657 | p.processOptions = processOptions; 658 | })(AutoRunner.prototype); 659 | Evidence.AutoRunner = AutoRunner; 660 | function TestResult() { 661 | this.testCount = 0; 662 | this.assertionCount = 0; 663 | this.skipCount = 0; 664 | this.skips = []; 665 | this.failureCount = 0; 666 | this.failures = []; 667 | this.errors = []; 668 | this.errorCount = 0; 669 | this.testCount = 0; 670 | } 671 | 672 | TestResult.displayName = 'TestResult'; 673 | 674 | (function(p) { 675 | function addAssertion() { 676 | this.assertionCount++; 677 | } 678 | 679 | function addSkip(testcase, reason) { 680 | this.skipCount++; 681 | this.skips.push(reason); 682 | } 683 | 684 | function addFailure(testcase, reason) { 685 | this.failureCount++; 686 | this.failures.push(reason); 687 | } 688 | 689 | function addError(testcase, error) { 690 | this.errorCount++; 691 | this.errors.push(error); 692 | } 693 | 694 | function startTest(testcase) { 695 | this.testCount++; 696 | } 697 | 698 | function stopTest(testcase) {} 699 | 700 | function pauseTest(testcase) {} 701 | 702 | function restartTest(testcase) {} 703 | 704 | function startSuite(suite) {} 705 | 706 | function stopSuite(suite) {} 707 | 708 | function start(t0) { 709 | this.t0 = t0; 710 | } 711 | 712 | function stop(t1) { 713 | this.t1 = t1; 714 | } 715 | 716 | function toString() { 717 | return this.testCount + ' tests, ' + 718 | this.assertionCount + ' assertions, ' + 719 | this.failureCount + ' failures, ' + 720 | this.errorCount + ' errors, ' + 721 | this.skipCount + ' skips'; 722 | } 723 | 724 | p.addAssertion = addAssertion; 725 | p.addSkip = addSkip; 726 | p.addFailure = addFailure; 727 | p.addError = addError; 728 | p.startTest = startTest; 729 | p.stopTest = stopTest; 730 | p.pauseTest = pauseTest; 731 | p.restartTest = restartTest; 732 | p.startSuite = startSuite; 733 | p.stopSuite = stopSuite; 734 | p.start = start; 735 | p.stop = stop; 736 | p.toString = toString; 737 | })(TestResult.prototype); 738 | Evidence.TestResult = TestResult; 739 | var Console = {}; 740 | 741 | function Logger(level) { 742 | if (typeof level !== 'undefined') { 743 | this.level = level; 744 | } 745 | } 746 | 747 | Logger.displayName = 'Logger'; 748 | Logger.LEVELS = ['NOTSET', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL']; 749 | Logger.CRITICAL = 5; 750 | Logger.ERROR = 4; 751 | Logger.WARN = 3; 752 | Logger.INFO = 2; 753 | Logger.DEBUG = 1; 754 | Logger.NOTSET = 0; 755 | 756 | (function(p) { 757 | function critical(template, params) { 758 | this.log(Logger.CRITICAL, template, params); 759 | } 760 | 761 | function error(template, params) { 762 | this.log(Logger.ERROR, template, params); 763 | } 764 | 765 | function warn(template, params) { 766 | this.log(Logger.WARN, template, params); 767 | } 768 | 769 | function info(template, params) { 770 | this.log(Logger.INFO, template, params); 771 | } 772 | 773 | function debug(template, params) { 774 | this.log(Logger.DEBUG, template, params); 775 | } 776 | 777 | function log(level, template, params) { 778 | level = level || Logger.NOTSET; 779 | var c = global.console; 780 | 781 | var method = Logger.LEVELS[level].toLowerCase(); 782 | if (method === 'critical') { method = 'error'; } 783 | method = (method in c) ? method : 'log'; 784 | 785 | if (level >= this.level) { 786 | if (params) { 787 | params = params.slice(0); 788 | params.unshift(template); 789 | c[method].apply(c, params); 790 | } else { 791 | c[method](template); 792 | } 793 | } 794 | } 795 | 796 | p.log = log; 797 | p.critical = critical; 798 | p.error = error; 799 | p.warn = warn; 800 | p.info = info; 801 | p.debug = debug; 802 | p.level = 0; 803 | })(Logger.prototype); 804 | Console.Logger = Logger; 805 | function PopupLogger(level) { 806 | Logger.call(this, level); 807 | } 808 | 809 | chain(PopupLogger, Logger); 810 | PopupLogger.displayName = 'PopupLogger'; 811 | 812 | (function(p) { 813 | var BASIC_STYLES = 'color: #333; background-color: #fff; font-family: monospace; border-bottom: 1px solid #ccc;'; 814 | var STYLES = { 815 | WARN: 'color: #000; background-color: #fc6;', 816 | ERROR: 'color: #f00; background-color: #fcc;', 817 | CRITICAL: 'color: #fff; background-color: #000;' 818 | }; 819 | 820 | function _cleanup(html) { 821 | return html.replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&').replace(/[\n\r]+/, '
'); 822 | } 823 | 824 | function _makePopup() { 825 | var popup = global.open('','popup','height=400,width=400'); 826 | var doc = popup.document; 827 | doc.write('\ 828 | \ 829 | \ 830 | \ 831 | Console\ 832 | \ 833 |
\ 834 | '); 835 | doc.close(); 836 | popup.focus(); 837 | return popup; 838 | } 839 | 840 | function _appendLine(level, msg) { 841 | this.popup = this.popup || this._makePopup(); 842 | var levelName = Logger.LEVELS[level]; 843 | 844 | var html = '
'; 848 | if (level > Logger.INFO) { 849 | html += ''; 850 | html += levelName; 851 | html += ': '; 852 | } 853 | html += _cleanup(msg); 854 | html += '
'; 855 | var doc = this.popup.document, 856 | div = doc.createElement('div'); 857 | div.innerHTML = html; 858 | html = div.firstChild; 859 | div = null; 860 | doc.getElementById('evidence_console').appendChild(html); 861 | } 862 | 863 | function log(level, msg, params) { 864 | level = level || Logger.NOTSET; 865 | if (level >= this.level) { 866 | if (params) { 867 | msg = UI.printf(msg, params); 868 | } 869 | this._appendLine(level, msg); 870 | } 871 | } 872 | 873 | p.log = log; 874 | p._makePopup = _makePopup; 875 | p._appendLine = _appendLine; 876 | })(PopupLogger.prototype); 877 | Console.PopupLogger = PopupLogger; 878 | function CommandLineLogger(level) { 879 | Logger.call(this, level); 880 | } 881 | 882 | chain(CommandLineLogger, Logger); 883 | CommandLineLogger.displayName = 'CommandLineLogger'; 884 | 885 | (function(p) { 886 | 887 | function log(level, msg, params) { 888 | level = level || Logger.NOTSET; 889 | if (level >= this.level) { 890 | var prefix = ''; 891 | if (level > Logger.INFO) { 892 | prefix = Logger.LEVELS[level]+ ': '; 893 | } 894 | if (params) { 895 | msg = UI.printf(msg, params); 896 | } 897 | global.print(prefix + msg); 898 | } 899 | } 900 | 901 | p.log = log; 902 | })(CommandLineLogger.prototype); 903 | Console.CommandLineLogger = CommandLineLogger; 904 | function ConsoleTestRunner(logger) { 905 | TestRunner.call(this); 906 | this.logger = logger; 907 | } 908 | 909 | chain(ConsoleTestRunner, TestRunner); 910 | ConsoleTestRunner.displayName = 'ConsoleTestRunner'; 911 | 912 | (function(p) { 913 | function _makeResult() { 914 | return new ConsoleTestResult(this.logger); 915 | } 916 | 917 | p._makeResult = _makeResult; 918 | })(ConsoleTestRunner.prototype); 919 | Console.TestRunner = ConsoleTestRunner; 920 | function ConsoleTestResult(logger) { 921 | TestResult.call(this); 922 | this.logger = logger; 923 | } 924 | 925 | chain(ConsoleTestResult, TestResult); 926 | ConsoleTestResult.displayName = 'ConsoleTestResult'; 927 | 928 | (function(p) { 929 | var _super = TestResult.prototype; 930 | 931 | function addAssertion() { 932 | this.assertionCount++; 933 | } 934 | 935 | function addSkip(testcase, msg) { 936 | _super.addSkip.call(this, testcase, msg); 937 | this.logger.warn('Skipping testcase ' + testcase + ': ' + msg.message); 938 | } 939 | 940 | function addFailure(testcase, msg) { 941 | _super.addFailure.call(this, testcase, msg); 942 | this.logger.error(testcase + ': ' + msg.message + ' ' + msg.template, msg.args); 943 | } 944 | 945 | function addError(testcase, error) { 946 | _super.addError.call(this, testcase, error); 947 | this.logger.error(testcase + ' threw an error. ' + error); 948 | } 949 | 950 | function startTest(testcase) { 951 | _super.startTest.call(this, testcase); 952 | this.logger.debug('Started testcase ' + testcase + '.'); 953 | } 954 | 955 | function stopTest(testcase) { 956 | this.logger.debug('Completed testcase ' + testcase + '.'); 957 | } 958 | 959 | function pauseTest(testcase) { 960 | this.logger.info('Paused testcase ' + testcase + '.'); 961 | } 962 | 963 | function restartTest(testcase) { 964 | this.logger.info('Restarted testcase ' + testcase + '.'); 965 | } 966 | 967 | function startSuite(suite) { 968 | this.logger.info('Started suite ' + suite + '.'); 969 | } 970 | 971 | function stopSuite(suite) { 972 | this.logger.info('Completed suite ' + suite + '.'); 973 | } 974 | 975 | function start(t0) { 976 | _super.start.call(this, t0); 977 | this.logger.info('Started tests.'); 978 | } 979 | 980 | function stop(t1) { 981 | _super.stop.call(this, t1); 982 | this.logger.info('Completed tests in ' + ((t1 - this.t0)/1000) + 's.'); 983 | this.logger.info(this.toString() + '.'); 984 | } 985 | 986 | p.addAssertion = addAssertion; 987 | p.addSkip = addSkip; 988 | p.addFailure = addFailure; 989 | p.addError = addError; 990 | p.startTest = startTest; 991 | p.stopTest = stopTest; 992 | p.pauseTest = pauseTest; 993 | p.restartTest = restartTest; 994 | p.startSuite = startSuite; 995 | p.stopSuite = stopSuite; 996 | p.start = start; 997 | p.stop = stop; 998 | })(ConsoleTestResult.prototype); 999 | 1000 | 1001 | Console.TestResult = ConsoleTestResult; 1002 | var UI = (function() { 1003 | function printf(template, args, inspector) { 1004 | var parts = [], 1005 | regexp = /(^%|.%)([a-zA-Z])/, 1006 | args = args.splice(0); // clone args 1007 | 1008 | inspector = inspector || String; 1009 | 1010 | if (template.length <= 0) { 1011 | return ''; 1012 | } 1013 | while (m = regexp.exec(template)) { 1014 | var match = m[0], index = m.index, type, arg; 1015 | 1016 | if (match.indexOf('%%') === 0) { 1017 | parts.push(template.substr(0, index)); 1018 | parts.push(match.substr(1)); 1019 | } else { 1020 | parts.push(template.substr(0, match.indexOf('%' === 0) ? index + 1 : index)); 1021 | type = m[2]; 1022 | arg = args.shift(); 1023 | arg = inspector(arg, type); 1024 | parts.push(arg); 1025 | } 1026 | template = template.substr(index + match.length); 1027 | } 1028 | parts.push(template); 1029 | return parts.join(''); 1030 | } 1031 | 1032 | return { 1033 | printf: printf, 1034 | Console: Console 1035 | }; 1036 | })(); 1037 | Evidence.UI = UI; 1038 | 1039 | var defaultLoader = new TestLoader(); 1040 | Evidence.defaultLoader = defaultLoader; 1041 | 1042 | global.Evidence = Evidence; 1043 | 1044 | if (global.location) { 1045 | global.onload = function() { 1046 | if (typeof originalOnload === 'function') { 1047 | originalOnload.call(global); 1048 | } 1049 | AutoRunner.run(); 1050 | }; 1051 | } else if (global.arguments) { 1052 | var runtime = java.lang.Runtime.getRuntime(); 1053 | var thread = new java.lang.Thread(function() { 1054 | AutoRunner.run(); 1055 | }); 1056 | runtime.addShutdownHook(thread); 1057 | } 1058 | 1059 | })(this); 1060 | -------------------------------------------------------------------------------- /test/keymaster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Keymaster unit tests 6 | 7 | 8 | 9 | 10 |

Keymaster unit tests

11 |

12 | See the browser console for results. 13 |

14 | 15 | 16 | 17 | 18 | 19 | 456 | 457 | 458 | --------------------------------------------------------------------------------