소스 검색

Keyboard Handling [3/8]: Add unit tests for new keyboard handling

Relies on the libraries chai and mocha (available via npm from Node.JS).

Add anything installed via npm to the .gitignore file.
jalf 11 년 전
부모
커밋
f00b6fb69a
4개의 변경된 파일1108개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      .gitignore
  2. 29 0
      tests/keyboard-tests.html
  3. 237 0
      tests/test.helper.js
  4. 841 0
      tests/test.keyboard.js

+ 1 - 0
.gitignore

@@ -2,3 +2,4 @@
 *.o
 tests/data_*.js
 utils/rebind.so
+node_modules

+ 29 - 0
tests/keyboard-tests.html

@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Mocha Tests</title>
+  <link rel="stylesheet" href="node_modules/mocha/mocha.css" />
+</head>
+<body>
+  <!--
+    To run tests
+    cd .../noVNC/tests
+    npm install chai mocha
+    open keyboard-tests.html in a browser
+  -->
+  <div id="mocha"></div>
+  <script src="node_modules/chai/chai.js"></script>
+  <script src="node_modules/mocha/mocha.js"></script>
+  <script>mocha.setup('bdd')</script>
+  <script src="../include/keysymdef.js"></script>
+  <script src="../include/keyboard.js"></script>
+  <script src="test.keyboard.js"></script>
+  <script src="test.helper.js"></script>
+  <script>
+    mocha.checkLeaks();
+    mocha.globals(['navigator']);
+    mocha.run();
+  </script>
+</body>
+</html>

+ 237 - 0
tests/test.helper.js

@@ -0,0 +1,237 @@
+var assert = chai.assert;
+var expect = chai.expect;
+
+describe('Helpers', function() {
+    "use strict";
+    describe('keysymFromKeyCode', function() {
+        it('should map known keycodes to keysyms', function() {
+            expect(kbdUtil.keysymFromKeyCode(0x41, false), 'a').to.be.equal(0x61);
+            expect(kbdUtil.keysymFromKeyCode(0x41, true), 'A').to.be.equal(0x41);
+            expect(kbdUtil.keysymFromKeyCode(0xd, false), 'enter').to.be.equal(0xFF0D);
+            expect(kbdUtil.keysymFromKeyCode(0x11, false), 'ctrl').to.be.equal(0xFFE3);
+            expect(kbdUtil.keysymFromKeyCode(0x12, false), 'alt').to.be.equal(0xFFE9);
+            expect(kbdUtil.keysymFromKeyCode(0xe1, false), 'altgr').to.be.equal(0xFE03);
+            expect(kbdUtil.keysymFromKeyCode(0x1b, false), 'esc').to.be.equal(0xFF1B);
+            expect(kbdUtil.keysymFromKeyCode(0x26, false), 'up').to.be.equal(0xFF52);
+        });
+        it('should return null for unknown keycodes', function() {
+            expect(kbdUtil.keysymFromKeyCode(0xc0, false), 'DK æ').to.be.null;
+            expect(kbdUtil.keysymFromKeyCode(0xde, false), 'DK ø').to.be.null;
+        });
+    });
+
+    describe('keysyms.fromUnicode', function() {
+        it('should map ASCII characters to keysyms', function() {
+            expect(keysyms.fromUnicode('a'.charCodeAt())).to.have.property('keysym', 0x61);
+            expect(keysyms.fromUnicode('A'.charCodeAt())).to.have.property('keysym', 0x41);
+            });
+        it('should map Latin-1 characters to keysyms', function() {
+            expect(keysyms.fromUnicode('ø'.charCodeAt())).to.have.property('keysym', 0xf8);
+
+            expect(keysyms.fromUnicode('é'.charCodeAt())).to.have.property('keysym', 0xe9);
+        });
+        it('should map characters that are in Windows-1252 but not in Latin-1 to keysyms', function() {
+            expect(keysyms.fromUnicode('Š'.charCodeAt())).to.have.property('keysym', 0x01a9);
+        });
+        it('should map characters which aren\'t in Latin1 *or* Windows-1252 to keysyms', function() {
+            expect(keysyms.fromUnicode('ŵ'.charCodeAt())).to.have.property('keysym', 0x1000175);
+        });
+        it('should return undefined for unknown codepoints', function() {
+            expect(keysyms.fromUnicode('\n'.charCodeAt())).to.be.undefined;
+            expect(keysyms.fromUnicode('\u1F686'.charCodeAt())).to.be.undefined;
+        });
+    });
+
+    describe('nonCharacterKey', function() {
+        it('should  recognize the right keys', function() {
+            expect(kbdUtil.nonCharacterKey({keyCode: 0xd}), 'enter').to.be.defined;
+            expect(kbdUtil.nonCharacterKey({keyCode: 0x08}), 'backspace').to.be.defined;
+            expect(kbdUtil.nonCharacterKey({keyCode: 0x09}), 'tab').to.be.defined;
+            expect(kbdUtil.nonCharacterKey({keyCode: 0x10}), 'shift').to.be.defined;
+            expect(kbdUtil.nonCharacterKey({keyCode: 0x11}), 'ctrl').to.be.defined;
+            expect(kbdUtil.nonCharacterKey({keyCode: 0x12}), 'alt').to.be.defined;
+            expect(kbdUtil.nonCharacterKey({keyCode: 0xe0}), 'meta').to.be.defined;
+        });
+        it('should  not recognize character keys', function() {
+            expect(kbdUtil.nonCharacterKey({keyCode: 'A'}), 'A').to.be.null;
+            expect(kbdUtil.nonCharacterKey({keyCode: '1'}), '1').to.be.null;
+            expect(kbdUtil.nonCharacterKey({keyCode: '.'}), '.').to.be.null;
+            expect(kbdUtil.nonCharacterKey({keyCode: ' '}), 'space').to.be.null;
+        });
+    });
+
+    describe('getKeysym', function() {
+        it('should prefer char', function() {
+            expect(kbdUtil.getKeysym({char : 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x61);
+        });
+        it('should use charCode if no char', function() {
+            expect(kbdUtil.getKeysym({char : '', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
+            expect(kbdUtil.getKeysym({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
+            expect(kbdUtil.getKeysym({char : 'hello', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
+        });
+        it('should use keyCode if no charCode', function() {
+            expect(kbdUtil.getKeysym({keyCode: 0x42, which: 0x43, shiftKey: false})).to.have.property('keysym', 0x62);
+            expect(kbdUtil.getKeysym({keyCode: 0x42, which: 0x43, shiftKey: true})).to.have.property('keysym', 0x42);
+        });
+        it('should use which if no keyCode', function() {
+            expect(kbdUtil.getKeysym({which: 0x43, shiftKey: false})).to.have.property('keysym', 0x63);
+            expect(kbdUtil.getKeysym({which: 0x43, shiftKey: true})).to.have.property('keysym', 0x43);
+        });
+    });
+
+    describe('Modifier Sync', function() { // return a list of fake events necessary to fix modifier state
+        describe('Toggle all modifiers', function() {
+            var sync = kbdUtil.ModifierSync();
+            it ('should do nothing if all modifiers are up as expected', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    ctrlKey: false,
+                    altKey: false,
+                    altGraphKey: false,
+                    shiftKey: false,
+                    metaKey: false})
+                    ).to.have.lengthOf(0);
+            });
+            it ('should synthesize events if all keys are unexpectedly down', function() {
+                var result = sync.keydown({
+                    keyCode: 0x41,
+                    ctrlKey: true,
+                    altKey: true,
+                    altGraphKey: true,
+                    shiftKey: true,
+                    metaKey: true
+                });
+                expect(result).to.have.lengthOf(5);
+                var keysyms = {};
+                for (var i = 0; i < result.length; ++i) {
+                    keysyms[result[i].keysym] = (result[i].type == 'keydown');
+                }
+                expect(keysyms[0xffe3]);
+                expect(keysyms[0xffe9]);
+                expect(keysyms[0xfe03]);
+                expect(keysyms[0xffe1]);
+                expect(keysyms[0xffe7]);
+            });
+            it ('should do nothing if all modifiers are down as expected', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    ctrlKey: true,
+                    altKey: true,
+                    altGraphKey: true,
+                    shiftKey: true,
+                    metaKey: true
+                    })).to.have.lengthOf(0);
+            });
+        });
+        describe('Toggle Ctrl', function() {
+            var sync = kbdUtil.ModifierSync();
+            it('should sync if modifier is suddenly down', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    ctrlKey: true,
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe3), type: 'keydown'}]);
+            });
+            it('should sync if modifier is suddenly up', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    ctrlKey: false
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe3), type: 'keyup'}]);
+            });
+        });
+        describe('Toggle Alt', function() {
+            var sync = kbdUtil.ModifierSync();
+            it('should sync if modifier is suddenly down', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    altKey: true,
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
+            });
+            it('should sync if modifier is suddenly up', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    altKey: false
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keyup'}]);
+            });
+        });
+        describe('Toggle AltGr', function() {
+            var sync = kbdUtil.ModifierSync();
+            it('should sync if modifier is suddenly down', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    altGraphKey: true,
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xfe03), type: 'keydown'}]);
+            });
+            it('should sync if modifier is suddenly up', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    altGraphKey: false
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xfe03), type: 'keyup'}]);
+            });
+        });
+        describe('Toggle Shift', function() {
+            var sync = kbdUtil.ModifierSync();
+            it('should sync if modifier is suddenly down', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    shiftKey: true,
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe1), type: 'keydown'}]);
+            });
+            it('should sync if modifier is suddenly up', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    shiftKey: false
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe1), type: 'keyup'}]);
+            });
+        });
+        describe('Toggle Meta', function() {
+            var sync = kbdUtil.ModifierSync();
+            it('should sync if modifier is suddenly down', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    metaKey: true,
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe7), type: 'keydown'}]);
+            });
+            it('should sync if modifier is suddenly up', function() {
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    metaKey: false
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe7), type: 'keyup'}]);
+            });
+        });
+        describe('Modifier keyevents', function() {
+            it('should not sync a modifier on its own events', function() {
+                expect(kbdUtil.ModifierSync().keydown({
+                    keyCode: 0x11,
+                    ctrlKey: false
+                })).to.be.deep.equal([]);
+                expect(kbdUtil.ModifierSync().keydown({
+                    keyCode: 0x11,
+                    ctrlKey: true
+                }), 'B').to.be.deep.equal([]);
+            })
+            it('should update state on modifier keyevents', function() {
+                var sync = kbdUtil.ModifierSync();
+                sync.keydown({
+                    keyCode: 0x11,
+                });
+                expect(sync.keydown({
+                    keyCode: 0x41,
+                    ctrlKey: true,
+                })).to.be.deep.equal([]);
+            });
+            it('should sync other modifiers on ctrl events', function() {
+                expect(kbdUtil.ModifierSync().keydown({
+                    keyCode: 0x11,
+                    altKey: true
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
+            })
+        });
+        describe('sync modifiers on non-key events', function() {
+            it('should generate sync events when receiving non-keyboard events', function() {
+                expect(kbdUtil.ModifierSync().syncAny({
+                    altKey: true
+                })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
+            });
+        });
+    });
+});

+ 841 - 0
tests/test.keyboard.js

@@ -0,0 +1,841 @@
+var assert = chai.assert;
+var expect = chai.expect;
+
+
+describe('Key Event Pipeline Stages', function() {
+    "use strict";
+    describe('Decode Keyboard Events', function() {
+        it('should pass events to the next stage', function(done) {
+            KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                expect(evt).to.be.an.object;
+                done();
+            }).keydown({keyCode: 0x41});
+        });
+        it('should pass the right keysym through', function(done) {
+            KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                expect(evt.keysym).to.be.deep.equal(keysyms.lookup(0x61));
+                done();
+            }).keypress({keyCode: 0x41});
+        });
+        it('should pass the right keyid through', function(done) {
+            KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                expect(evt).to.have.property('keyId', 0x41);
+                done();
+            }).keydown({keyCode: 0x41});
+        });
+        it('should not sync modifiers on a keypress', function() {
+            // Firefox provides unreliable modifier state on keypress events
+            var count = 0;
+            KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                ++count;
+            }).keypress({keyCode: 0x41, ctrlKey: true});
+            expect(count).to.be.equal(1);
+        });
+        it('should sync modifiers if necessary', function(done) {
+            var count = 0;
+            KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                switch (count) {
+                case 0: // fake a ctrl keydown
+                    expect(evt).to.be.deep.equal({keysym: keysyms.lookup(0xffe3), type: 'keydown'});
+                    ++count;
+                    break;
+                case 1:
+                    expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown', keysym: keysyms.lookup(0x61)});
+                    done();
+                    break;
+                }
+            }).keydown({keyCode: 0x41, ctrlKey: true});
+        });
+        it('should forward keydown events with the right type', function(done) {
+            KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
+                done();
+            }).keydown({keyCode: 0x41})
+        });
+        it('should forward keyup events with the right type', function(done) {
+            KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keyup'});
+                done();
+            }).keyup({keyCode: 0x41});
+        });
+        it('should forward keypress events with the right type', function(done) {
+            KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keypress'});
+                done();
+            }).keypress({keyCode: 0x41});
+        });
+        it('should generate stalls if a char modifier is down while a key is pressed', function(done) {
+            var count = 0;
+            KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) {
+                switch (count) {
+                case 0: // fake altgr
+                    expect(evt).to.be.deep.equal({keysym: keysyms.lookup(0xfe03), type: 'keydown'});
+                    ++count;
+                    break;
+                case 1: // stall before processing the 'a' keydown
+                    expect(evt).to.be.deep.equal({type: 'stall'});
+                    ++count;
+                    break;
+                case 2: // 'a'
+                    expect(evt).to.be.deep.equal({
+                        type: 'keydown',
+                        keyId: 0x41,
+                        keysym: keysyms.lookup(0x61)
+                    });
+
+                    done();
+                    break;
+                }
+            }).keydown({keyCode: 0x41, altGraphKey: true});
+
+        });
+        describe('suppress the right events at the right time', function() {
+            it('should suppress anything while a shortcut modifier is down', function() {
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {});
+
+                obj.keydown({keyCode: 0x11}); // press ctrl
+                expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.true;
+                expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.true;
+                expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.true;
+                expect(obj.keydown({keyCode: 0x3c})).to.be.true; // < key on DK Windows
+                expect(obj.keydown({keyCode: 0xde})).to.be.true; // Ø key on DK
+            });
+            it('should suppress non-character keys', function() {
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {});
+
+                expect(obj.keydown({keyCode: 0x08}), 'a').to.be.true;
+                expect(obj.keydown({keyCode: 0x09}), 'b').to.be.true;
+                expect(obj.keydown({keyCode: 0x11}), 'd').to.be.true;
+                expect(obj.keydown({keyCode: 0x12}), 'e').to.be.true;
+            });
+            it('should not suppress shift', function() {
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {});
+
+                expect(obj.keydown({keyCode: 0x10}), 'd').to.be.false;
+            });
+            it('should generate event for shift keydown', function() {
+                var called = false;
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                    expect(evt).to.have.property('keysym');
+                    called = true;
+                }).keydown({keyCode: 0x10});
+                expect(called).to.be.true;
+            });
+            it('should not suppress character keys', function() {
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {});
+
+                expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false;
+                expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
+                expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.false;
+                expect(obj.keydown({keyCode: 0x3c})).to.be.false; // < key on DK Windows
+                expect(obj.keydown({keyCode: 0xde})).to.be.false; // Ø key on DK
+            });
+            it('should not suppress if a char modifier is down', function() {
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) {});
+
+                obj.keydown({keyCode: 0xe1}); // press altgr
+                expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false;
+                expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
+                expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.false;
+                expect(obj.keydown({keyCode: 0x3c})).to.be.false; // < key on DK Windows
+                expect(obj.keydown({keyCode: 0xde})).to.be.false; // Ø key on DK
+            });
+        });
+        describe('Keypress and keyup events', function() {
+            it('should always suppress event propagation', function() {
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {});
+
+                expect(obj.keypress({keyCode: 'A'.charCodeAt()})).to.be.true;
+                expect(obj.keypress({keyCode: 0x3c})).to.be.true; // < key on DK Windows
+                expect(obj.keypress({keyCode: 0x11})).to.be.true;
+
+                expect(obj.keyup({keyCode: 'A'.charCodeAt()})).to.be.true;
+                expect(obj.keyup({keyCode: 0x3c})).to.be.true; // < key on DK Windows
+                expect(obj.keyup({keyCode: 0x11})).to.be.true;
+            });
+            it('should never generate stalls', function() {
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                    expect(evt.type).to.not.be.equal('stall');
+                });
+
+                obj.keypress({keyCode: 'A'.charCodeAt()});
+                obj.keypress({keyCode: 0x3c});
+                obj.keypress({keyCode: 0x11});
+
+                obj.keyup({keyCode: 'A'.charCodeAt()});
+                obj.keyup({keyCode: 0x3c});
+                obj.keyup({keyCode: 0x11});
+            });
+        });
+        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 = KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) {
+                    switch (times_called++) {
+                    case 0: //altgr
+                        break;
+                    case 1: // 'a'
+                        expect(evt).to.not.have.property('escape');
+                        break;
+                    }
+                });
+
+                obj.keydown({keyCode: 0xe1}); // press altgr
+                obj.keydown({keyCode: 'A'.charCodeAt()});
+            });
+
+            it('should indicate on events if a single-key char modifier is down', function(done) {
+                var times_called = 0;
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) {
+                    switch (times_called++) {
+                    case 0: //altgr
+                        break;
+                    case 1: // 'a'
+                        expect(evt).to.be.deep.equal({
+                            type: 'keypress',
+                            keyId: 'A'.charCodeAt(),
+                            keysym: keysyms.lookup('a'.charCodeAt()),
+                            escape: [0xfe03]
+                        });
+                        done();
+                        return;
+                    }
+                });
+
+                obj.keydown({keyCode: 0xe1}); // press altgr
+                obj.keypress({keyCode: 'A'.charCodeAt()});
+            });
+            it('should indicate on events if a multi-key char modifier is down', function(done) {
+                var times_called = 0;
+                var obj = KeyEventDecoder(kbdUtil.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',
+                            keyId: 'A'.charCodeAt(),
+                            keysym: keysyms.lookup('a'.charCodeAt()),
+                            escape: [0xffe9, 0xffe3]
+                        });
+                        done();
+                        return;
+                    }
+                });
+
+                obj.keydown({keyCode: 0x11}); // press ctrl
+                obj.keydown({keyCode: 0x12}); // press alt
+                obj.keypress({keyCode: 'A'.charCodeAt()});
+            });
+            it('should not consider a char modifier to be down on the modifier key itself', function() {
+                var obj = KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) {
+                    expect(evt).to.not.have.property('escape');
+                });
+
+                obj.keydown({keyCode: 0xe1}); // press altgr
+
+            });
+        });
+        describe('add/remove keysym', function() {
+            it('should remove keysym from keydown if a char key and no modifier', function() {
+                KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                    expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
+                }).keydown({keyCode: 0x41});
+            });
+            it('should not remove keysym from keydown if a shortcut modifier is down', function() {
+                var times_called = 0;
+                KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                    switch (times_called++) {
+                    case 1:
+                        expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keydown'});
+                        break;
+                    }
+                }).keydown({keyCode: 0x41, ctrlKey: true});
+                expect(times_called).to.be.equal(2);
+            });
+            it('should not remove keysym from keydown if a char modifier is down', function() {
+                var times_called = 0;
+                KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) {
+                    switch (times_called++) {
+                    case 2:
+                        expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keydown'});
+                        break;
+                    }
+                }).keydown({keyCode: 0x41, altGraphKey: true});
+                expect(times_called).to.be.equal(3);
+            });
+            it('should not remove keysym from keydown if key is noncharacter', function() {
+                KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                    expect(evt, 'bacobjpace').to.be.deep.equal({keyId: 0x09, keysym: keysyms.lookup(0xff09), type: 'keydown'});
+                }).keydown({keyCode: 0x09});
+
+                KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                    expect(evt, 'ctrl').to.be.deep.equal({keyId: 0x11, keysym: keysyms.lookup(0xffe3), type: 'keydown'});
+                }).keydown({keyCode: 0x11});
+            });
+            it('should never remove keysym from keypress', function() {
+                KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                    expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keypress'});
+                }).keypress({keyCode: 0x41});
+            });
+            it('should never remove keysym from keyup', function() {
+                KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
+                    expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keyup'});
+                }).keyup({keyCode: 0x41});
+            });
+        });
+        // on keypress, keyup(?), always set keysym
+        // on keydown, only do it if we don't expect a keypress: if noncharacter OR modifier is down
+    });
+
+    describe('Verify that char modifiers are active', function() {
+        it('should pass keydown events through if there is no stall', function(done) {
+            var obj = VerifyCharModifier(function(evt){
+                expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+                done();
+            })({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+        });
+        it('should pass keyup events through if there is no stall', function(done) {
+            var obj = VerifyCharModifier(function(evt){
+                expect(evt).to.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+                done();
+            })({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+        });
+        it('should pass keypress events through if there is no stall', function(done) {
+            var obj = VerifyCharModifier(function(evt){
+                expect(evt).to.deep.equal({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+                done();
+            })({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+        });
+        it('should not pass stall events through', function(done){
+            var obj = VerifyCharModifier(function(evt){
+                // should only be called once, for the keydown
+                expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+                done();
+            });
+
+            obj({type: 'stall'});
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+        });
+        it('should merge keydown and keypress events if they come after a stall', function(done) {
+            var next_called = false;
+            var obj = VerifyCharModifier(function(evt){
+                // should only be called once, for the keydown
+                expect(next_called).to.be.false;
+                next_called = true;
+                expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x44)});
+                done();
+            });
+
+            obj({type: 'stall'});
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+            obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
+            expect(next_called).to.be.false;
+        });
+        it('should preserve modifier attribute when merging if keysyms differ', function(done) {
+            var next_called = false;
+            var obj = VerifyCharModifier(function(evt){
+                // should only be called once, for the keydown
+                expect(next_called).to.be.false;
+                next_called = true;
+                expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x44), escape: [0xffe3]});
+                done();
+            });
+
+            obj({type: 'stall'});
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+            obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44), escape: [0xffe3]});
+            expect(next_called).to.be.false;
+        });
+        it('should not preserve modifier attribute when merging if keysyms are the same', function() {
+            var obj = VerifyCharModifier(function(evt){
+                expect(evt).to.not.have.property('escape');
+            });
+
+            obj({type: 'stall'});
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+            obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x42), escape: [0xffe3]});
+        });
+        it('should not merge keydown and keypress events if there is no stall', function(done) {
+            var times_called = 0;
+            var obj = VerifyCharModifier(function(evt){
+                switch(times_called) {
+                case 0:
+                    expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+                    break;
+                case 1:
+                    expect(evt).to.deep.equal({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
+                    done();
+                    break;
+                }
+
+                ++times_called;
+            });
+
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+            obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
+        });
+        it('should not merge keydown and keypress events if separated by another event', function(done) {
+            var times_called = 0;
+            var obj = VerifyCharModifier(function(evt){
+                switch(times_called) {
+                case 0:
+                    expect(evt,1).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+                    break;
+                case 1:
+                    expect(evt,2).to.deep.equal({type: 'keyup', keyId: 0x43, keysym: keysyms.lookup(0x44)});
+                    break;
+                case 2:
+                    expect(evt,3).to.deep.equal({type: 'keypress', keyId: 0x45, keysym: keysyms.lookup(0x46)});
+                    done();
+                    break;
+                }
+
+                ++times_called;
+            });
+
+            obj({type: 'stall'});
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+            obj({type: 'keyup', keyId: 0x43, keysym: keysyms.lookup(0x44)});
+            obj({type: 'keypress', keyId: 0x45, keysym: keysyms.lookup(0x46)});
+        });
+    });
+
+    describe('Track Key State', function() {
+        it('should do nothing on keyup events if no keys are down', function() {
+            var obj = TrackKeyState(function(evt) {
+                expect(true).to.be.false;
+            });
+            obj({type: 'keyup', keyId: 0x41});
+        });
+        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 = TrackKeyState(function(evt) {
+                ++times_called;
+                if (elem.type == 'keyup') {
+                expect(evt).to.have.property('keysym');
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    delete keysymsdown[evt.keysym.keysym];
+                }
+                else {
+                    expect(evt).to.be.deep.equal(elem);
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                }
+                elem = null;
+            });
+
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keyup', keyId: 0x41};
+            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 = TrackKeyState(function(evt) {
+                ++times_called;
+                if (elem.type == 'keyup') {
+                    expect(evt).to.have.property('keysym');
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    delete keysymsdown[evt.keysym.keysym];
+                }
+                else {
+                    expect(evt).to.be.deep.equal(elem);
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                }
+                elem = null;
+            });
+
+            expect(elem).to.be.null;
+            elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keyup', keyId: 0x41};
+            obj(elem);
+            expect(elem).to.be.null;
+            expect(times_called).to.be.equal(2);
+        });
+        it('should add keysym to last key entry if keyId matches', function() {
+            // this implies that a single keyup will release both keysyms
+            var times_called = 0;
+            var elem = null;
+            var keysymsdown = {};
+            var obj = TrackKeyState(function(evt) {
+                ++times_called;
+                if (elem.type == 'keyup') {
+                    expect(evt).to.have.property('keysym');
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    delete keysymsdown[evt.keysym.keysym];
+                }
+                else {
+                    expect(evt).to.be.deep.equal(elem);
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    elem = null;
+                }
+            });
+
+            expect(elem).to.be.null;
+            elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x43)};
+            keysymsdown[keysyms.lookup(0x43).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keyup', keyId: 0x41};
+            obj(elem);
+            expect(times_called).to.be.equal(4);
+        });
+        it('should create new key entry if keyId 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 = TrackKeyState(function(evt) {
+                ++times_called;
+                if (elem.type == 'keyup') {
+                    expect(evt).to.have.property('keysym');
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    delete keysymsdown[evt.keysym.keysym];
+                }
+                else {
+                    expect(evt).to.be.deep.equal(elem);
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    elem = null;
+                }
+            });
+
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x43)};
+            keysymsdown[keysyms.lookup(0x43).keysym] = true;
+            obj(elem);
+            expect(times_called).to.be.equal(2);
+            expect(elem).to.be.null;
+            elem = {type: 'keyup', keyId: 0};
+            obj(elem);
+            expect(times_called).to.be.equal(3);
+            elem = {type: 'keyup', keyId: 0};
+            obj(elem);
+            expect(times_called).to.be.equal(4);
+        });
+        it('should merge key entry if keyIds 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 = TrackKeyState(function(evt) {
+                ++times_called;
+                if (elem.type == 'keyup') {
+                    expect(evt).to.have.property('keysym');
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    delete keysymsdown[evt.keysym.keysym];
+                }
+                else {
+                    expect(evt).to.be.deep.equal(elem);
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    elem = null;
+                }
+            });
+
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(times_called).to.be.equal(2);
+            expect(elem).to.be.null;
+            elem = {type: 'keyup', keyId: 0};
+            obj(elem);
+            expect(times_called).to.be.equal(3);
+        });
+        it('should add keysym as separate entry if keyId does not match last event', function() {
+            // this implies that separate keyups are required
+            var times_called = 0;
+            var elem = null;
+            var keysymsdown = {};
+            var obj = TrackKeyState(function(evt) {
+                ++times_called;
+                if (elem.type == 'keyup') {
+                    expect(evt).to.have.property('keysym');
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    delete keysymsdown[evt.keysym.keysym];
+                }
+                else {
+                    expect(evt).to.be.deep.equal(elem);
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    elem = null;
+                }
+            });
+
+            expect(elem).to.be.null;
+            elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keypress', keyId: 0x42, keysym: keysyms.lookup(0x43)};
+            keysymsdown[keysyms.lookup(0x43).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keyup', keyId: 0x41};
+            obj(elem);
+            expect(times_called).to.be.equal(4);
+            elem = {type: 'keyup', keyId: 0x42};
+            obj(elem);
+            expect(times_called).to.be.equal(4);
+        });
+        it('should add keysym as separate entry if keyId does not match last event and first is zero', function() {
+            // this implies that separate keyups are required
+            var times_called = 0;
+            var elem = null;
+            var keysymsdown = {};
+            var obj = TrackKeyState(function(evt) {
+                ++times_called;
+                if (elem.type == 'keyup') {
+                    expect(evt).to.have.property('keysym');
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    delete keysymsdown[evt.keysym.keysym];
+                }
+                else {
+                    expect(evt).to.be.deep.equal(elem);
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    elem = null;
+                }
+            });
+
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x43)};
+            keysymsdown[keysyms.lookup(0x43).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            expect(times_called).to.be.equal(2);
+            elem = {type: 'keyup', keyId: 0};
+            obj(elem);
+            expect(times_called).to.be.equal(3);
+            elem = {type: 'keyup', keyId: 0x42};
+            obj(elem);
+            expect(times_called).to.be.equal(4);
+        });
+        it('should add keysym as separate entry if keyId does not match last event and second is zero', function() {
+            // this implies that a separate keyups are required
+            var times_called = 0;
+            var elem = null;
+            var keysymsdown = {};
+            var obj = TrackKeyState(function(evt) {
+                ++times_called;
+                if (elem.type == 'keyup') {
+                    expect(evt).to.have.property('keysym');
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    delete keysymsdown[evt.keysym.keysym];
+                }
+                else {
+                    expect(evt).to.be.deep.equal(elem);
+                    expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
+                    elem = null;
+                }
+            });
+
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)};
+            keysymsdown[keysyms.lookup(0x42).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x43)};
+            keysymsdown[keysyms.lookup(0x43).keysym] = true;
+            obj(elem);
+            expect(elem).to.be.null;
+            elem = {type: 'keyup', keyId: 0x41};
+            obj(elem);
+            expect(times_called).to.be.equal(3);
+            elem = {type: 'keyup', keyId: 0};
+            obj(elem);
+            expect(times_called).to.be.equal(4);
+        });
+        it('should pop matching key event on keyup', function() {
+            var times_called = 0;
+            var obj = 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', keyId: 0x42, keysym: keysyms.lookup(0x62)});
+                        break;
+                }
+            });
+
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x61)});
+            obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x62)});
+            obj({type: 'keydown', keyId: 0x43, keysym: keysyms.lookup(0x63)});
+            obj({type: 'keyup', keyId: 0x42});
+            expect(times_called).to.equal(4);
+        });
+        it('should pop the first zero keyevent on keyup with zero keyId', function() {
+            var times_called = 0;
+            var obj = 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', keyId: 0, keysym: keysyms.lookup(0x61)});
+                        break;
+                }
+            });
+
+            obj({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x61)});
+            obj({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x62)});
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x63)});
+            obj({type: 'keyup', keyId: 0x0});
+            expect(times_called).to.equal(4);
+        });
+        it('should pop the last keyevents keysym if no match is found for keyId', function() {
+            var times_called = 0;
+            var obj = 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', keyId: 0x44, keysym: keysyms.lookup(0x63)});
+                        break;
+                }
+            });
+
+            obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x61)});
+            obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x62)});
+            obj({type: 'keydown', keyId: 0x43, keysym: keysyms.lookup(0x63)});
+            obj({type: 'keyup', keyId: 0x44});
+            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 = TrackKeyState(function(evt) {
+                    expect(times_called).to.be.equal(0);
+                    ++times_called;
+                });
+
+                obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+                expect(times_called).to.be.equal(1);
+                obj({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x43)});
+            });
+        });
+        describe('releaseAll', function() {
+            it('should do nothing if no keys have been pressed', function() {
+                var times_called = 0;
+                var obj = 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 = TrackKeyState(function(evt) {
+                    switch (times_called++) {
+                    case 2:
+                        expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x41)});
+                        break;
+                    case 3:
+                        expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x42)});
+                        break;
+                    }
+                });
+                obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
+                obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(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;
+                EscapeModifiers(function(evt) {
+                    expect(times_called).to.be.equal(0);
+                    ++times_called;
+                    expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+                })({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(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;
+                EscapeModifiers(function(evt) {
+                    switch(times_called++) {
+                    case 0:
+                        expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0xffe9)});
+                        break;
+                    case 1:
+                        expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0xffe3)});
+                        break;
+                    case 2:
+                        expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xffe9, 0xffe3]});
+                        break;
+                    case 3:
+                        expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0xffe9)});
+                        break;
+                    case 4:
+                        expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0xffe3)});
+                        break;
+                    }
+                })({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(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;
+                EscapeModifiers(function(evt) {
+                    expect(times_called).to.be.equal(0);
+                    ++times_called;
+                    expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xfe03]});
+                })({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(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;
+                EscapeModifiers(function(evt) {
+                    expect(times_called).to.be.equal(0);
+                    ++times_called;
+                    expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+                })({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42)});
+                expect(times_called).to.be.equal(1);
+            });
+        });
+    });
+});