/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
goog.module('goog.ui.KeyboardShortcutHandlerTest');
goog.setTestOnly();
const BrowserEvent = goog.require('goog.events.BrowserEvent');
const KeyCodes = goog.require('goog.events.KeyCodes');
const KeyboardShortcutHandler = goog.require('goog.ui.KeyboardShortcutHandler');
const MockClock = goog.require('goog.testing.MockClock');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const StrictMock = goog.require('goog.testing.StrictMock');
const dom = goog.require('goog.dom');
const events = goog.require('goog.events');
const testSuite = goog.require('goog.testing.testSuite');
const testingEvents = goog.require('goog.testing.events');
const userAgent = goog.require('goog.userAgent');
const Modifiers = KeyboardShortcutHandler.Modifiers;
let handler;
let targetDiv;
let listener;
let mockClock;
const stubs = new 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, extraProperties = undefined, element = undefined) {
return testingEvents.fireKeySequence(
element || targetDiv, keycode, 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=} extraProperties Event properties to be mixed into the
* BrowserEvent.
* @param {EventTarget=} 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, extraProperties = undefined,
element = undefined) {
return testingEvents.fireNonAsciiKeySequence(
element || targetDiv, keycode, keyPressKeyCode, extraProperties);
}
/**
* 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.
* @param {Object?=} extraProperties Event properties to be mixed into the
* BrowserEvent.
*/
function fireEnterSpaceXF1AltY(target, extraProperties) {
if (!extraProperties) {
extraProperties = {};
}
fire(KeyCodes.ENTER, extraProperties, target);
fire(KeyCodes.SPACE, extraProperties, target);
fire(KeyCodes.X, extraProperties, target);
fire(KeyCodes.F1, extraProperties, target);
fire(KeyCodes.Y, Object.assign({}, {altKey: true}, extraProperties), target);
}
/**
* Checks that the shortcuts are fired on each target.
* @param {Array<string>} shortcuts A list of shortcut identifiers.
* @param {Array<string>} targets A list of element IDs.
* @param {function(Element)} fireEvents Function that fires events.
* @suppress {missingProperties} suppression added to enable type checking
*/
function expectShortcutsOnTargets(shortcuts, targets, fireEvents) {
for (let i = 0, ii = targets.length; i < ii; i++) {
for (let j = 0, jj = shortcuts.length; j < jj; j++) {
listener.shortcutFired(shortcuts[j]);
}
listener.$replay();
fireEvents(dom.getElement(targets[i]));
listener.$verify();
listener.$reset();
}
}
// Regression test for failure to reset keyCode between strokes.
testSuite({
setUp() {
targetDiv = dom.getElement('targetDiv');
handler = new KeyboardShortcutHandler(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 StrictMock({shortcutFired: goog.nullFunction});
events.listen(
handler, KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
/**
@suppress {missingProperties} suppression added to enable type
checking
*/
(event) => {
listener.shortcutFired(event.identifier);
});
// Set up a fake clock, because keyboard shortcuts *are* time
// sensitive.
mockClock = new MockClock(true);
},
tearDown() {
mockClock.uninstall();
handler.dispose();
stubs.reset();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsSingleLetterKeyBindingsSpecifiedAsString() {
listener.shortcutFired('lettergee');
listener.$replay();
handler.registerShortcut('lettergee', 'g');
fire(KeyCodes.G);
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsSingleLetterKeyBindingsSpecifiedAsStringKeyValue() {
listener.shortcutFired('lettergee');
listener.$replay();
handler.registerShortcut('lettergee', 'g');
fire('g');
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsSingleLetterKeyBindingsSpecifiedAsKeyCode() {
listener.shortcutFired('lettergee');
listener.$replay();
handler.registerShortcut('lettergee', KeyCodes.G);
fire(KeyCodes.G);
listener.$verify();
},
testDoesntFireWhenWrongKeyIsPressed() {
listener.$replay(); // no events expected
handler.registerShortcut('letterjay', 'j');
fire(KeyCodes.G);
fire('g');
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsControlAndLetterSpecifiedAsAString() {
listener.shortcutFired('lettergee');
listener.$replay();
handler.registerShortcut('lettergee', 'ctrl+g');
fire(KeyCodes.G, {ctrlKey: true});
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsControlAndLetterSpecifiedAsAStringKeyValue() {
listener.shortcutFired('lettergee');
listener.$replay();
handler.registerShortcut('lettergee', 'ctrl+g');
fire('g', {ctrlKey: true});
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsControlAndLetterSpecifiedAsArgSequence() {
listener.shortcutFired('lettergeectrl');
listener.$replay();
handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL);
fire(KeyCodes.G, {ctrlKey: true});
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsControlAndLetterSpecifiedAsArray() {
listener.shortcutFired('lettergeectrl');
listener.$replay();
handler.registerShortcut('lettergeectrl', [KeyCodes.G, Modifiers.CTRL]);
fire(KeyCodes.G, {ctrlKey: true});
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsShift() {
listener.shortcutFired('lettergeeshift');
listener.$replay();
handler.registerShortcut('lettergeeshift', [KeyCodes.G, Modifiers.SHIFT]);
fire(KeyCodes.G, {shiftKey: true});
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsAlt() {
listener.shortcutFired('lettergeealt');
listener.$replay();
handler.registerShortcut('lettergeealt', [KeyCodes.G, Modifiers.ALT]);
fire(KeyCodes.G, {altKey: true});
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsMeta() {
listener.shortcutFired('lettergeemeta');
listener.$replay();
handler.registerShortcut('lettergeemeta', [KeyCodes.G, Modifiers.META]);
fire(KeyCodes.G, {metaKey: true});
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
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();
},
/** @suppress {missingProperties} suppression added to enable type checking */
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();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testPreventsDefaultOnReturnFalse() {
listener.shortcutFired('x');
listener.$replay();
handler.registerShortcut('x', 'x');
const key = events.listen(
handler, KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
(event) => false);
assertFalse(
'return false in listener must prevent default', fire(KeyCodes.X));
listener.$verify();
events.unlistenByKey(key);
},
testPreventsDefaultWhenExceptionThrown() {
handler.registerShortcut('x', 'x');
handler.setAlwaysPreventDefault(true);
events.listenOnce(
handler, KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
(event) => {
throw new Error('x');
});
// We can't use the standard infrastructure to detect that
// the event was preventDefaulted, because of the exception.
let callCount = 0;
stubs.set(BrowserEvent.prototype, 'preventDefault', () => {
callCount++;
});
const e = assertThrows(goog.partial(fire, KeyCodes.X));
assertEquals('x', e.message);
assertEquals(1, callCount);
},
testDoesntFireWhenUserForgetsRequiredModifier() {
listener.$replay(); // no events expected
handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL);
fire(KeyCodes.G);
listener.$verify();
},
testDoesntFireIfTooManyModifiersPressed() {
listener.$replay(); // no events expected
handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL);
fire(KeyCodes.G, {ctrlKey: true, metaKey: true});
listener.$verify();
},
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();
},
/** @suppress {missingProperties} suppression added to enable type checking */
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();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testAllowsMultiKeySequenceSpecifiedAsArguments() {
listener.shortcutFired('quitvi');
listener.$replay();
handler.registerShortcut(
'quitvi', KeyCodes.SEMICOLON, Modifiers.SHIFT, KeyCodes.Q,
Modifiers.NONE, KeyCodes.NUM_ONE, Modifiers.SHIFT);
const shiftProperties = {shiftKey: true};
assertFalse(fire(KeyCodes.SEMICOLON, shiftProperties));
assertFalse(fire(KeyCodes.Q));
fire(KeyCodes.NUM_ONE, shiftProperties);
listener.$verify();
},
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();
},
/** @suppress {missingProperties} suppression added to enable type checking */
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
const 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();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testCanRemoveOneHandler() {
listener.shortcutFired('letterex');
listener.$replay();
// register 2 handlers, then remove quitvi
handler.registerShortcut(
'quitvi', KeyCodes.SEMICOLON, Modifiers.SHIFT, KeyCodes.Q,
Modifiers.NONE, KeyCodes.ONE, Modifiers.SHIFT);
handler.registerShortcut('letterex', 'x');
handler.unregisterShortcut(
KeyCodes.SEMICOLON, Modifiers.SHIFT, KeyCodes.Q, Modifiers.NONE,
KeyCodes.ONE, Modifiers.SHIFT);
// call the "quit VI" keycodes, even though it is removed
fire(KeyCodes.SEMICOLON, Modifiers.SHIFT);
fire(KeyCodes.Q);
fire(KeyCodes.ONE, Modifiers.SHIFT);
// press the letter x
fire(KeyCodes.X);
listener.$verify();
},
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();
},
testIsShortcutRegistered_single() {
assertFalse(handler.isShortcutRegistered('x'));
handler.registerShortcut('letterex', 'x');
assertTrue(handler.isShortcutRegistered('x'));
handler.unregisterShortcut('x');
assertFalse(handler.isShortcutRegistered('x'));
},
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'));
},
testRegisterShortcutThrowsIfShortcutsConflict() {
handler.registerShortcut('ab', 'a b');
assertThrows(
'Registering a shortcut that triggers a pre-existing shortcut when' +
'its sequence is typed out should throw',
() => handler.registerShortcut('abc', 'a b c'));
assertTrue(handler.isShortcutRegistered('a b'));
assertFalse(handler.isShortcutRegistered('a b c'));
// Check that the error message displays the name of the existing shortcut.
try {
handler.registerShortcut('abc', 'a b c');
} catch (e) {
assertEquals(
'Keyboard shortcut conflicts with existing shortcut: ab', e.message);
}
},
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'));
},
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'));
},
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'));
},
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));
},
/** @suppress {missingProperties} suppression added to enable type checking */
testIgnoreNonGlobalShortcutsInSelect() {
const targetSelect = dom.getElement('targetSelect');
listener.shortcutFired('global');
listener.shortcutFired('withAlt');
listener.$replay();
registerEnterSpaceXF1AltY();
fireEnterSpaceXF1AltY(dom.getElement('targetSelect'));
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testIgnoreNonGlobalShortcutsInTextArea() {
listener.shortcutFired('global');
listener.shortcutFired('withAlt');
listener.$replay();
registerEnterSpaceXF1AltY();
fireEnterSpaceXF1AltY(dom.getElement('targetTextArea'));
listener.$verify();
},
testIgnoreShortcutsExceptEnterInTextInputFields() {
const targets = [
'targetColor',
'targetDate',
'targetDateTime',
'targetDateTimeLocal',
'targetEmail',
'targetMonth',
'targetNumber',
'targetPassword',
'targetSearch',
'targetTel',
'targetText',
'targetTime',
'targetUrl',
'targetWeek',
];
registerEnterSpaceXF1AltY();
expectShortcutsOnTargets(
['enter', 'global', 'withAlt'], targets, fireEnterSpaceXF1AltY);
},
/**
@suppress {strictMissingProperties,missingProperties} suppression added to
enable type checking
*/
testIgnoreShortcutsInShadowTextInputFields() {
const shadowDiv = dom.getElement('targetShadow');
// skip if shadow dom is not supported
if (!shadowDiv.attachShadow) {
return;
}
const shadowInput = dom.createElement('input');
shadowDiv.attachShadow({mode: 'open'});
shadowDiv.shadowRoot.appendChild(shadowInput);
registerEnterSpaceXF1AltY();
listener.shortcutFired('enter');
listener.shortcutFired('global');
listener.shortcutFired('withAlt');
listener.$replay();
const shadowProps = {
composed: true,
composedPath: function() {
return [shadowInput];
},
};
fireEnterSpaceXF1AltY(shadowDiv, shadowProps);
listener.$verify();
},
testIgnoreSpaceInCheckBoxAndButton() {
registerEnterSpaceXF1AltY();
expectShortcutsOnTargets(
['enter', 'x', 'global', 'withAlt'], ['targetCheckBox', 'targetButton'],
fireEnterSpaceXF1AltY);
},
/** @suppress {missingProperties} suppression added to enable type checking */
testIgnoreNonGlobalShortcutsInContentEditable() {
// Don't set design mode in later IE as javascripts don't run when in
// that mode.
const setDesignMode = !userAgent.IE;
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';
}
},
testSetAllShortcutsAreGlobal() {
handler.setAllShortcutsAreGlobal(true);
registerEnterSpaceXF1AltY();
expectShortcutsOnTargets(
['enter', 'space', 'x', 'global', 'withAlt'], ['targetTextArea'],
fireEnterSpaceXF1AltY);
},
testSetModifierShortcutsAreGlobalFalse() {
handler.setModifierShortcutsAreGlobal(false);
registerEnterSpaceXF1AltY();
expectShortcutsOnTargets(
['global'], ['targetTextArea'], fireEnterSpaceXF1AltY);
},
/** @suppress {missingProperties} suppression added to enable type checking */
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 (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();
}
},
/** @suppress {missingProperties} suppression added to enable type checking */
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 (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();
}
},
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 (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();
}
},
/** @suppress {missingProperties} suppression added to enable type checking */
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 (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();
}
},
/** @suppress {missingProperties} suppression added to enable type checking */
testNumpadKeyShortcuts() {
const 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 (let i = 0; i < testCases.length; ++i) {
listener.shortcutFired(testCases[i][0]);
}
listener.$replay();
// Register shortcuts for numpad keys and send numpad-key events.
for (let i = 0; i < testCases.length; ++i) {
handler.registerShortcut(testCases[i][0], testCases[i][1]);
fire(testCases[i][2]);
}
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testGeckoShortcuts() {
listener.shortcutFired('1');
listener.$replay();
handler.registerShortcut('1', 'semicolon');
if (userAgent.GECKO) {
fire(KeyCodes.FF_SEMICOLON);
} else {
fire(KeyCodes.SEMICOLON);
}
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testWindows_multiKeyShortcuts() {
if (userAgent.WINDOWS) {
listener.shortcutFired('nextComment');
listener.$replay();
handler.registerShortcut('nextComment', 'ctrl+alt+n ctrl+alt+c');
// We need to specify a keyPressKeyCode of 0 here because on Windows,
// keystrokes that don't produce printable characters don't cause a
// keyPress event to fire.
assertFalse(
fireAltGraphKey(KeyCodes.N, 0, {ctrlKey: true, altKey: true}));
assertFalse(
fireAltGraphKey(KeyCodes.C, 0, {ctrlKey: true, altKey: true}));
listener.$verify();
}
},
/** @suppress {missingProperties} suppression added to enable type checking */
testWindows_multikeyShortcuts_repeatedKeyDoesntInterfere() {
if (userAgent.WINDOWS) {
listener.shortcutFired('announceCursorLocation');
listener.$replay();
handler.registerShortcut('announceAnchorText', 'ctrl+alt+a ctrl+alt+a');
handler.registerShortcut(
'announceCursorLocation', 'ctrl+alt+a ctrl+alt+l');
// We need to specify a keyPressKeyCode of 0 here because on Windows,
// keystrokes that don't produce printable characters don't cause a
// keyPress event to fire.
assertFalse(
fireAltGraphKey(KeyCodes.A, 0, {ctrlKey: true, altKey: true}));
assertFalse(
fireAltGraphKey(KeyCodes.L, 0, {ctrlKey: true, altKey: true}));
listener.$verify();
}
},
testWindows_multikeyShortcuts_polishKey() {
if (userAgent.WINDOWS) {
listener.$replay();
handler.registerShortcut(
'announceCursorLocation', 'ctrl+alt+a ctrl+alt+l');
// If a Polish key is a subsection of a keyboard shortcut, then
// the key should still be written.
assertTrue(
fireAltGraphKey(KeyCodes.A, 0x0105, {ctrlKey: true, altKey: true}));
listener.$verify();
}
},
testRegisterShortcut_modifierOnly() {
assertThrows(
'Registering a shortcut with just modifiers should fail.',
goog.bind(handler.registerShortcut, handler, 'name', 'Shift'));
},
testParseStringShortcut_unknownKey() {
assertThrows(
'Unknown keys should fail.',
goog.bind(
KeyboardShortcutHandler.parseStringShortcut, null, 'NotAKey'));
},
testParseStringShortcut_resetKeyCode() {
const strokes = KeyboardShortcutHandler.parseStringShortcut('A Shift');
assertNull(
'The second stroke only has a modifier key.', strokes[1].keyCode);
},
/** @suppress {missingProperties} suppression added to enable type checking */
testOsxGeckoCopyShortcuts() {
// Ensures that Meta+C still fires a shortcut. In legacy versions of
// Closure, we had to listen for Meta+C/X/V on keyup instead of keydown due
// to a bug in Gecko 1.8 on OS X. This is a sanity check to ensure that
// behavior has not regressed.
listener.shortcutFired('copy');
listener.$replay();
handler.registerShortcut('copy', [KeyCodes.C, Modifiers.META]);
fire(KeyCodes.C, {metaKey: true});
listener.$verify();
},
/** @suppress {missingProperties} suppression added to enable type checking */
testHandleEmptyBrowserEvent() {
const rootDiv = dom.getElement('rootDiv');
const emptyEvent = new BrowserEvent();
emptyEvent.type = events.EventType.KEYDOWN;
emptyEvent.target = rootDiv;
emptyEvent.key = 'g';
emptyEvent.keyCode = KeyCodes.G;
emptyEvent.preventDefault = goog.nullFunction;
emptyEvent.stopPropagation = goog.nullFunction;
handler.registerShortcut('lettergee', 'g');
listener.shortcutFired('lettergee');
listener.$replay();
events.fireListeners(rootDiv, emptyEvent.type, false, emptyEvent);
listener.$verify();
},
});