Improve character keysym lookup

Use the more modern 'key' field, and remove some legacy fallbacks
that are no longer required. This also removes the "stall" mechanism
as it is not needed with current browsers.
This commit is contained in:
Pierre Ossman 2017-01-25 11:29:08 +01:00
parent 80cb8ffddd
commit bfa1b237b9
4 changed files with 53 additions and 286 deletions

View File

@ -29,12 +29,10 @@ const Keyboard = function (defaults) {
// create the keyboard handler
this._handler = new KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(),
KeyboardUtil.VerifyCharModifier( /* jshint newcap: false */
KeyboardUtil.TrackKeyState(
KeyboardUtil.EscapeModifiers(this._handleRfbEvent.bind(this))
)
KeyboardUtil.TrackKeyState(
KeyboardUtil.EscapeModifiers(this._handleRfbEvent.bind(this))
)
); /* jshint newcap: true */
);
// keep these here so we can refer to them later
this._eventHandlers = {

View File

@ -196,23 +196,20 @@ export function getKeycode(evt){
// if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
export function getKeysym(evt){
var codepoint;
if (evt.char && evt.char.length === 1) {
codepoint = evt.char.charCodeAt();
}
else if (evt.charCode) {
if ('key' in evt) {
// Ignore special keys
if (evt.key.length === 1) {
codepoint = evt.key.charCodeAt();
}
} else if ('charCode' in evt) {
codepoint = evt.charCode;
}
else if (evt.keyCode && evt.type === 'keypress') {
// IE10 stores the char code as keyCode, and has no other useful properties
codepoint = evt.keyCode;
}
if (codepoint) {
return keysyms.lookup(codepoint);
}
// we could check evt.key here.
// Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
// so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
// so we don't *need* it yet
if (evt.keyCode) {
return keysymFromKeyCode(evt.keyCode, evt.shiftKey);
}
@ -437,7 +434,6 @@ export function TrackQEMUKeyState (next) {
// - determines a code identifying the key that was pressed (corresponding to the code/keyCode properties on the DOM event)
// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
// - marks each event with an 'escape' property if a modifier was down which should be "escaped"
// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
// This information is collected into an object which is passed to the next() function. (one call per event)
export function KeyEventDecoder (modifierState, next) {
"use strict";
@ -476,10 +472,6 @@ export function KeyEventDecoder (modifierState, next) {
// so only do that if we have to.
var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt));
// If a char modifier is down on a keydown, we need to insert a stall,
// so VerifyCharModifier knows to wait and see if a keypress is comnig
var stall = type === 'keydown' && modifierState.activeCharModifier() && !nonCharacterKey(evt);
// if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
var active = modifierState.activeCharModifier();
@ -498,10 +490,6 @@ export function KeyEventDecoder (modifierState, next) {
}
}
if (stall) {
// insert a fake "stall" event
next({type: 'stall'});
}
next(result);
return suppress;
@ -526,64 +514,6 @@ export function KeyEventDecoder (modifierState, next) {
};
};
// Combines keydown and keypress events where necessary to handle char modifiers.
// On some OS'es, a char modifier is sometimes used as a shortcut modifier.
// For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing
// so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not.
// The only way we can distinguish these cases is to wait and see if a keypress event arrives
// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
export function VerifyCharModifier (next) {
"use strict";
var queue = [];
var timer = null;
function process() {
if (timer) {
return;
}
var delayProcess = function () {
clearTimeout(timer);
timer = null;
process();
};
while (queue.length !== 0) {
var cur = queue[0];
queue = queue.splice(1);
switch (cur.type) {
case 'stall':
// insert a delay before processing available events.
/* jshint loopfunc: true */
timer = setTimeout(delayProcess, 5);
/* jshint loopfunc: false */
return;
case 'keydown':
// is the next element a keypress? Then we should merge the two
if (queue.length !== 0 && queue[0].type === 'keypress') {
// Firefox sends keypress even when no char is generated.
// so, if keypress keysym is the same as we'd have guessed from keydown,
// the modifier didn't have any effect, and should not be escaped
if (queue[0].escape && (!cur.keysym || cur.keysym !== queue[0].keysym)) {
cur.escape = queue[0].escape;
}
cur.keysym = queue[0].keysym;
queue = queue.splice(1);
}
break;
}
// swallow stall events, and pass all others to the next stage
if (cur.type !== 'stall') {
next(cur);
}
}
}
return function(evt) {
queue.push(evt);
process();
};
};
// Keeps track of which keys we (and the server) believe are down
// When a keyup is received, match it against this list, to determine the corresponding keysym(s)
// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars

View File

@ -99,25 +99,11 @@ describe('Helpers', function() {
});
describe('getKeysym', function() {
it('should prefer char', function() {
expect(KeyboardUtil.getKeysym({char : 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal(0x61);
it('should prefer key', function() {
expect(KeyboardUtil.getKeysym({key: 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal(0x61);
});
it('should use charCode if no char', function() {
expect(KeyboardUtil.getKeysym({char : '', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal(0x01a9);
it('should use charCode if no key', function() {
expect(KeyboardUtil.getKeysym({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal(0x01a9);
expect(KeyboardUtil.getKeysym({char : 'hello', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal(0x01a9);
});
it('should use keyCode if no charCode', function() {
expect(KeyboardUtil.getKeysym({keyCode: 0x42, which: 0x43, shiftKey: false})).to.be.equal(0x62);
expect(KeyboardUtil.getKeysym({keyCode: 0x42, which: 0x43, shiftKey: true})).to.be.equal(0x42);
});
it('should return null for unknown keycodes', function() {
expect(KeyboardUtil.getKeysym({keyCode: 0xc0, which: 0xc1, shiftKey:false})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: 0xde, which: 0xdf, shiftKey:false})).to.be.null;
});
it('should use which if no keyCode', function() {
expect(KeyboardUtil.getKeysym({which: 0x43, shiftKey: false})).to.be.equal(0x63);
expect(KeyboardUtil.getKeysym({which: 0x43, shiftKey: true})).to.be.equal(0x43);
});
describe('Non-character keys', function() {
@ -133,6 +119,10 @@ describe('Helpers', function() {
expect(KeyboardUtil.getKeysym({keyCode: 0x1b})).to.be.equal(0xFF1B);
expect(KeyboardUtil.getKeysym({keyCode: 0x26})).to.be.equal(0xFF52);
});
it('should return null for unknown keycodes', function() {
expect(KeyboardUtil.getKeysym({keyCode: 0xc0})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: 0xde})).to.be.null;
});
it('should not recognize character keys', function() {
expect(KeyboardUtil.getKeysym({keyCode: 'A'})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: '1'})).to.be.null;

View File

@ -12,26 +12,26 @@ describe('Key Event Pipeline Stages', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.an.object;
done();
}).keydown({code: 'KeyA', keyCode: 0x41});
}).keydown({code: 'KeyA', key: 'a'});
});
it('should pass the right keysym through', function(done) {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt.keysym).to.be.deep.equal(0x61);
done();
}).keypress({code: 'KeyA', keyCode: 0x41});
}).keypress({code: 'KeyA', key: 'a'});
});
it('should pass the right keyid through', function(done) {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.have.property('code', 'KeyA');
done();
}).keydown({code: 'KeyA', keyCode: 0x41});
}).keydown({code: 'KeyA', key: 'a'});
});
it('should not sync modifiers on a keypress', function() {
// Firefox provides unreliable modifier state on keypress events
var count = 0;
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
++count;
}).keypress({code: 'KeyA', keyCode: 0x41, ctrlKey: true});
}).keypress({code: 'KeyA', key: 'a', ctrlKey: true});
expect(count).to.be.equal(1);
});
it('should sync modifiers if necessary', function(done) {
@ -47,61 +47,36 @@ describe('Key Event Pipeline Stages', function() {
done();
break;
}
}).keydown({code: 'KeyA', keyCode: 0x41, ctrlKey: true});
}).keydown({code: 'KeyA', key: 'a', ctrlKey: true});
});
it('should forward keydown events with the right type', function(done) {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', type: 'keydown'});
done();
}).keydown({code: 'KeyA', keyCode: 0x41});
}).keydown({code: 'KeyA', key: 'a'});
});
it('should forward keyup events with the right type', function(done) {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keyup'});
done();
}).keyup({code: 'KeyA', keyCode: 0x41});
}).keyup({code: 'KeyA', key: 'a'});
});
it('should forward keypress events with the right type', function(done) {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keypress'});
done();
}).keypress({code: 'KeyA', keyCode: 0x41});
});
it('should generate stalls if a char modifier is down while a key is pressed', function(done) {
var count = 0;
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {
switch (count) {
case 0: // fake altgr
expect(evt).to.be.deep.equal({keysym: 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',
code: 'KeyA',
keysym: 0x61
});
done();
break;
}
}).keydown({code: 'KeyA', keyCode: 0x41, altGraphKey: true});
}).keypress({code: 'KeyA', key: 'a'});
});
describe('suppress the right events at the right time', function() {
it('should suppress anything while a shortcut modifier is down', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
obj.keydown({keyCode: 0x11}); // press ctrl
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.true;
expect(obj.keydown({key: 'A'})).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
expect(obj.keydown({key: '1'})).to.be.true;
expect(obj.keydown({key: '<'})).to.be.true;
expect(obj.keydown({key: 'ø'})).to.be.true;
});
it('should suppress non-character keys', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
@ -127,48 +102,35 @@ describe('Key Event Pipeline Stages', function() {
it('should not suppress character keys', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false;
expect(obj.keydown({key: 'A'})).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
expect(obj.keydown({key: '1'})).to.be.false;
expect(obj.keydown({key: '<'})).to.be.false; // < key on DK Windows
expect(obj.keydown({key: 'ø'})).to.be.false; // Ø key on DK
});
it('should not suppress if a char modifier is down', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {});
obj.keydown({keyCode: 0xe1}); // press altgr
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false;
expect(obj.keydown({key: 'A'})).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
expect(obj.keydown({key: '1'})).to.be.false;
expect(obj.keydown({key: '<'})).to.be.false;
expect(obj.keydown({key: 'ø'})).to.be.false;
});
});
describe('Keypress and keyup events', function() {
it('should always suppress event propagation', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
expect(obj.keypress({keyCode: 'A'.charCodeAt()})).to.be.true;
expect(obj.keypress({keyCode: 0x3c})).to.be.true; // < key on DK Windows
expect(obj.keypress({key: 'A'})).to.be.true;
expect(obj.keypress({key: '<'})).to.be.true;
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({key: 'A'})).to.be.true;
expect(obj.keyup({key: '<'})).to.be.true;
expect(obj.keyup({keyCode: 0x11})).to.be.true;
});
it('should never generate stalls', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.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() {
@ -184,7 +146,7 @@ describe('Key Event Pipeline Stages', function() {
});
obj.keydown({keyCode: 0xe1}); // press altgr
obj.keydown({code: 'KeyA', keyCode: 0x41});
obj.keydown({code: 'KeyA', key: 'a'});
});
it('should indicate on events if a single-key char modifier is down', function(done) {
@ -206,7 +168,7 @@ describe('Key Event Pipeline Stages', function() {
});
obj.keydown({keyCode: 0xe1}); // press altgr
obj.keypress({code: 'KeyA', keyCode: 0x41});
obj.keypress({code: 'KeyA', key: 'a'});
});
it('should indicate on events if a multi-key char modifier is down', function(done) {
var times_called = 0;
@ -230,7 +192,7 @@ describe('Key Event Pipeline Stages', function() {
obj.keydown({keyCode: 0x11}); // press ctrl
obj.keydown({keyCode: 0x12}); // press alt
obj.keypress({code: 'KeyA', keyCode: 0x41});
obj.keypress({code: 'KeyA', key: 'a'});
});
it('should not consider a char modifier to be down on the modifier key itself', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {
@ -245,7 +207,7 @@ describe('Key Event Pipeline Stages', function() {
it('should remove keysym from keydown if a char key and no modifier', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', type: 'keydown'});
}).keydown({code: 'KeyA', keyCode: 0x41});
}).keydown({code: 'KeyA', key: 'a'});
});
it('should not remove keysym from keydown if a shortcut modifier is down', function() {
var times_called = 0;
@ -255,19 +217,19 @@ describe('Key Event Pipeline Stages', function() {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
break;
}
}).keydown({code: 'KeyA', keyCode: 0x41, ctrlKey: true});
}).keydown({code: 'KeyA', key: 'a', 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;
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {
switch (times_called++) {
case 2:
case 1:
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
break;
}
}).keydown({code: 'KeyA', keyCode: 0x41, altGraphKey: true});
expect(times_called).to.be.equal(3);
}).keydown({code: 'KeyA', key: 'a', altGraphKey: true});
expect(times_called).to.be.equal(2);
});
it('should not remove keysym from keydown if key is noncharacter', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
@ -281,131 +243,18 @@ describe('Key Event Pipeline Stages', function() {
it('should never remove keysym from keypress', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keypress'});
}).keypress({code: 'KeyA', keyCode: 0x41});
}).keypress({code: 'KeyA', key: 'a'});
});
it('should never remove keysym from keyup', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keyup'});
}).keyup({code: 'KeyA', keyCode: 0x41});
}).keyup({code: 'KeyA', key: 'a'});
});
});
// 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 = KeyboardUtil.VerifyCharModifier(function(evt){
expect(evt).to.deep.equal({type: 'keydown', code: 'KeyA', keysym: 0x41});
done();
})({type: 'keydown', code: 'KeyA', keysym: 0x41});
});
it('should pass keyup events through if there is no stall', function(done) {
var obj = KeyboardUtil.VerifyCharModifier(function(evt){
expect(evt).to.deep.equal({type: 'keyup', code: 'KeyA', keysym: 0x41});
done();
})({type: 'keyup', code: 'KeyA', keysym: 0x41});
});
it('should pass keypress events through if there is no stall', function(done) {
var obj = KeyboardUtil.VerifyCharModifier(function(evt){
expect(evt).to.deep.equal({type: 'keypress', code: 'KeyA', keysym: 0x41});
done();
})({type: 'keypress', code: 'KeyA', keysym: 0x41});
});
it('should not pass stall events through', function(done){
var obj = KeyboardUtil.VerifyCharModifier(function(evt){
// should only be called once, for the keydown
expect(evt).to.deep.equal({type: 'keydown', code: 'KeyA', keysym: 0x41});
done();
});
obj({type: 'stall'});
obj({type: 'keydown', code: 'KeyA', keysym: 0x41});
});
it('should merge keydown and keypress events if they come after a stall', function(done) {
var next_called = false;
var obj = KeyboardUtil.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', code: 'KeyA', keysym: 0x44});
done();
});
obj({type: 'stall'});
obj({type: 'keydown', code: 'KeyA', keysym: 0x42});
obj({type: 'keypress', code: 'KeyC', keysym: 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 = KeyboardUtil.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', code: 'KeyA', keysym: 0x44, escape: [0xffe3]});
done();
});
obj({type: 'stall'});
obj({type: 'keydown', code: 'KeyA', keysym: 0x42});
obj({type: 'keypress', code: 'KeyC', keysym: 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 = KeyboardUtil.VerifyCharModifier(function(evt){
expect(evt).to.not.have.property('escape');
});
obj({type: 'stall'});
obj({type: 'keydown', code: 'KeyA', keysym: 0x42});
obj({type: 'keypress', code: 'KeyC', keysym: 0x42, escape: [0xffe3]});
});
it('should not merge keydown and keypress events if there is no stall', function(done) {
var times_called = 0;
var obj = KeyboardUtil.VerifyCharModifier(function(evt){
switch(times_called) {
case 0:
expect(evt).to.deep.equal({type: 'keydown', code: 'KeyA', keysym: 0x42});
break;
case 1:
expect(evt).to.deep.equal({type: 'keypress', code: 'KeyC', keysym: 0x44});
done();
break;
}
++times_called;
});
obj({type: 'keydown', code: 'KeyA', keysym: 0x42});
obj({type: 'keypress', code: 'KeyC', keysym: 0x44});
});
it('should not merge keydown and keypress events if separated by another event', function(done) {
var times_called = 0;
var obj = KeyboardUtil.VerifyCharModifier(function(evt){
switch(times_called) {
case 0:
expect(evt,1).to.deep.equal({type: 'keydown', code: 'KeyA', keysym: 0x42});
break;
case 1:
expect(evt,2).to.deep.equal({type: 'keyup', code: 'KeyC', keysym: 0x44});
break;
case 2:
expect(evt,3).to.deep.equal({type: 'keypress', code: 'KeyE', keysym: 0x46});
done();
break;
}
++times_called;
});
obj({type: 'stall'});
obj({type: 'keydown', code: 'KeyA', keysym: 0x42});
obj({type: 'keyup', code: 'KeyC', keysym: 0x44});
obj({type: 'keypress', code: 'KeyE', keysym: 0x46});
});
});
describe('Track Key State', function() {
it('should do nothing on keyup events if no keys are down', function() {
var obj = KeyboardUtil.TrackKeyState(function(evt) {