| const expect = chai.expect; |
| |
| import Keyboard from '../core/input/keyboard.js'; |
| import * as browser from '../core/util/browser.js'; |
| |
| 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) { |
| 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 (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 (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 () { |
| const kbd = new Keyboard(document); |
| kbd.onkeyevent = sinon.spy(); |
| kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); |
| expect(kbd.onkeyevent).to.not.have.been.called; |
| }); |
| it('should decode keypress events', function (done) { |
| 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 () { |
| 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(kbd.onkeyevent).to.not.have.been.called; |
| }); |
| it('should handle keypress with missing code', function (done) { |
| 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) { |
| 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) { |
| 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) { |
| 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) { |
| 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 (browser.isIE() || browser.isEdge()) this.skip(); |
| }); |
| it('should suppress anything with a valid key', function () { |
| 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 () { |
| 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 () { |
| 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 (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); |
| expect(code).to.be.equal('Unidentified'); |
| expect(down).to.be.equal(true); |
| break; |
| case 1: |
| expect(keysym).to.be.equal(0x61); |
| expect(code).to.be.equal('Unidentified'); |
| expect(down).to.be.equal(false); |
| done(); |
| } |
| }; |
| kbd._handleKeyDown(keyevent('keydown', {code: 'Unidentified', key: 'a'})); |
| }); |
| |
| describe('iOS', function () { |
| let 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 = "iPhone 9.0"; |
| }); |
| afterEach(function () { |
| Object.defineProperty(window, "navigator", origNavigator); |
| }); |
| |
| it('should fake keyup events on iOS', function (done) { |
| 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); |
| expect(code).to.be.equal('KeyA'); |
| expect(down).to.be.equal(true); |
| break; |
| case 1: |
| expect(keysym).to.be.equal(0x61); |
| expect(code).to.be.equal('KeyA'); |
| expect(down).to.be.equal(false); |
| done(); |
| } |
| }; |
| kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); |
| }); |
| }); |
| }); |
| |
| describe('Track Key State', function () { |
| beforeEach(function () { |
| if (browser.isIE() || browser.isEdge()) this.skip(); |
| }); |
| it('should send release using the same keysym as the press', function (done) { |
| 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 () { |
| 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 () { |
| const kbd = new Keyboard(document); |
| kbd.onkeyevent = sinon.spy(); |
| kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); |
| expect(kbd.onkeyevent).to.not.have.been.called; |
| }); |
| |
| describe('Legacy Events', function () { |
| it('should track keys using keyCode if no code', function (done) { |
| 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 () { |
| 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) { |
| 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'})); |
| }); |
| }); |
| }); |
| |
| describe('Shuffle modifiers on macOS', function () { |
| let 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 = "Mac x86_64"; |
| }); |
| afterEach(function () { |
| Object.defineProperty(window, "navigator", origNavigator); |
| }); |
| |
| it('should change Alt to AltGraph', function () { |
| let count = 0; |
| const kbd = new Keyboard(document); |
| kbd.onkeyevent = (keysym, code, down) => { |
| switch (count++) { |
| case 0: |
| expect(keysym).to.be.equal(0xFF7E); |
| expect(code).to.be.equal('AltLeft'); |
| break; |
| case 1: |
| expect(keysym).to.be.equal(0xFE03); |
| 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) { |
| 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) { |
| 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 () { |
| let 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"; |
| |
| this.clock = sinon.useFakeTimers(); |
| }); |
| afterEach(function () { |
| Object.defineProperty(window, "navigator", origNavigator); |
| this.clock.restore(); |
| }); |
| |
| 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})); |
| expect(kbd.onkeyevent).to.not.have.been.called; |
| }); |
| |
| 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: '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 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})); |
| 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(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); |
| }); |
| }); |
| }); |