// requires local modules: input, keyboard, keysymdef var assert = chai.assert; var expect = chai.expect; /* jshint newcap: false, expr: true */ 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); }); }); }); });