]> git.proxmox.com Git - mirror_novnc.git/commitdiff
Move keyboard handling in to Keyboard class
authorPierre Ossman <ossman@cendio.se>
Fri, 27 Jan 2017 09:36:10 +0000 (10:36 +0100)
committerPierre Ossman <ossman@cendio.se>
Thu, 4 May 2017 10:13:47 +0000 (12:13 +0200)
Replace the multi stage pipeline system with something simpler.
That level of abstraction is not needed.

core/input/devices.js
core/input/util.js
tests/test.keyboard.js

index 2308be9c3ef9ea0f9e9034c0eacdba38b95753d0..f981c6f9fa0a13367f0575f6627bbbcfee73e6cd 100644 (file)
@@ -22,18 +22,13 @@ const Keyboard = function (defaults) {
     this._keyDownList = [];         // List of depressed keys
                                     // (even if they are happy)
 
+    this._modifierState = KeyboardUtil.ModifierSync();
+
     set_defaults(this, defaults, {
         'target': document,
         'focused': true
     });
 
-    // create the keyboard handler
-    this._handler = new KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(),
-        KeyboardUtil.TrackKeyState(
-            KeyboardUtil.EscapeModifiers(this._handleRfbEvent.bind(this))
-        )
-    );
-
     // keep these here so we can refer to them later
     this._eventHandlers = {
         'keyup': this._handleKeyUp.bind(this),
@@ -46,47 +41,220 @@ const Keyboard = function (defaults) {
 Keyboard.prototype = {
     // private methods
 
-    _handleRfbEvent: function (e) {
-        if (this._onKeyEvent) {
-            Log.Debug("onKeyEvent " + (e.type == 'keydown' ? "down" : "up") +
-                      ", keysym: " + e.keysym);
-            this._onKeyEvent(e.keysym, e.code, e.type == 'keydown');
+    _sendKeyEvent: function (keysym, code, down) {
+        if (!this._onKeyEvent) {
+            return;
+        }
+
+        Log.Debug("onKeyEvent " + (down ? "down" : "up") +
+                  ", keysym: " + keysym, ", code: " + code);
+
+        this._onKeyEvent(keysym, code, down);
+    },
+
+    _getKeyCode: function (e) {
+        var code = KeyboardUtil.getKeycode(e);
+        if (code === 'Unidentified') {
+            // Unstable, but we don't have anything else to go on
+            // (don't use it for 'keypress' events thought since
+            // WebKit sets it to the same as charCode)
+            if (e.keyCode && (e.type !== 'keypress')) {
+                code = 'Platform' + e.keyCode;
+            }
         }
+
+        return code;
     },
 
     _handleKeyDown: function (e) {
         if (!this._focused) { return; }
 
-        if (this._handler.keydown(e)) {
-            // Suppress bubbling/default actions
+        this._modifierState.keydown(e);
+
+        var code = this._getKeyCode(e);
+        var keysym = KeyboardUtil.getKeysym(e);
+
+        // If this is a legacy browser then we'll need to wait for
+        // a keypress event as well. Otherwise we supress the
+        // browser's handling at this point
+        if (keysym) {
             stopEvent(e);
+        }
+
+        // if a char modifier is pressed, get the keys it consists
+        // of (on Windows, AltGr is equivalent to Ctrl+Alt)
+        var active = this._modifierState.activeCharModifier();
+
+        // If we have a char modifier down, and we're able to
+        // determine a keysym reliably then (a) we know to treat
+        // the modifier as a char modifier, and (b) we'll have to
+        // "escape" the modifier to undo the modifier when sending
+        // the char.
+        if (active && keysym) {
+            var isCharModifier = false;
+            for (var i  = 0; i < active.length; ++i) {
+                if (active[i] === keysym) {
+                    isCharModifier = true;
+                }
+            }
+            if (!isCharModifier) {
+                var escape = this._modifierState.activeCharModifier();
+            }
+        }
+
+        var last;
+        if (this._keyDownList.length === 0) {
+            last = null;
         } else {
-            // Allow the event to bubble and become a keyPress event which
-            // will have the character code translated
+            last = this._keyDownList[this._keyDownList.length-1];
+        }
+
+        // insert a new entry if last seen key was different.
+        if (!last || code === 'Unidentified' || last.code !== code) {
+            last = {code: code, keysyms: {}};
+            this._keyDownList.push(last);
+        }
+
+        // Wait for keypress?
+        if (!keysym) {
+            return;
+        }
+
+        // make sure last event contains this keysym (a single "logical" keyevent
+        // can cause multiple key events to be sent to the VNC server)
+        last.keysyms[keysym] = keysym;
+        last.ignoreKeyPress = true;
+
+        // undo modifiers
+        if (escape) {
+            for (var i = 0; i < escape.length; ++i) {
+                this._sendKeyEvent(escape[i], 'Unidentified', false);
+            }
+        }
+
+        // send the character event
+        this._sendKeyEvent(keysym, code, true);
+
+        // redo modifiers
+        if (escape) {
+            for (i = 0; i < escape.length; ++i) {
+                this._sendKeyEvent(escape[i], 'Unidentified', true);
+            }
         }
     },
 
+    // Legacy event for browsers without code/key
     _handleKeyPress: function (e) {
         if (!this._focused) { return; }
 
-        if (this._handler.keypress(e)) {
-            // Suppress bubbling/default actions
-            stopEvent(e);
+        stopEvent(e);
+
+        var code = this._getKeyCode(e);
+        var keysym = KeyboardUtil.getKeysym(e);
+
+        // if a char modifier is pressed, get the keys it consists
+        // of (on Windows, AltGr is equivalent to Ctrl+Alt)
+        var active = this._modifierState.activeCharModifier();
+
+        // If we have a char modifier down, and we're able to
+        // determine a keysym reliably then (a) we know to treat
+        // the modifier as a char modifier, and (b) we'll have to
+        // "escape" the modifier to undo the modifier when sending
+        // the char.
+        if (active && keysym) {
+            var isCharModifier = false;
+            for (var i  = 0; i < active.length; ++i) {
+                if (active[i] === keysym) {
+                    isCharModifier = true;
+                }
+            }
+            if (!isCharModifier) {
+                var escape = this._modifierState.activeCharModifier();
+            }
+        }
+
+        var last;
+        if (this._keyDownList.length === 0) {
+            last = null;
+        } else {
+            last = this._keyDownList[this._keyDownList.length-1];
+        }
+
+        if (!last) {
+            last = {code: code, keysyms: {}};
+            this._keyDownList.push(last);
+        }
+        if (!keysym) {
+            console.log('keypress with no keysym:', e);
+            return;
+        }
+
+        // If we didn't expect a keypress, and already sent a keydown to the VNC server
+        // based on the keydown, make sure to skip this event.
+        if (last.ignoreKeyPress) {
+            return;
+        }
+
+        last.keysyms[keysym] = keysym;
+
+        // undo modifiers
+        if (escape) {
+            for (var i = 0; i < escape.length; ++i) {
+                this._sendKeyEvent(escape[i], 'Unidentified', false);
+            }
+        }
+
+        // send the character event
+        this._sendKeyEvent(keysym, code, true);
+
+        // redo modifiers
+        if (escape) {
+            for (i = 0; i < escape.length; ++i) {
+                this._sendKeyEvent(escape[i], 'Unidentified', true);
+            }
         }
     },
 
     _handleKeyUp: function (e) {
         if (!this._focused) { return; }
 
-        if (this._handler.keyup(e)) {
-            // Suppress bubbling/default actions
-            stopEvent(e);
+        stopEvent(e);
+
+        this._modifierState.keyup(e);
+
+        var code = this._getKeyCode(e);
+
+        if (this._keyDownList.length === 0) {
+            return;
+        }
+        var idx = null;
+        // do we have a matching key tracked as being down?
+        for (var i = 0; i !== this._keyDownList.length; ++i) {
+            if (this._keyDownList[i].code === code) {
+                idx = i;
+                break;
+            }
+        }
+        // if we couldn't find a match (it happens), assume it was the last key pressed
+        if (idx === null) {
+            idx = this._keyDownList.length - 1;
+        }
+
+        var item = this._keyDownList.splice(idx, 1)[0];
+        for (var key in item.keysyms) {
+            this._sendKeyEvent(item.keysyms[key], code, false);
         }
     },
 
     _allKeysUp: function () {
         Log.Debug(">> Keyboard.allKeysUp");
-        this._handler.releaseAll();
+        for (var i = 0; i < this._keyDownList.length; i++) {
+            var item = this._keyDownList[i];
+            for (var key in item.keysyms) {
+                this._sendKeyEvent(item.keysyms[key], 'Unidentified', false);
+            }
+        };
+        this._keyDownList = [];
         Log.Debug("<< Keyboard.allKeysUp");
     },
 
index e5363a52aa527db335c4dd8a4b9b38a7058588a0..110526a3e04ee6feee2bbee3a7555c2d012d5515 100644 (file)
@@ -254,187 +254,3 @@ export function getKeysym(evt){
 
     return null;
 }
-
-// Takes a DOM keyboard event and:
-// - determines which keysym it represents
-// - determines a code identifying the key that was pressed (corresponding to the code/keyCode properties on the DOM event)
-// - marks each event with an 'escape' property if a modifier was down which should be "escaped"
-// This information is collected into an object which is passed to the next() function. (one call per event)
-export function KeyEventDecoder (modifierState, next) {
-    "use strict";
-    function process(evt, type) {
-        var result = {type: type};
-        var code = getKeycode(evt);
-        if (code === 'Unidentified') {
-            // Unstable, but we don't have anything else to go on
-            // (don't use it for 'keypress' events thought since
-            // WebKit sets it to the same as charCode)
-            if (evt.keyCode && (evt.type !== 'keypress')) {
-                code = 'Platform' + evt.keyCode;
-            }
-        }
-        result.code = code;
-
-        var keysym = getKeysym(evt);
-
-        // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
-        // "special" keys like enter, tab or backspace don't send keypress events,
-        // and some browsers don't send keypresses at all if a modifier is down
-        if (keysym) {
-            result.keysym = keysym;
-        }
-
-        // Should we prevent the browser from handling the event?
-        // Doing so on a keydown (in most browsers) prevents keypress from being generated
-        // so only do that if we have to.
-        var suppress = type !== 'keydown' || !!keysym;
-
-        // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
-        var active = modifierState.activeCharModifier();
-
-        // If we have a char modifier down, and we're able to determine a keysym reliably
-        // then (a) we know to treat the modifier as a char modifier,
-        // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
-        if (active && keysym) {
-            var isCharModifier = false;
-            for (var i  = 0; i < active.length; ++i) {
-                if (active[i] === keysym) {
-                    isCharModifier = true;
-                }
-            }
-            if (type === 'keypress' && !isCharModifier) {
-                result.escape = modifierState.activeCharModifier();
-            }
-        }
-
-        next(result);
-
-        return suppress;
-    }
-
-    return {
-        keydown: function(evt) {
-            modifierState.keydown(evt);
-            return process(evt, 'keydown');
-        },
-        keypress: function(evt) {
-            return process(evt, 'keypress');
-        },
-        keyup: function(evt) {
-            modifierState.keyup(evt);
-            return process(evt, 'keyup');
-        },
-        releaseAll: function() { next({type: 'releaseall'}); }
-    };
-};
-
-// Keeps track of which keys we (and the server) believe are down
-// When a keyup is received, match it against this list, to determine the corresponding keysym(s)
-// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
-// key repeat events should be merged into a single entry.
-// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
-export function TrackKeyState (next) {
-    "use strict";
-    var state = [];
-
-    return function (evt) {
-        var last = state.length !== 0 ? state[state.length-1] : null;
-
-        switch (evt.type) {
-        case 'keydown':
-            // insert a new entry if last seen key was different.
-            if (!last || evt.code === 'Unidentified' || last.code !== evt.code) {
-                last = {code: evt.code, keysyms: {}};
-                state.push(last);
-            }
-            if (evt.keysym) {
-                // make sure last event contains this keysym (a single "logical" keyevent
-                // can cause multiple key events to be sent to the VNC server)
-                last.keysyms[evt.keysym] = evt.keysym;
-                last.ignoreKeyPress = true;
-                next(evt);
-            }
-            break;
-        case 'keypress':
-            if (!last) {
-                last = {code: evt.code, keysyms: {}};
-                state.push(last);
-            }
-            if (!evt.keysym) {
-                console.log('keypress with no keysym:', evt);
-            }
-
-            // If we didn't expect a keypress, and already sent a keydown to the VNC server
-            // based on the keydown, make sure to skip this event.
-            if (evt.keysym && !last.ignoreKeyPress) {
-                last.keysyms[evt.keysym] = evt.keysym;
-                evt.type = 'keydown';
-                next(evt);
-            }
-            break;
-        case 'keyup':
-            if (state.length === 0) {
-                return;
-            }
-            var idx = null;
-            // do we have a matching key tracked as being down?
-            for (var i = 0; i !== state.length; ++i) {
-                if (state[i].code === evt.code) {
-                    idx = i;
-                    break;
-                }
-            }
-            // if we couldn't find a match (it happens), assume it was the last key pressed
-            if (idx === null) {
-                idx = state.length - 1;
-            }
-
-            var item = state.splice(idx, 1)[0];
-            // for each keysym tracked by this key entry, clone the current event and override the keysym
-            var clone = (function(){
-                function Clone(){}
-                return function (obj) { Clone.prototype=obj; return new Clone(); };
-            }());
-            for (var key in item.keysyms) {
-                var out = clone(evt);
-                out.keysym = item.keysyms[key];
-                next(out);
-            }
-            break;
-        case 'releaseall':
-            /* jshint shadow: true */
-            for (var i = 0; i < state.length; ++i) {
-                for (var key in state[i].keysyms) {
-                    var keysym = state[i].keysyms[key];
-                    next({code: 'Unidentified', keysym: keysym, type: 'keyup'});
-                }
-            }
-            /* jshint shadow: false */
-            state = [];
-        }
-    };
-};
-
-// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
-// then the modifier must be "undone" before sending the @, and "redone" afterwards.
-export function EscapeModifiers (next) {
-    "use strict";
-    return function(evt) {
-        if (evt.type !== 'keydown' || evt.escape === undefined) {
-            next(evt);
-            return;
-        }
-        // undo modifiers
-        for (var i = 0; i < evt.escape.length; ++i) {
-            next({type: 'keyup', code: 'Unidentified', keysym: evt.escape[i]});
-        }
-        // send the character event
-        next(evt);
-        // redo modifiers
-        /* jshint shadow: true */
-        for (var i = 0; i < evt.escape.length; ++i) {
-            next({type: 'keydown', code: 'Unidentified', keysym: evt.escape[i]});
-        }
-        /* jshint shadow: false */
-    };
-};
index 7ecfcaf1388c41f7887bb542773eed1b0e768a1f..e4ee503f47235008034018c537645a409fb5051c 100644 (file)
 var assert = chai.assert;
 var expect = chai.expect;
 
+import { Keyboard } from '../core/input/devices.js';
 import keysyms from '../core/input/keysymdef.js';
 import * as KeyboardUtil from '../core/input/util.js';
 
 /* jshint newcap: false, expr: true */
-describe('Key Event Pipeline Stages', function() {
+describe('Key Event Handling', function() {
     "use strict";
-    describe('Decode Keyboard Events', function() {
-        it('should pass events to the next stage', function(done) {
-            KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
-                expect(evt).to.be.an.object;
-                done();
-            }).keydown({code: 'KeyA', key: 'a'});
-        });
-        it('should pass the right keysym through', function(done) {
-            KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
-                expect(evt.keysym).to.be.deep.equal(0x61);
-                done();
-            }).keypress({code: 'KeyA', key: 'a'});
-        });
-        it('should pass the right keyid through', function(done) {
-            KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
-                expect(evt).to.have.property('code', 'KeyA');
-                done();
-            }).keydown({code: 'KeyA', key: 'a'});
-        });
-        it('should forward keydown events with the right type', function(done) {
-            KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
-                expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
-                done();
-            }).keydown({code: 'KeyA', key: 'a'});
-        });
-        it('should forward keyup events with the right type', function(done) {
-            KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
-                expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keyup'});
-                done();
-            }).keyup({code: 'KeyA', key: 'a'});
-        });
-        it('should forward keypress events with the right type', function(done) {
-            KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
-                expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keypress'});
-                done();
-            }).keypress({code: 'KeyA', key: 'a'});
-        });
-        describe('suppress the right events at the right time', function() {
-            it('should suppress anything while a shortcut modifier is down', function() {
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
 
-                obj.keydown({code: 'ControlLeft'});
-                expect(obj.keydown({code: 'KeyA', key: 'a'})).to.be.true;
-                expect(obj.keydown({code: 'Space', key: ' '})).to.be.true;
-                expect(obj.keydown({code: 'Digit1', key: '1'})).to.be.true;
-                expect(obj.keydown({code: 'IntlBackslash', key: '<'})).to.be.true;
-                expect(obj.keydown({code: 'Semicolon', key: 'ø'})).to.be.true;
-            });
-            it('should suppress non-character keys', function() {
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
+    // The real KeyboardEvent constructor might not work everywhere we
+    // want to run these tests
+    function keyevent(typeArg, KeyboardEventInit) {
+        var e = { type: typeArg };
+        for (var key in KeyboardEventInit) {
+            e[key] = KeyboardEventInit[key];
+        }
+        e.stopPropagation = sinon.spy();
+        e.preventDefault = sinon.spy();
+        return e;
+    };
 
-                expect(obj.keydown({code: 'Backspace'}), 'a').to.be.true;
-                expect(obj.keydown({code: 'Tab'}), 'b').to.be.true;
-                expect(obj.keydown({code: 'ControlLeft'}), 'd').to.be.true;
-                expect(obj.keydown({code: 'AltLeft'}), 'e').to.be.true;
-            });
-            it('should generate event for shift keydown', function() {
-                var called = false;
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
-                    expect(evt).to.have.property('keysym');
-                    called = true;
-                }).keydown({code: 'ShiftLeft'});
-                expect(called).to.be.true;
-            });
-            it('should suppress character keys with key', function() {
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
-
-                expect(obj.keydown({code: 'KeyA', key: 'a'})).to.be.true;
-                expect(obj.keydown({code: 'Digit1', key: '1'})).to.be.true;
-                expect(obj.keydown({code: 'IntlBackslash', key: '<'})).to.be.true;
-                expect(obj.keydown({code: 'Semicolon', key: 'ø'})).to.be.true;
-            });
-            it('should not suppress character keys without key', function() {
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
-
-                expect(obj.keydown({code: 'KeyA'})).to.be.false;
-                expect(obj.keydown({code: 'Digit1'})).to.be.false;
-                expect(obj.keydown({code: 'IntlBackslash'})).to.be.false;
-                expect(obj.keydown({code: 'Semicolon'})).to.be.false;
-            });
-        });
-        describe('Keypress and keyup events', function() {
-            it('should always suppress event propagation', function() {
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
-
-                expect(obj.keypress({code: 'KeyA', key: 'a'})).to.be.true;
-                expect(obj.keypress({code: 'IntlBackslash', key: '<'})).to.be.true;
-                expect(obj.keypress({code: 'ControlLeft', key: 'Control'})).to.be.true;
-
-                expect(obj.keyup({code: 'KeyA', key: 'a'})).to.be.true;
-                expect(obj.keyup({code: 'IntlBackslash', key: '<'})).to.be.true;
-                expect(obj.keyup({code: 'ControlLeft', key: 'Control'})).to.be.true;
+    describe('Decode Keyboard Events', function() {
+        it('should decode keydown events', function(done) {
+            var kbd = new Keyboard({
+            onKeyEvent: function(keysym, code, down) {
+                expect(keysym).to.be.equal(0x61);
+                expect(code).to.be.equal('KeyA');
+                expect(down).to.be.equal(true);
+                done();
+            }});
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
+        });
+        it('should decode keyup events', function(done) {
+            var calls = 0;
+            var kbd = new Keyboard({
+            onKeyEvent: function(keysym, code, down) {
+                expect(keysym).to.be.equal(0x61);
+                expect(code).to.be.equal('KeyA');
+                if (calls++ === 1) {
+                    expect(down).to.be.equal(false);
+                    done();
+                }
+            }});
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
+            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
+        });
+
+        describe('Legacy keypress Events', function() {
+            it('should wait for keypress when needed', function() {
+                var callback = sinon.spy();
+                var kbd = new Keyboard({onKeyEvent: callback});
+                kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
+                expect(callback).to.not.have.been.called;
+            });
+            it('should decode keypress events', function(done) {
+                var kbd = new Keyboard({
+                onKeyEvent: function(keysym, code, down) {
+                    expect(keysym).to.be.equal(0x61);
+                    expect(code).to.be.equal('KeyA');
+                    expect(down).to.be.equal(true);
+                    done();
+                }});
+                kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
+                kbd._handleKeyPress(keyevent('keypress', {code: 'KeyA', charCode: 0x61}));
             });
         });
-        describe('mark events if a char modifier is down', function() {
-            it('should not mark modifiers on a keydown event', function() {
-                var times_called = 0;
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {
-                    switch (times_called++) {
-                    case 0: //altgr
-                        break;
-                    case 1: // 'a'
-                        expect(evt).to.not.have.property('escape');
-                        break;
-                    }
-                });
-
-                obj.keydown({code: 'AltRight', key: 'AltGraph'})
-                obj.keydown({code: 'KeyA', key: 'a'});
-            });
-
-            it('should indicate on events if a single-key char modifier is down', function(done) {
-                var times_called = 0;
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {
-                    switch (times_called++) {
-                    case 0: //altgr
-                        break;
-                    case 1: // 'a'
-                        expect(evt).to.be.deep.equal({
-                            type: 'keypress',
-                            code: 'KeyA',
-                            keysym: 0x61,
-                            escape: [0xfe03]
-                        });
-                        done();
-                        return;
-                    }
-                });
-
-                obj.keydown({code: 'AltRight', key: 'AltGraph'})
-                obj.keypress({code: 'KeyA', key: 'a'});
-            });
-            it('should indicate on events if a multi-key char modifier is down', function(done) {
-                var times_called = 0;
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xffe9, 0xffe3]), function(evt) {
-                    switch (times_called++) {
-                    case 0: //ctrl
-                        break;
-                    case 1: //alt
-                        break;
-                    case 2: // 'a'
-                        expect(evt).to.be.deep.equal({
-                            type: 'keypress',
-                            code: 'KeyA',
-                            keysym: 0x61,
-                            escape: [0xffe9, 0xffe3]
-                        });
-                        done();
-                        return;
-                    }
-                });
-
-                obj.keydown({code: 'ControlLeft'});
-                obj.keydown({code: 'AltLeft'});
-                obj.keypress({code: 'KeyA', key: 'a'});
-            });
-            it('should not consider a char modifier to be down on the modifier key itself', function() {
-                var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {
-                    expect(evt).to.not.have.property('escape');
-                });
-
-                obj.keydown({code: 'AltRight', key: 'AltGraph'})
 
+        describe('suppress the right events at the right time', function() {
+            it('should suppress anything with a valid key', function() {
+                var kbd = new Keyboard({});
+                var evt = keyevent('keydown', {code: 'KeyA', key: 'a'});
+                kbd._handleKeyDown(evt);
+                expect(evt.preventDefault).to.have.been.called;
+                evt = keyevent('keyup', {code: 'KeyA', key: 'a'});
+                kbd._handleKeyUp(evt);
+                expect(evt.preventDefault).to.have.been.called;
+            });
+            it('should not suppress keys without key', function() {
+                var kbd = new Keyboard({});
+                var evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41});
+                kbd._handleKeyDown(evt);
+                expect(evt.preventDefault).to.not.have.been.called;
+            });
+            it('should suppress the following keypress event', function() {
+                var kbd = new Keyboard({});
+                var evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41});
+                kbd._handleKeyDown(evt);
+                var evt = keyevent('keypress', {code: 'KeyA', charCode: 0x41});
+                kbd._handleKeyPress(evt);
+                expect(evt.preventDefault).to.have.been.called;
             });
         });
     });
 
     describe('Track Key State', function() {
-        it('should do nothing on keyup events if no keys are down', function() {
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                expect(true).to.be.false;
-            });
-            obj({type: 'keyup', code: 'KeyA'});
+        it('should send release using the same keysym as the press', function(done) {
+            var kbd = new Keyboard({
+            onKeyEvent: function(keysym, code, down) {
+                expect(keysym).to.be.equal(0x61);
+                expect(code).to.be.equal('KeyA');
+                if (!down) {
+                    done();
+                }
+            }});
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
+            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'b'}));
         });
-        it('should insert into the queue on keydown if no keys are down', function() {
-            var times_called = 0;
-            var elem = null;
-            var keysymsdown = {};
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                ++times_called;
-                if (elem.type == 'keyup') {
-                expect(evt).to.have.property('keysym');
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    delete keysymsdown[evt.keysym];
-                }
-                else {
-                    expect(evt).to.be.deep.equal(elem);
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                }
-                elem = null;
-            });
-
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'KeyA', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keyup', code: 'KeyA'};
-            obj(elem);
-            expect(elem).to.be.null;
-            expect(times_called).to.be.equal(2);
-        });
-        it('should insert into the queue on keypress if no keys are down', function() {
-            var times_called = 0;
-            var elem = null;
-            var keysymsdown = {};
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                ++times_called;
-                if (elem.type == 'keyup') {
-                    expect(evt).to.have.property('keysym');
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    delete keysymsdown[evt.keysym];
-                }
-                else {
-                    expect(evt).to.be.deep.equal(elem);
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                }
-                elem = null;
-            });
-
-            expect(elem).to.be.null;
-            elem = {type: 'keypress', code: 'KeyA', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keyup', code: 'KeyA'};
-            obj(elem);
-            expect(elem).to.be.null;
-            expect(times_called).to.be.equal(2);
-        });
-        it('should add keysym to last key entry if code matches', function() {
-            // this implies that a single keyup will release both keysyms
-            var times_called = 0;
-            var elem = null;
-            var keysymsdown = {};
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                ++times_called;
-                if (elem.type == 'keyup') {
-                    expect(evt).to.have.property('keysym');
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    delete keysymsdown[evt.keysym];
-                }
-                else {
-                    expect(evt).to.be.deep.equal(elem);
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    elem = null;
-                }
-            });
-
-            expect(elem).to.be.null;
-            elem = {type: 'keypress', code: 'KeyA', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keypress', code: 'KeyA', keysym: 0x43};
-            keysymsdown[0x43] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keyup', code: 'KeyA'};
-            obj(elem);
-            expect(times_called).to.be.equal(4);
-        });
-        it('should create new key entry if code matches and keysym does not', function() {
-            // this implies that a single keyup will release both keysyms
-            var times_called = 0;
-            var elem = null;
-            var keysymsdown = {};
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                ++times_called;
-                if (elem.type == 'keyup') {
-                    expect(evt).to.have.property('keysym');
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    delete keysymsdown[evt.keysym];
-                }
-                else {
-                    expect(evt).to.be.deep.equal(elem);
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    elem = null;
-                }
-            });
-
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'Unidentified', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'Unidentified', keysym: 0x43};
-            keysymsdown[0x43] = true;
-            obj(elem);
-            expect(times_called).to.be.equal(2);
-            expect(elem).to.be.null;
-            elem = {type: 'keyup', code: 'Unidentified'};
-            obj(elem);
-            expect(times_called).to.be.equal(3);
-            elem = {type: 'keyup', code: 'Unidentified'};
-            obj(elem);
-            expect(times_called).to.be.equal(4);
+        it('should do nothing on keyup events if no keys are down', function() {
+            var callback = sinon.spy();
+            var kbd = new Keyboard({onKeyEvent: callback});
+            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
+            expect(callback).to.not.have.been.called;
+        });
+        it('should send a key release for each key press with the same code', function() {
+            var callback = sinon.spy();
+            var kbd = new Keyboard({onKeyEvent: callback});
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'b'}));
+            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA'}));
+            expect(callback.callCount).to.be.equal(4);
         });
-        it('should merge key entry if codes are zero and keysyms match', function() {
-            // this implies that a single keyup will release both keysyms
-            var times_called = 0;
-            var elem = null;
-            var keysymsdown = {};
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                ++times_called;
-                if (elem.type == 'keyup') {
-                    expect(evt).to.have.property('keysym');
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    delete keysymsdown[evt.keysym];
-                }
-                else {
-                    expect(evt).to.be.deep.equal(elem);
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    elem = null;
-                }
-            });
+    });
 
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'Unidentified', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'Unidentified', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(times_called).to.be.equal(2);
-            expect(elem).to.be.null;
-            elem = {type: 'keyup', code: 'Unidentified'};
-            obj(elem);
-            expect(times_called).to.be.equal(3);
-        });
-        it('should add keysym as separate entry if code does not match last event', function() {
-            // this implies that separate keyups are required
+    describe('Escape Modifiers', function() {
+        var origNavigator;
+        beforeEach(function () {
+            // window.navigator is a protected read-only property in many
+            // environments, so we need to redefine it whilst running these
+            // tests.
+            origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
+            if (origNavigator === undefined) {
+                // Object.getOwnPropertyDescriptor() doesn't work
+                // properly in any version of IE
+                this.skip();
+            }
+
+            Object.defineProperty(window, "navigator", {value: {}});
+            if (window.navigator.platform !== undefined) {
+                // Object.defineProperty() doesn't work properly in old
+                // versions of Chrome
+                this.skip();
+            }
+
+            window.navigator.platform = "Windows x86_64";
+        });
+        afterEach(function () {
+            Object.defineProperty(window, "navigator", origNavigator);
+        });
+
+        it('should generate fake undo/redo events on press when a char modifier is down', function() {
             var times_called = 0;
-            var elem = null;
-            var keysymsdown = {};
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                ++times_called;
-                if (elem.type == 'keyup') {
-                    expect(evt).to.have.property('keysym');
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    delete keysymsdown[evt.keysym];
-                }
-                else {
-                    expect(evt).to.be.deep.equal(elem);
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    elem = null;
-                }
-            });
-
-            expect(elem).to.be.null;
-            elem = {type: 'keypress', code: 'KeyA', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keypress', code: 'KeyB', keysym: 0x43};
-            keysymsdown[0x43] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keyup', code: 'KeyA'};
-            obj(elem);
-            expect(times_called).to.be.equal(4);
-            elem = {type: 'keyup', code: 'KeyB'};
-            obj(elem);
-            expect(times_called).to.be.equal(4);
-        });
-        it('should add keysym as separate entry if code does not match last event and first is zero', function() {
-            // this implies that separate keyups are required
+            var kbd = new Keyboard({
+            onKeyEvent: function(keysym, code, down) {
+                switch(times_called++) {
+                case 0:
+                    expect(keysym).to.be.equal(0xFFE3);
+                    expect(code).to.be.equal('ControlLeft');
+                    expect(down).to.be.equal(true);
+                    break;
+                case 1:
+                    expect(keysym).to.be.equal(0xFFE9);
+                    expect(code).to.be.equal('AltLeft');
+                    expect(down).to.be.equal(true);
+                    break;
+                case 2:
+                    expect(keysym).to.be.equal(0xFFE9);
+                    expect(code).to.be.equal('Unidentified');
+                    expect(down).to.be.equal(false);
+                    break;
+                case 3:
+                    expect(keysym).to.be.equal(0xFFE3);
+                    expect(code).to.be.equal('Unidentified');
+                    expect(down).to.be.equal(false);
+                    break;
+                case 4:
+                    expect(keysym).to.be.equal(0x61);
+                    expect(code).to.be.equal('KeyA');
+                    expect(down).to.be.equal(true);
+                    break;
+                case 5:
+                    expect(keysym).to.be.equal(0xFFE9);
+                    expect(code).to.be.equal('Unidentified');
+                    expect(down).to.be.equal(true);
+                    break;
+                case 6:
+                    expect(keysym).to.be.equal(0xFFE3);
+                    expect(code).to.be.equal('Unidentified');
+                    expect(down).to.be.equal(true);
+                    break;
+                }
+            }});
+            // First the modifier combo
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'}));
+            kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'}));
+            // Next a normal character
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
+            expect(times_called).to.be.equal(7);
+        });
+        it('should no do anything on key release', function() {
             var times_called = 0;
-            var elem = null;
-            var keysymsdown = {};
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                ++times_called;
-                if (elem.type == 'keyup') {
-                    expect(evt).to.have.property('keysym');
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    delete keysymsdown[evt.keysym];
-                }
-                else {
-                    expect(evt).to.be.deep.equal(elem);
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    elem = null;
-                }
-            });
-
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'Unidentified', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'KeyB', keysym: 0x43};
-            keysymsdown[0x43] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            expect(times_called).to.be.equal(2);
-            elem = {type: 'keyup', code: 'Unidentified'};
-            obj(elem);
-            expect(times_called).to.be.equal(3);
-            elem = {type: 'keyup', code: 'KeyB'};
-            obj(elem);
-            expect(times_called).to.be.equal(4);
-        });
-        it('should add keysym as separate entry if code does not match last event and second is zero', function() {
-            // this implies that a separate keyups are required
+            var kbd = new Keyboard({
+            onKeyEvent: function(keysym, code, down) {
+                switch(times_called++) {
+                case 7:
+                    expect(keysym).to.be.equal(0x61);
+                    expect(code).to.be.equal('KeyA');
+                    expect(down).to.be.equal(false);
+                    break;
+                }
+            }});
+            // First the modifier combo
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'}));
+            kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'}));
+            // Next a normal character
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
+            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
+            expect(times_called).to.be.equal(8);
+        });
+        it('should not consider a char modifier to be down on the modifier key itself', function() {
             var times_called = 0;
-            var elem = null;
-            var keysymsdown = {};
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                ++times_called;
-                if (elem.type == 'keyup') {
-                    expect(evt).to.have.property('keysym');
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    delete keysymsdown[evt.keysym];
-                }
-                else {
-                    expect(evt).to.be.deep.equal(elem);
-                    expect (keysymsdown[evt.keysym]).to.not.be.undefined;
-                    elem = null;
-                }
-            });
-
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'KeyA', keysym: 0x42};
-            keysymsdown[0x42] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keydown', code: 'Unidentified', keysym: 0x43};
-            keysymsdown[0x43] = true;
-            obj(elem);
-            expect(elem).to.be.null;
-            elem = {type: 'keyup', code: 'KeyA'};
-            obj(elem);
+            var kbd = new Keyboard({
+            onKeyEvent: function(keysym, code, down) {
+                switch(times_called++) {
+                case 0:
+                    expect(keysym).to.be.equal(0xFFE3);
+                    expect(code).to.be.equal('ControlLeft');
+                    expect(down).to.be.equal(true);
+                    break;
+                case 1:
+                    expect(keysym).to.be.equal(0xFFE9);
+                    expect(code).to.be.equal('AltLeft');
+                    expect(down).to.be.equal(true);
+                    break;
+                case 2:
+                    expect(keysym).to.be.equal(0xFFE3);
+                    expect(code).to.be.equal('ControlLeft');
+                    expect(down).to.be.equal(true);
+                    break;
+                }
+            }});
+            // First the modifier combo
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'}));
+            kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'}));
+            // Then one of the keys again
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'}));
             expect(times_called).to.be.equal(3);
-            elem = {type: 'keyup', code: 'Unidentified'};
-            obj(elem);
-            expect(times_called).to.be.equal(4);
-        });
-        it('should pop matching key event on keyup', function() {
-            var times_called = 0;
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                switch (times_called++) {
-                    case 0:
-                    case 1:
-                    case 2:
-                        expect(evt.type).to.be.equal('keydown');
-                        break;
-                    case 3:
-                        expect(evt).to.be.deep.equal({type: 'keyup', code: 'KeyB', keysym: 0x62});
-                        break;
-                }
-            });
-
-            obj({type: 'keydown', code: 'KeyA', keysym: 0x61});
-            obj({type: 'keydown', code: 'KeyB', keysym: 0x62});
-            obj({type: 'keydown', code: 'KeyC', keysym: 0x63});
-            obj({type: 'keyup', code: 'KeyB'});
-            expect(times_called).to.equal(4);
-        });
-        it('should pop the first zero keyevent on keyup with zero code', function() {
-            var times_called = 0;
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                switch (times_called++) {
-                    case 0:
-                    case 1:
-                    case 2:
-                        expect(evt.type).to.be.equal('keydown');
-                        break;
-                    case 3:
-                        expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0x61});
-                        break;
-                }
-            });
-
-            obj({type: 'keydown', code: 'Unidentified', keysym: 0x61});
-            obj({type: 'keydown', code: 'Unidentified', keysym: 0x62});
-            obj({type: 'keydown', code: 'KeyA', keysym: 0x63});
-            obj({type: 'keyup', code: 'Unidentified'});
-            expect(times_called).to.equal(4);
-        });
-        it('should pop the last keyevents keysym if no match is found for code', function() {
-            var times_called = 0;
-            var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                switch (times_called++) {
-                    case 0:
-                    case 1:
-                    case 2:
-                        expect(evt.type).to.be.equal('keydown');
-                        break;
-                    case 3:
-                        expect(evt).to.be.deep.equal({type: 'keyup', code: 'KeyD', keysym: 0x63});
-                        break;
-                }
-            });
-
-            obj({type: 'keydown', code: 'KeyA', keysym: 0x61});
-            obj({type: 'keydown', code: 'KeyB', keysym: 0x62});
-            obj({type: 'keydown', code: 'KeyC', keysym: 0x63});
-            obj({type: 'keyup', code: 'KeyD'});
-            expect(times_called).to.equal(4);
-        });
-        describe('Firefox sends keypress even when keydown is suppressed', function() {
-            it('should discard the keypress', function() {
-                var times_called = 0;
-                var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                    expect(times_called).to.be.equal(0);
-                    ++times_called;
-                });
-
-                obj({type: 'keydown', code: 'KeyA', keysym: 0x42});
-                expect(times_called).to.be.equal(1);
-                obj({type: 'keypress', code: 'KeyA', keysym: 0x43});
-            });
-        });
-        describe('releaseAll', function() {
-            it('should do nothing if no keys have been pressed', function() {
-                var times_called = 0;
-                var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                    ++times_called;
-                });
-                obj({type: 'releaseall'});
-                expect(times_called).to.be.equal(0);
-            });
-            it('should release the keys that have been pressed', function() {
-                var times_called = 0;
-                var obj = KeyboardUtil.TrackKeyState(function(evt) {
-                    switch (times_called++) {
-                    case 2:
-                        expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0x41});
-                        break;
-                    case 3:
-                        expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0x42});
-                        break;
-                    }
-                });
-                obj({type: 'keydown', code: 'KeyA', keysym: 0x41});
-                obj({type: 'keydown', code: 'KeyB', keysym: 0x42});
-                expect(times_called).to.be.equal(2);
-                obj({type: 'releaseall'});
-                expect(times_called).to.be.equal(4);
-                obj({type: 'releaseall'});
-                expect(times_called).to.be.equal(4);
-            });
-        });
-
-    });
-
-    describe('Escape Modifiers', function() {
-        describe('Keydown', function() {
-            it('should pass through when a char modifier is not down', function() {
-                var times_called = 0;
-                KeyboardUtil.EscapeModifiers(function(evt) {
-                    expect(times_called).to.be.equal(0);
-                    ++times_called;
-                    expect(evt).to.be.deep.equal({type: 'keydown', code: 'KeyA', keysym: 0x42});
-                })({type: 'keydown', code: 'KeyA', keysym: 0x42});
-                expect(times_called).to.be.equal(1);
-            });
-            it('should generate fake undo/redo events when a char modifier is down', function() {
-                var times_called = 0;
-                KeyboardUtil.EscapeModifiers(function(evt) {
-                    switch(times_called++) {
-                    case 0:
-                        expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0xffe9});
-                        break;
-                    case 1:
-                        expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0xffe3});
-                        break;
-                    case 2:
-                        expect(evt).to.be.deep.equal({type: 'keydown', code: 'KeyA', keysym: 0x42, escape: [0xffe9, 0xffe3]});
-                        break;
-                    case 3:
-                        expect(evt).to.be.deep.equal({type: 'keydown', code: 'Unidentified', keysym: 0xffe9});
-                        break;
-                    case 4:
-                        expect(evt).to.be.deep.equal({type: 'keydown', code: 'Unidentified', keysym: 0xffe3});
-                        break;
-                    }
-                })({type: 'keydown', code: 'KeyA', keysym: 0x42, escape: [0xffe9, 0xffe3]});
-                expect(times_called).to.be.equal(5);
-            });
-        });
-        describe('Keyup', function() {
-            it('should pass through when a char modifier is down', function() {
-                var times_called = 0;
-                KeyboardUtil.EscapeModifiers(function(evt) {
-                    expect(times_called).to.be.equal(0);
-                    ++times_called;
-                    expect(evt).to.be.deep.equal({type: 'keyup', code: 'KeyA', keysym: 0x42, escape: [0xfe03]});
-                })({type: 'keyup', code: 'KeyA', keysym: 0x42, escape: [0xfe03]});
-                expect(times_called).to.be.equal(1);
-            });
-            it('should pass through when a char modifier is not down', function() {
-                var times_called = 0;
-                KeyboardUtil.EscapeModifiers(function(evt) {
-                    expect(times_called).to.be.equal(0);
-                    ++times_called;
-                    expect(evt).to.be.deep.equal({type: 'keyup', code: 'KeyA', keysym: 0x42});
-                })({type: 'keyup', code: 'KeyA', keysym: 0x42});
-                expect(times_called).to.be.equal(1);
-            });
         });
     });
 });