// Copyright 2008 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. goog.provide('goog.ui.KeyboardShortcutHandlerTest'); goog.setTestOnly('goog.ui.KeyboardShortcutHandlerTest'); goog.require('goog.dom'); goog.require('goog.events'); goog.require('goog.events.BrowserEvent'); goog.require('goog.events.KeyCodes'); goog.require('goog.testing.MockClock'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.StrictMock'); goog.require('goog.testing.events'); goog.require('goog.testing.jsunit'); goog.require('goog.ui.KeyboardShortcutHandler'); goog.require('goog.userAgent'); var Modifiers = goog.ui.KeyboardShortcutHandler.Modifiers; var KeyCodes = goog.events.KeyCodes; var handler; var targetDiv; var listener; var mockClock; var stubs = new goog.testing.PropertyReplacer(); /** * Fires a keypress on the target div. * @return {boolean} The returnValue of the sequence: false if * preventDefault() was called on any of the events, true otherwise. */ function fire(keycode, opt_extraProperties, opt_element) { return goog.testing.events.fireKeySequence( opt_element || targetDiv, keycode, opt_extraProperties); } /** * Simulates a complete keystroke (keydown, keypress, and keyup) when typing * a non-ASCII character. * * @param {number} keycode The keycode of the keydown and keyup events. * @param {number} keyPressKeyCode The keycode of the keypress event. * @param {Object=} opt_extraProperties Event properties to be mixed into the * BrowserEvent. * @param {EventTarget=} opt_element Optional target for the event. * @return {boolean} The returnValue of the sequence: false if preventDefault() * was called on any of the events, true otherwise. */ function fireAltGraphKey( keycode, keyPressKeyCode, opt_extraProperties, opt_element) { return goog.testing.events.fireNonAsciiKeySequence( opt_element || targetDiv, keycode, keyPressKeyCode, opt_extraProperties); } function setUp() { targetDiv = goog.dom.getElement('targetDiv'); handler = new goog.ui.KeyboardShortcutHandler(goog.dom.getElement('rootDiv')); // Create a mock event listener in order to set expectations on what // events are fired. We create a fake class whose only method is // shortcutFired(shortcut identifier). listener = new goog.testing.StrictMock({shortcutFired: goog.nullFunction}); goog.events.listen( handler, goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED, function(event) { listener.shortcutFired(event.identifier); }); // Set up a fake clock, because keyboard shortcuts *are* time // sensitive. mockClock = new goog.testing.MockClock(true); } function tearDown() { mockClock.uninstall(); handler.dispose(); stubs.reset(); } function testAllowsSingleLetterKeyBindingsSpecifiedAsString() { listener.shortcutFired('lettergee'); listener.$replay(); handler.registerShortcut('lettergee', 'g'); fire(KeyCodes.G); listener.$verify(); } function testAllowsSingleLetterKeyBindingsSpecifiedAsStringKeyValue() { listener.shortcutFired('lettergee'); listener.$replay(); handler.registerShortcut('lettergee', 'g'); fire('g'); listener.$verify(); } function testAllowsSingleLetterKeyBindingsSpecifiedAsKeyCode() { listener.shortcutFired('lettergee'); listener.$replay(); handler.registerShortcut('lettergee', KeyCodes.G); fire(KeyCodes.G); listener.$verify(); } function testDoesntFireWhenWrongKeyIsPressed() { listener.$replay(); // no events expected handler.registerShortcut('letterjay', 'j'); fire(KeyCodes.G); fire('g'); listener.$verify(); } function testAllowsControlAndLetterSpecifiedAsAString() { listener.shortcutFired('lettergee'); listener.$replay(); handler.registerShortcut('lettergee', 'ctrl+g'); fire(KeyCodes.G, {ctrlKey: true}); listener.$verify(); } function testAllowsControlAndLetterSpecifiedAsAStringKeyValue() { listener.shortcutFired('lettergee'); listener.$replay(); handler.registerShortcut('lettergee', 'ctrl+g'); fire('g', {ctrlKey: true}); listener.$verify(); } function testAllowsControlAndLetterSpecifiedAsArgSequence() { listener.shortcutFired('lettergeectrl'); listener.$replay(); handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL); fire(KeyCodes.G, {ctrlKey: true}); listener.$verify(); } function testAllowsControlAndLetterSpecifiedAsArray() { listener.shortcutFired('lettergeectrl'); listener.$replay(); handler.registerShortcut('lettergeectrl', [KeyCodes.G, Modifiers.CTRL]); fire(KeyCodes.G, {ctrlKey: true}); listener.$verify(); } function testAllowsShift() { listener.shortcutFired('lettergeeshift'); listener.$replay(); handler.registerShortcut('lettergeeshift', [KeyCodes.G, Modifiers.SHIFT]); fire(KeyCodes.G, {shiftKey: true}); listener.$verify(); } function testAllowsAlt() { listener.shortcutFired('lettergeealt'); listener.$replay(); handler.registerShortcut('lettergeealt', [KeyCodes.G, Modifiers.ALT]); fire(KeyCodes.G, {altKey: true}); listener.$verify(); } function testAllowsMeta() { listener.shortcutFired('lettergeemeta'); listener.$replay(); handler.registerShortcut('lettergeemeta', [KeyCodes.G, Modifiers.META]); fire(KeyCodes.G, {metaKey: true}); listener.$verify(); } function testAllowsMultipleModifiers() { listener.shortcutFired('lettergeectrlaltshift'); listener.$replay(); handler.registerShortcut( 'lettergeectrlaltshift', KeyCodes.G, Modifiers.CTRL | Modifiers.ALT | Modifiers.SHIFT); fireAltGraphKey(KeyCodes.G, 0, {ctrlKey: true, altKey: true, shiftKey: true}); listener.$verify(); } function testAllowsMultipleModifiersSpecifiedAsString() { listener.shortcutFired('lettergeectrlaltshiftmeta'); listener.$replay(); handler.registerShortcut( 'lettergeectrlaltshiftmeta', 'ctrl+shift+alt+meta+g'); fireAltGraphKey( KeyCodes.G, 0, {ctrlKey: true, altKey: true, shiftKey: true, metaKey: true}); listener.$verify(); } function testPreventsDefaultOnReturnFalse() { listener.shortcutFired('x'); listener.$replay(); handler.registerShortcut('x', 'x'); var key = goog.events.listen( handler, goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED, function(event) { return false }); assertFalse( 'return false in listener must prevent default', fire(KeyCodes.X)); listener.$verify(); goog.events.unlistenByKey(key); } function testPreventsDefaultWhenExceptionThrown() { handler.registerShortcut('x', 'x'); handler.setAlwaysPreventDefault(true); goog.events.listenOnce( handler, goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED, function(event) { throw new Error('x'); }); // We can't use the standard infrastructure to detect that // the event was preventDefaulted, because of the exception. var callCount = 0; stubs.set(goog.events.BrowserEvent.prototype, 'preventDefault', function() { callCount++; }); var e = assertThrows(goog.partial(fire, KeyCodes.X)); assertEquals('x', e.message); assertEquals(1, callCount); } function testDoesntFireWhenUserForgetsRequiredModifier() { listener.$replay(); // no events expected handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL); fire(KeyCodes.G); listener.$verify(); } function testDoesntFireIfTooManyModifiersPressed() { listener.$replay(); // no events expected handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL); fire(KeyCodes.G, {ctrlKey: true, metaKey: true}); listener.$verify(); } function testDoesntFireIfAnyRequiredModifierForgotten() { listener.$replay(); // no events expected handler.registerShortcut( 'lettergeectrlaltshift', KeyCodes.G, Modifiers.CTRL | Modifiers.ALT | Modifiers.SHIFT); fire(KeyCodes.G, {altKey: true, shiftKey: true}); listener.$verify(); } function testAllowsMultiKeySequenceSpecifiedAsArray() { listener.shortcutFired('quitemacs'); listener.$replay(); handler.registerShortcut( 'quitemacs', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]); assertFalse(fire(KeyCodes.X, {ctrlKey: true})); fire(KeyCodes.C); listener.$verify(); } function testAllowsMultiKeySequenceSpecifiedAsArguments() { listener.shortcutFired('quitvi'); listener.$replay(); handler.registerShortcut( 'quitvi', KeyCodes.SEMICOLON, Modifiers.SHIFT, KeyCodes.Q, Modifiers.NONE, KeyCodes.NUM_ONE, Modifiers.SHIFT); var shiftProperties = {shiftKey: true}; assertFalse(fire(KeyCodes.SEMICOLON, shiftProperties)); assertFalse(fire(KeyCodes.Q)); fire(KeyCodes.NUM_ONE, shiftProperties); listener.$verify(); } function testMultiKeyEventIsNotFiredIfUserIsTooSlow() { listener.$replay(); // no events expected handler.registerShortcut( 'quitemacs', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]); fire(KeyCodes.X, {ctrlKey: true}); // Wait 3 seconds before hitting C. Although the actual limit is 1500 // at time of writing, it's best not to over-specify functionality. mockClock.tick(3000); fire(KeyCodes.C); listener.$verify(); } function testAllowsMultipleAHandlers() { listener.shortcutFired('quitvi'); listener.shortcutFired('letterex'); listener.shortcutFired('quitemacs'); listener.$replay(); // register 3 handlers in 3 diferent ways handler.registerShortcut( 'quitvi', KeyCodes.SEMICOLON, Modifiers.SHIFT, KeyCodes.Q, Modifiers.NONE, KeyCodes.NUM_ONE, Modifiers.SHIFT); handler.registerShortcut( 'quitemacs', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]); handler.registerShortcut('letterex', 'x'); // quit vi var shiftProperties = {shiftKey: true}; fire(KeyCodes.SEMICOLON, shiftProperties); fire(KeyCodes.Q); fire(KeyCodes.NUM_ONE, shiftProperties); // then press the letter x fire(KeyCodes.X); // then quit emacs fire(KeyCodes.X, {ctrlKey: true}); fire(KeyCodes.C); listener.$verify(); } function testCanRemoveOneHandler() { listener.shortcutFired('letterex'); listener.$replay(); // register 2 handlers, then remove quitvi handler.registerShortcut( 'quitvi', KeyCodes.COLON, Modifiers.NONE, KeyCodes.Q, Modifiers.NONE, KeyCodes.EXCLAMATION, Modifiers.NONE); handler.registerShortcut('letterex', 'x'); handler.unregisterShortcut( KeyCodes.COLON, Modifiers.NONE, KeyCodes.Q, Modifiers.NONE, KeyCodes.EXCLAMATION, Modifiers.NONE); // call the "quit VI" keycodes, even though it is removed fire(KeyCodes.COLON); fire(KeyCodes.Q); fire(KeyCodes.EXCLAMATION); // press the letter x fire(KeyCodes.X); listener.$verify(); } function testCanRemoveTwoHandlers() { listener.$replay(); // no events expected handler.registerShortcut( 'quitemacs', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]); handler.registerShortcut('letterex', 'x'); handler.unregisterShortcut([KeyCodes.X, Modifiers.CTRL, KeyCodes.C]); handler.unregisterShortcut('x'); fire(KeyCodes.X, {ctrlKey: true}); fire(KeyCodes.C); fire(KeyCodes.X); listener.$verify(); } function testIsShortcutRegistered_single() { assertFalse(handler.isShortcutRegistered('x')); handler.registerShortcut('letterex', 'x'); assertTrue(handler.isShortcutRegistered('x')); handler.unregisterShortcut('x'); assertFalse(handler.isShortcutRegistered('x')); } function testIsShortcutRegistered_multi() { assertFalse(handler.isShortcutRegistered('a')); assertFalse(handler.isShortcutRegistered('a b')); assertFalse(handler.isShortcutRegistered('a b c')); handler.registerShortcut('ab', 'a b'); assertFalse(handler.isShortcutRegistered('a')); assertTrue(handler.isShortcutRegistered('a b')); assertFalse(handler.isShortcutRegistered('a b c')); handler.unregisterShortcut('a b'); assertFalse(handler.isShortcutRegistered('a')); assertFalse(handler.isShortcutRegistered('a b')); assertFalse(handler.isShortcutRegistered('a b c')); } function testUnregister_subsequence() { // Unregistering a partial sequence should not orphan shortcuts further in the // sequence. handler.registerShortcut('abc', 'a b c'); handler.unregisterShortcut('a b'); assertTrue(handler.isShortcutRegistered('a b c')); } function testUnregister_supersequence() { // Unregistering a sequence that extends beyond a registered sequence should // do nothing. handler.registerShortcut('ab', 'a b'); handler.unregisterShortcut('a b c'); assertTrue(handler.isShortcutRegistered('a b')); } function testUnregister_partialMatchSequence() { // Unregistering a sequence that partially matches a registered sequence // should do nothing. handler.registerShortcut('abc', 'a b c'); handler.unregisterShortcut('a b x'); assertTrue(handler.isShortcutRegistered('a b c')); } function testUnregister_deadBranch() { // Unregistering a sequence should prune any dead branches in the tree. handler.registerShortcut('abc', 'a b c'); handler.unregisterShortcut('a b c'); // Default is not should not be prevented in the A key stroke because the A // branch has been removed from the tree. assertTrue(fire(KeyCodes.A)); } /** * Registers a slew of keyboard shortcuts to test each primary category * of shortcuts. */ function registerEnterSpaceXF1AltY() { // Enter and space are specially handled keys. handler.registerShortcut('enter', KeyCodes.ENTER); handler.registerShortcut('space', KeyCodes.SPACE); // 'x' should be treated as text in many contexts handler.registerShortcut('x', 'x'); // F1 is a global shortcut. handler.registerShortcut('global', KeyCodes.F1); // Alt-Y has modifiers, which pass through most form elements. handler.registerShortcut('withAlt', 'alt+y'); } /** * Fires enter, space, X, F1, and Alt-Y keys on a widget. * @param {Element} target The element on which to fire the events. */ function fireEnterSpaceXF1AltY(target) { fire(KeyCodes.ENTER, undefined, target); fire(KeyCodes.SPACE, undefined, target); fire(KeyCodes.X, undefined, target); fire(KeyCodes.F1, undefined, target); fire(KeyCodes.Y, {altKey: true}, target); } function testIgnoreNonGlobalShortcutsInSelect() { var targetSelect = goog.dom.getElement('targetSelect'); listener.shortcutFired('global'); listener.shortcutFired('withAlt'); listener.$replay(); registerEnterSpaceXF1AltY(); fireEnterSpaceXF1AltY(goog.dom.getElement('targetSelect')); listener.$verify(); } function testIgnoreNonGlobalShortcutsInTextArea() { listener.shortcutFired('global'); listener.shortcutFired('withAlt'); listener.$replay(); registerEnterSpaceXF1AltY(); fireEnterSpaceXF1AltY(goog.dom.getElement('targetTextArea')); listener.$verify(); } /** * Checks that the shortcuts are fired on each target. * @param {Array} shortcuts A list of shortcut identifiers. * @param {Array} targets A list of element IDs. * @param {function(Element)} fireEvents Function that fires events. */ function expectShortcutsOnTargets(shortcuts, targets, fireEvents) { for (var i = 0, ii = targets.length; i < ii; i++) { for (var j = 0, jj = shortcuts.length; j < jj; j++) { listener.shortcutFired(shortcuts[j]); } listener.$replay(); fireEvents(goog.dom.getElement(targets[i])); listener.$verify(); listener.$reset(); } } function testIgnoreShortcutsExceptEnterInTextInputFields() { var targets = [ 'targetColor', 'targetDate', 'targetDateTime', 'targetDateTimeLocal', 'targetEmail', 'targetMonth', 'targetNumber', 'targetPassword', 'targetSearch', 'targetTel', 'targetText', 'targetTime', 'targetUrl', 'targetWeek' ]; registerEnterSpaceXF1AltY(); expectShortcutsOnTargets( ['enter', 'global', 'withAlt'], targets, fireEnterSpaceXF1AltY); } function testIgnoreSpaceInCheckBoxAndButton() { registerEnterSpaceXF1AltY(); expectShortcutsOnTargets( ['enter', 'x', 'global', 'withAlt'], ['targetCheckBox', 'targetButton'], fireEnterSpaceXF1AltY); } function testIgnoreNonGlobalShortcutsInContentEditable() { // Don't set design mode in later IE as javascripts don't run when in // that mode. var setDesignMode = !goog.userAgent.IE || !goog.userAgent.isVersionOrHigher('9'); try { if (setDesignMode) { document.designMode = 'on'; } targetDiv.contentEditable = 'true'; // Expect only global shortcuts. listener.shortcutFired('global'); listener.$replay(); registerEnterSpaceXF1AltY(); fireEnterSpaceXF1AltY(targetDiv); listener.$verify(); } finally { if (setDesignMode) { document.designMode = 'off'; } targetDiv.contentEditable = 'false'; } } function testSetAllShortcutsAreGlobal() { handler.setAllShortcutsAreGlobal(true); registerEnterSpaceXF1AltY(); expectShortcutsOnTargets( ['enter', 'space', 'x', 'global', 'withAlt'], ['targetTextArea'], fireEnterSpaceXF1AltY); } function testSetModifierShortcutsAreGlobalFalse() { handler.setModifierShortcutsAreGlobal(false); registerEnterSpaceXF1AltY(); expectShortcutsOnTargets( ['global'], ['targetTextArea'], fireEnterSpaceXF1AltY); } function testAltGraphKeyOnUSLayout() { // Windows does not assign printable characters to any ctrl+alt keys of // the US layout. This test verifies we fire shortcut events when typing // ctrl+alt keys on the US layout. listener.shortcutFired('letterOne'); listener.shortcutFired('letterTwo'); listener.shortcutFired('letterThree'); listener.shortcutFired('letterFour'); listener.shortcutFired('letterFive'); if (goog.userAgent.WINDOWS) { listener.$replay(); handler.registerShortcut('letterOne', 'ctrl+alt+1'); handler.registerShortcut('letterTwo', 'ctrl+alt+2'); handler.registerShortcut('letterThree', 'ctrl+alt+3'); handler.registerShortcut('letterFour', 'ctrl+alt+4'); handler.registerShortcut('letterFive', 'ctrl+alt+5'); // Send key events on the English (United States) layout. fireAltGraphKey(KeyCodes.ONE, 0, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.TWO, 0, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.THREE, 0, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.FOUR, 0, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.FIVE, 0, {ctrlKey: true, altKey: true}); listener.$verify(); } } function testAltGraphKeyOnFrenchLayout() { // Windows assigns printable characters to ctrl+alt+[2-5] keys of the // French layout. This test verifies we fire shortcut events only when // we type ctrl+alt+1 keys on the French layout. listener.shortcutFired('letterOne'); if (goog.userAgent.WINDOWS) { listener.$replay(); handler.registerShortcut('letterOne', 'ctrl+alt+1'); handler.registerShortcut('letterTwo', 'ctrl+alt+2'); handler.registerShortcut('letterThree', 'ctrl+alt+3'); handler.registerShortcut('letterFour', 'ctrl+alt+4'); handler.registerShortcut('letterFive', 'ctrl+alt+5'); // Send key events on the French (France) layout. fireAltGraphKey(KeyCodes.ONE, 0, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.TWO, 0x0303, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.THREE, 0x0023, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.FOUR, 0x007b, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.FIVE, 0x205b, {ctrlKey: true, altKey: true}); listener.$verify(); } } function testAltGraphKeyOnSpanishLayout() { // Windows assigns printable characters to ctrl+alt+[1-5] keys of the // Spanish layout. This test verifies we do not fire shortcut events at // all when typing ctrl+alt+[1-5] keys on the Spanish layout. if (goog.userAgent.WINDOWS) { listener.$replay(); handler.registerShortcut('letterOne', 'ctrl+alt+1'); handler.registerShortcut('letterTwo', 'ctrl+alt+2'); handler.registerShortcut('letterThree', 'ctrl+alt+3'); handler.registerShortcut('letterFour', 'ctrl+alt+4'); handler.registerShortcut('letterFive', 'ctrl+alt+5'); // Send key events on the Spanish (Spain) layout. fireAltGraphKey(KeyCodes.ONE, 0x007c, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.TWO, 0x0040, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.THREE, 0x0023, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.FOUR, 0x0303, {ctrlKey: true, altKey: true}); fireAltGraphKey(KeyCodes.FIVE, 0x20ac, {ctrlKey: true, altKey: true}); listener.$verify(); } } function testAltGraphKeyOnPolishLayout_withShift() { // Windows assigns printable characters to ctrl+alt+shift+A key in polish // layout. This test verifies that we do not fire shortcut events for A, but // does fire for Q which does not have a printable character. if (goog.userAgent.WINDOWS) { listener.shortcutFired('letterQ'); listener.$replay(); handler.registerShortcut('letterA', 'ctrl+alt+shift+A'); handler.registerShortcut('letterQ', 'ctrl+alt+shift+Q'); // Send key events on the Polish (Programmer) layout. assertTrue(fireAltGraphKey( KeyCodes.A, 0x0104, {ctrlKey: true, altKey: true, shiftKey: true})); assertFalse(fireAltGraphKey( KeyCodes.Q, 0, {ctrlKey: true, altKey: true, shiftKey: true})); listener.$verify(); } } function testNumpadKeyShortcuts() { var testCases = [ ['letterNumpad0', 'num-0', KeyCodes.NUM_ZERO], ['letterNumpad1', 'num-1', KeyCodes.NUM_ONE], ['letterNumpad2', 'num-2', KeyCodes.NUM_TWO], ['letterNumpad3', 'num-3', KeyCodes.NUM_THREE], ['letterNumpad4', 'num-4', KeyCodes.NUM_FOUR], ['letterNumpad5', 'num-5', KeyCodes.NUM_FIVE], ['letterNumpad6', 'num-6', KeyCodes.NUM_SIX], ['letterNumpad7', 'num-7', KeyCodes.NUM_SEVEN], ['letterNumpad8', 'num-8', KeyCodes.NUM_EIGHT], ['letterNumpad9', 'num-9', KeyCodes.NUM_NINE], ['letterNumpadMultiply', 'num-multiply', KeyCodes.NUM_MULTIPLY], ['letterNumpadPlus', 'num-plus', KeyCodes.NUM_PLUS], ['letterNumpadMinus', 'num-minus', KeyCodes.NUM_MINUS], ['letterNumpadPERIOD', 'num-period', KeyCodes.NUM_PERIOD], ['letterNumpadDIVISION', 'num-division', KeyCodes.NUM_DIVISION] ]; for (var i = 0; i < testCases.length; ++i) { listener.shortcutFired(testCases[i][0]); } listener.$replay(); // Register shortcuts for numpad keys and send numpad-key events. for (var i = 0; i < testCases.length; ++i) { handler.registerShortcut(testCases[i][0], testCases[i][1]); fire(testCases[i][2]); } listener.$verify(); } function testGeckoShortcuts() { listener.shortcutFired('1'); listener.$replay(); handler.registerShortcut('1', 'semicolon'); if (goog.userAgent.GECKO) { fire(goog.events.KeyCodes.FF_SEMICOLON); } else { fire(goog.events.KeyCodes.SEMICOLON); } listener.$verify(); } function testWindows_multiKeyShortcuts() { if (goog.userAgent.WINDOWS) { listener.shortcutFired('1'); listener.$replay(); handler.registerShortcut('1', 'ctrl+alt+n c'); fire(KeyCodes.N, {ctrlKey: true, altKey: true}); fire(KeyCodes.C); listener.$verify(); } } function testWindows_multikeyShortcuts_polishKey() { if (goog.userAgent.WINDOWS) { listener.$replay(); handler.registerShortcut('announceCursorLocation', 'ctrl+alt+a l'); // If a Polish key is a subsection of a keyboard shortcut, then // the key should still be written. assertTrue(fireAltGraphKey( KeyCodes.A, 0x0104, {ctrlKey: true, altKey: true})); listener.$verify(); } } function testRegisterShortcut_modifierOnly() { assertThrows( 'Registering a shortcut with just modifiers should fail.', goog.bind(handler.registerShortcut, handler, 'name', 'Shift')); } function testParseStringShortcut_unknownKey() { assertThrows( 'Unknown keys should fail.', goog.bind( goog.ui.KeyboardShortcutHandler.parseStringShortcut, null, 'NotAKey')); } // Regression test for failure to reset keyCode between strokes. function testParseStringShortcut_resetKeyCode() { var strokes = goog.ui.KeyboardShortcutHandler.parseStringShortcut('A Shift'); assertNull('The second stroke only has a modifier key.', strokes[1].keyCode); }