]> git.proxmox.com Git - mirror_novnc.git/blobdiff - tests/test.keyboard.js
Use fat arrow functions `const foo = () => { ... };` for callbacks
[mirror_novnc.git] / tests / test.keyboard.js
index 4654df9c01aa9420ac09b9437dd23413b4c5138e..c555b4fbfb96c1b200b3d9deb7ca2751cb69f856 100644 (file)
-var assert = chai.assert;
-var expect = chai.expect;
+const expect = chai.expect;
 
-import Keyboard from '../core/input/keyboard.js';
+import sinon from '../vendor/sinon.js';
 
-function isIE() {
-    return navigator && !!(/trident/i).exec(navigator.userAgent);
-}
-function isEdge() {
-    return navigator && !!(/edge/i).exec(navigator.userAgent);
-}
+import Keyboard from '../core/input/keyboard.js';
+import * as browser from '../core/util/browser.js';
 
-/* jshint newcap: false, expr: true */
 describe('Key Event Handling', function() {
     "use strict";
 
     // 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) {
+        const e = { type: typeArg };
+        for (let key in KeyboardEventInit) {
             e[key] = KeyboardEventInit[key];
         }
         e.stopPropagation = sinon.spy();
         e.preventDefault = sinon.spy();
         return e;
-    };
+    }
 
     describe('Decode Keyboard Events', function() {
         it('should decode keydown events', function(done) {
-            if (isIE() || isEdge()) this.skip();
-            var kbd = new Keyboard({
-            onKeyEvent: function(keysym, code, down) {
+            if (browser.isIE() || browser.isEdge()) this.skip();
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = (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) {
-            if (isIE() || isEdge()) this.skip();
-            var calls = 0;
-            var kbd = new Keyboard({
-            onKeyEvent: function(keysym, code, down) {
+            if (browser.isIE() || browser.isEdge()) this.skip();
+            let calls = 0;
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = (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});
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = sinon.spy();
                 kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
-                expect(callback).to.not.have.been.called;
+                expect(kbd.onkeyevent).to.not.have.been.called;
             });
             it('should decode keypress events', function(done) {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (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}));
             });
             it('should ignore keypress with different code', function() {
-                var callback = sinon.spy();
-                var kbd = new Keyboard({onKeyEvent: callback});
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = sinon.spy();
                 kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41}));
                 kbd._handleKeyPress(keyevent('keypress', {code: 'KeyB', charCode: 0x61}));
-                expect(callback).to.not.have.been.called;
+                expect(kbd.onkeyevent).to.not.have.been.called;
             });
             it('should handle keypress with missing code', function(done) {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (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', {charCode: 0x61}));
             });
             it('should guess key if no keypress and numeric key', function(done) {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (keysym, code, down) => {
                     expect(keysym).to.be.equal(0x32);
                     expect(code).to.be.equal('Digit2');
                     expect(down).to.be.equal(true);
                     done();
-                }});
+                };
                 kbd._handleKeyDown(keyevent('keydown', {code: 'Digit2', keyCode: 0x32}));
             });
             it('should guess key if no keypress and alpha key', function(done) {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (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, shiftKey: false}));
             });
             it('should guess key if no keypress and alpha key (with shift)', function(done) {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (keysym, code, down) => {
                     expect(keysym).to.be.equal(0x41);
                     expect(code).to.be.equal('KeyA');
                     expect(down).to.be.equal(true);
                     done();
-                }});
+                };
                 kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: true}));
             });
             it('should not guess key if no keypress and unknown key', function(done) {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (keysym, code, down) => {
                     expect(keysym).to.be.equal(0);
                     expect(code).to.be.equal('KeyA');
                     expect(down).to.be.equal(true);
                     done();
-                }});
+                };
                 kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x09}));
             });
         });
 
         describe('suppress the right events at the right time', function() {
             beforeEach(function () {
-                if (isIE() || isEdge()) this.skip();
+                if (browser.isIE() || browser.isEdge()) this.skip();
             });
             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;
+                const kbd = new Keyboard(document, {});
+                const evt1 = keyevent('keydown', {code: 'KeyA', key: 'a'});
+                kbd._handleKeyDown(evt1);
+                expect(evt1.preventDefault).to.have.been.called;
+                const evt2 = keyevent('keyup', {code: 'KeyA', key: 'a'});
+                kbd._handleKeyUp(evt2);
+                expect(evt2.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});
+                const kbd = new Keyboard(document, {});
+                const 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;
+                const kbd = new Keyboard(document, {});
+                const evt1 = keyevent('keydown', {code: 'KeyA', keyCode: 0x41});
+                kbd._handleKeyDown(evt1);
+                const evt2 = keyevent('keypress', {code: 'KeyA', charCode: 0x41});
+                kbd._handleKeyPress(evt2);
+                expect(evt2.preventDefault).to.have.been.called;
             });
         });
     });
 
     describe('Fake keyup', function() {
         it('should fake keyup events for virtual keyboards', function(done) {
-            if (isIE() || isEdge()) this.skip();
-            var count = 0;
-            var kbd = new Keyboard({
-            onKeyEvent: function(keysym, code, down) {
+            if (browser.isIE() || browser.isEdge()) this.skip();
+            let count = 0;
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = (keysym, code, down) => {
                 switch (count++) {
                     case 0:
                         expect(keysym).to.be.equal(0x61);
@@ -180,12 +174,12 @@ describe('Key Event Handling', function() {
                         expect(down).to.be.equal(false);
                         done();
                 }
-            }});
+            };
             kbd._handleKeyDown(keyevent('keydown', {code: 'Unidentified', key: 'a'}));
         });
 
         describe('iOS', function() {
-            var origNavigator;
+            let origNavigator;
             beforeEach(function () {
                 // window.navigator is a protected read-only property in many
                 // environments, so we need to redefine it whilst running these
@@ -211,10 +205,10 @@ describe('Key Event Handling', function() {
             });
 
             it('should fake keyup events on iOS', function(done) {
-                if (isIE() || isEdge()) this.skip();
-                var count = 0;
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                if (browser.isIE() || browser.isEdge()) this.skip();
+                let count = 0;
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (keysym, code, down) => {
                     switch (count++) {
                         case 0:
                             expect(keysym).to.be.equal(0x61);
@@ -227,7 +221,7 @@ describe('Key Event Handling', function() {
                             expect(down).to.be.equal(false);
                             done();
                     }
-                }});
+                };
                 kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
             });
         });
@@ -235,70 +229,70 @@ describe('Key Event Handling', function() {
 
     describe('Track Key State', function() {
         beforeEach(function () {
-            if (isIE() || isEdge()) this.skip();
+            if (browser.isIE() || browser.isEdge()) this.skip();
         });
         it('should send release using the same keysym as the press', function(done) {
-            var kbd = new Keyboard({
-            onKeyEvent: function(keysym, code, down) {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = (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 send the same keysym for multiple presses', function() {
-            var count = 0;
-            var kbd = new Keyboard({
-            onKeyEvent: function(keysym, code, down) {
+            let count = 0;
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = (keysym, code, down) => {
                 expect(keysym).to.be.equal(0x61);
                 expect(code).to.be.equal('KeyA');
                 expect(down).to.be.equal(true);
                 count++;
-            }});
+            };
             kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
             kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'b'}));
             expect(count).to.be.equal(2);
         });
         it('should do nothing on keyup events if no keys are down', function() {
-            var callback = sinon.spy();
-            var kbd = new Keyboard({onKeyEvent: callback});
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
             kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
-            expect(callback).to.not.have.been.called;
+            expect(kbd.onkeyevent).to.not.have.been.called;
         });
 
         describe('Legacy Events', function() {
             it('should track keys using keyCode if no code', function(done) {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (keysym, code, down) => {
                     expect(keysym).to.be.equal(0x61);
                     expect(code).to.be.equal('Platform65');
                     if (!down) {
                         done();
                     }
-                }});
+                };
                 kbd._handleKeyDown(keyevent('keydown', {keyCode: 65, key: 'a'}));
                 kbd._handleKeyUp(keyevent('keyup', {keyCode: 65, key: 'b'}));
             });
             it('should ignore compositing code', function() {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (keysym, code, down) => {
                     expect(keysym).to.be.equal(0x61);
                     expect(code).to.be.equal('Unidentified');
-                }});
+                };
                 kbd._handleKeyDown(keyevent('keydown', {keyCode: 229, key: 'a'}));
             });
             it('should track keys using keyIdentifier if no code', function(done) {
-                var kbd = new Keyboard({
-                onKeyEvent: function(keysym, code, down) {
+                const kbd = new Keyboard(document);
+                kbd.onkeyevent = (keysym, code, down) => {
                     expect(keysym).to.be.equal(0x61);
                     expect(code).to.be.equal('Platform65');
                     if (!down) {
                         done();
                     }
-                }});
+                };
                 kbd._handleKeyDown(keyevent('keydown', {keyIdentifier: 'U+0041', key: 'a'}));
                 kbd._handleKeyUp(keyevent('keyup', {keyIdentifier: 'U+0041', key: 'b'}));
             });
@@ -306,7 +300,7 @@ describe('Key Event Handling', function() {
     });
 
     describe('Shuffle modifiers on macOS', function() {
-        var origNavigator;
+        let origNavigator;
         beforeEach(function () {
             // window.navigator is a protected read-only property in many
             // environments, so we need to redefine it whilst running these
@@ -332,9 +326,9 @@ describe('Key Event Handling', function() {
         });
 
         it('should change Alt to AltGraph', function() {
-            var count = 0;
-            var kbd = new Keyboard({
-            onKeyEvent: function(keysym, code, down) {
+            let count = 0;
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = (keysym, code, down) => {
                 switch (count++) {
                     case 0:
                         expect(keysym).to.be.equal(0xFF7E);
@@ -345,33 +339,33 @@ describe('Key Event Handling', function() {
                         expect(code).to.be.equal('AltRight');
                         break;
                 }
-            }});
+            };
             kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1}));
             kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));
             expect(count).to.be.equal(2);
         });
         it('should change left Super to Alt', function(done) {
-            var kbd = new Keyboard({
-            onKeyEvent: function(keysym, code, down) {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = (keysym, code, down) => {
                 expect(keysym).to.be.equal(0xFFE9);
                 expect(code).to.be.equal('MetaLeft');
                 done();
-            }});
+            };
             kbd._handleKeyDown(keyevent('keydown', {code: 'MetaLeft', key: 'Meta', location: 1}));
         });
         it('should change right Super to left Super', function(done) {
-            var kbd = new Keyboard({
-            onKeyEvent: function(keysym, code, down) {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = (keysym, code, down) => {
                 expect(keysym).to.be.equal(0xFFEB);
                 expect(code).to.be.equal('MetaRight');
                 done();
-            }});
+            };
             kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta', location: 2}));
         });
     });
 
     describe('Escape AltGraph on Windows', function() {
-        var origNavigator;
+        let origNavigator;
         beforeEach(function () {
             // window.navigator is a protected read-only property in many
             // environments, so we need to redefine it whilst running these
@@ -391,108 +385,128 @@ describe('Key Event Handling', function() {
             }
 
             window.navigator.platform = "Windows x86_64";
+
+            this.clock = sinon.useFakeTimers();
         });
         afterEach(function () {
             Object.defineProperty(window, "navigator", origNavigator);
+            this.clock.restore();
         });
 
-        it('should generate fake undo/redo events on press when AltGraph is down', function() {
-            var times_called = 0;
-            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(0xFFEA);
-                    expect(code).to.be.equal('AltRight');
-                    expect(down).to.be.equal(true);
-                    break;
-                case 2:
-                    expect(keysym).to.be.equal(0xFFEA);
-                    expect(code).to.be.equal('AltRight');
-                    expect(down).to.be.equal(false);
-                    break;
-                case 3:
-                    expect(keysym).to.be.equal(0xFFE3);
-                    expect(code).to.be.equal('ControlLeft');
-                    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(0xFFE3);
-                    expect(code).to.be.equal('ControlLeft');
-                    expect(down).to.be.equal(true);
-                    break;
-                case 6:
-                    expect(keysym).to.be.equal(0xFFEA);
-                    expect(code).to.be.equal('AltRight');
-                    expect(down).to.be.equal(true);
-                    break;
-                }
-            }});
-            // First the modifier combo
+        it('should supress ControlLeft until it knows if it is AltGr', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
             kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
-            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));
-            // Next a normal character
-            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
-            expect(times_called).to.be.equal(7);
+            expect(kbd.onkeyevent).to.not.have.been.called;
         });
-        it('should no do anything on key release', function() {
-            var times_called = 0;
-            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
+
+        it('should not trigger on repeating ControlLeft', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
             kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
-            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));
-            // 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);
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
+            expect(kbd.onkeyevent).to.have.been.calledTwice;
+            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true);
+            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", true);
         });
-        it('should not consider a char modifier to be down on the modifier key itself', function() {
-            var times_called = 0;
-            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
+
+        it('should not supress ControlRight', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlRight', key: 'Control', location: 2}));
+            expect(kbd.onkeyevent).to.have.been.calledOnce;
+            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe4, "ControlRight", true);
+        });
+
+        it('should release ControlLeft after 100 ms', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
             kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
-            kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1}));
-            // Then one of the keys again
+            expect(kbd.onkeyevent).to.not.have.been.called;
+            this.clock.tick(100);
+            expect(kbd.onkeyevent).to.have.been.calledOnce;
+            expect(kbd.onkeyevent).to.have.been.calledWith(0xffe3, "ControlLeft", true);
+        });
+
+        it('should release ControlLeft on other key press', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
+            expect(kbd.onkeyevent).to.not.have.been.called;
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
+            expect(kbd.onkeyevent).to.have.been.calledTwice;
+            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true);
+            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0x61, "KeyA", true);
+
+            // Check that the timer is properly dead
+            kbd.onkeyevent.reset();
+            this.clock.tick(100);
+            expect(kbd.onkeyevent).to.not.have.been.called;
+        });
+
+        it('should release ControlLeft on other key release', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
+            kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
             kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1}));
-            expect(times_called).to.be.equal(3);
+            expect(kbd.onkeyevent).to.have.been.calledOnce;
+            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x61, "KeyA", true);
+            kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
+            expect(kbd.onkeyevent).to.have.been.calledThrice;
+            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", true);
+            expect(kbd.onkeyevent.thirdCall).to.have.been.calledWith(0x61, "KeyA", false);
+
+            // Check that the timer is properly dead
+            kbd.onkeyevent.reset();
+            this.clock.tick(100);
+            expect(kbd.onkeyevent).to.not.have.been.called;
+        });
+
+        it('should generate AltGraph for quick Ctrl+Alt sequence', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()}));
+            this.clock.tick(20);
+            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()}));
+            expect(kbd.onkeyevent).to.have.been.calledOnce;
+            expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true);
+
+            // Check that the timer is properly dead
+            kbd.onkeyevent.reset();
+            this.clock.tick(100);
+            expect(kbd.onkeyevent).to.not.have.been.called;
+        });
+
+        it('should generate Ctrl, Alt for slow Ctrl+Alt sequence', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
+            kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()}));
+            this.clock.tick(60);
+            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()}));
+            expect(kbd.onkeyevent).to.have.been.calledTwice;
+            expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true);
+            expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffea, "AltRight", true);
+
+            // Check that the timer is properly dead
+            kbd.onkeyevent.reset();
+            this.clock.tick(100);
+            expect(kbd.onkeyevent).to.not.have.been.called;
+        });
+
+        it('should pass through single Alt', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
+            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2}));
+            expect(kbd.onkeyevent).to.have.been.calledOnce;
+            expect(kbd.onkeyevent).to.have.been.calledWith(0xffea, 'AltRight', true);
+        });
+
+        it('should pass through single AltGr', function () {
+            const kbd = new Keyboard(document);
+            kbd.onkeyevent = sinon.spy();
+            kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2}));
+            expect(kbd.onkeyevent).to.have.been.calledOnce;
+            expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true);
         });
     });
 });