keyboardshortcuthandler_test.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. goog.provide('goog.ui.KeyboardShortcutHandlerTest');
  15. goog.setTestOnly('goog.ui.KeyboardShortcutHandlerTest');
  16. goog.require('goog.dom');
  17. goog.require('goog.events');
  18. goog.require('goog.events.BrowserEvent');
  19. goog.require('goog.events.KeyCodes');
  20. goog.require('goog.testing.MockClock');
  21. goog.require('goog.testing.PropertyReplacer');
  22. goog.require('goog.testing.StrictMock');
  23. goog.require('goog.testing.events');
  24. goog.require('goog.testing.jsunit');
  25. goog.require('goog.ui.KeyboardShortcutHandler');
  26. goog.require('goog.userAgent');
  27. var Modifiers = goog.ui.KeyboardShortcutHandler.Modifiers;
  28. var KeyCodes = goog.events.KeyCodes;
  29. var handler;
  30. var targetDiv;
  31. var listener;
  32. var mockClock;
  33. var stubs = new goog.testing.PropertyReplacer();
  34. /**
  35. * Fires a keypress on the target div.
  36. * @return {boolean} The returnValue of the sequence: false if
  37. * preventDefault() was called on any of the events, true otherwise.
  38. */
  39. function fire(keycode, opt_extraProperties, opt_element) {
  40. return goog.testing.events.fireKeySequence(
  41. opt_element || targetDiv, keycode, opt_extraProperties);
  42. }
  43. /**
  44. * Simulates a complete keystroke (keydown, keypress, and keyup) when typing
  45. * a non-ASCII character.
  46. *
  47. * @param {number} keycode The keycode of the keydown and keyup events.
  48. * @param {number} keyPressKeyCode The keycode of the keypress event.
  49. * @param {Object=} opt_extraProperties Event properties to be mixed into the
  50. * BrowserEvent.
  51. * @param {EventTarget=} opt_element Optional target for the event.
  52. * @return {boolean} The returnValue of the sequence: false if preventDefault()
  53. * was called on any of the events, true otherwise.
  54. */
  55. function fireAltGraphKey(
  56. keycode, keyPressKeyCode, opt_extraProperties, opt_element) {
  57. return goog.testing.events.fireNonAsciiKeySequence(
  58. opt_element || targetDiv, keycode, keyPressKeyCode, opt_extraProperties);
  59. }
  60. function setUp() {
  61. targetDiv = goog.dom.getElement('targetDiv');
  62. handler = new goog.ui.KeyboardShortcutHandler(goog.dom.getElement('rootDiv'));
  63. // Create a mock event listener in order to set expectations on what
  64. // events are fired. We create a fake class whose only method is
  65. // shortcutFired(shortcut identifier).
  66. listener = new goog.testing.StrictMock({shortcutFired: goog.nullFunction});
  67. goog.events.listen(
  68. handler, goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
  69. function(event) { listener.shortcutFired(event.identifier); });
  70. // Set up a fake clock, because keyboard shortcuts *are* time
  71. // sensitive.
  72. mockClock = new goog.testing.MockClock(true);
  73. }
  74. function tearDown() {
  75. mockClock.uninstall();
  76. handler.dispose();
  77. stubs.reset();
  78. }
  79. function testAllowsSingleLetterKeyBindingsSpecifiedAsString() {
  80. listener.shortcutFired('lettergee');
  81. listener.$replay();
  82. handler.registerShortcut('lettergee', 'g');
  83. fire(KeyCodes.G);
  84. listener.$verify();
  85. }
  86. function testAllowsSingleLetterKeyBindingsSpecifiedAsStringKeyValue() {
  87. listener.shortcutFired('lettergee');
  88. listener.$replay();
  89. handler.registerShortcut('lettergee', 'g');
  90. fire('g');
  91. listener.$verify();
  92. }
  93. function testAllowsSingleLetterKeyBindingsSpecifiedAsKeyCode() {
  94. listener.shortcutFired('lettergee');
  95. listener.$replay();
  96. handler.registerShortcut('lettergee', KeyCodes.G);
  97. fire(KeyCodes.G);
  98. listener.$verify();
  99. }
  100. function testDoesntFireWhenWrongKeyIsPressed() {
  101. listener.$replay(); // no events expected
  102. handler.registerShortcut('letterjay', 'j');
  103. fire(KeyCodes.G);
  104. fire('g');
  105. listener.$verify();
  106. }
  107. function testAllowsControlAndLetterSpecifiedAsAString() {
  108. listener.shortcutFired('lettergee');
  109. listener.$replay();
  110. handler.registerShortcut('lettergee', 'ctrl+g');
  111. fire(KeyCodes.G, {ctrlKey: true});
  112. listener.$verify();
  113. }
  114. function testAllowsControlAndLetterSpecifiedAsAStringKeyValue() {
  115. listener.shortcutFired('lettergee');
  116. listener.$replay();
  117. handler.registerShortcut('lettergee', 'ctrl+g');
  118. fire('g', {ctrlKey: true});
  119. listener.$verify();
  120. }
  121. function testAllowsControlAndLetterSpecifiedAsArgSequence() {
  122. listener.shortcutFired('lettergeectrl');
  123. listener.$replay();
  124. handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL);
  125. fire(KeyCodes.G, {ctrlKey: true});
  126. listener.$verify();
  127. }
  128. function testAllowsControlAndLetterSpecifiedAsArray() {
  129. listener.shortcutFired('lettergeectrl');
  130. listener.$replay();
  131. handler.registerShortcut('lettergeectrl', [KeyCodes.G, Modifiers.CTRL]);
  132. fire(KeyCodes.G, {ctrlKey: true});
  133. listener.$verify();
  134. }
  135. function testAllowsShift() {
  136. listener.shortcutFired('lettergeeshift');
  137. listener.$replay();
  138. handler.registerShortcut('lettergeeshift', [KeyCodes.G, Modifiers.SHIFT]);
  139. fire(KeyCodes.G, {shiftKey: true});
  140. listener.$verify();
  141. }
  142. function testAllowsAlt() {
  143. listener.shortcutFired('lettergeealt');
  144. listener.$replay();
  145. handler.registerShortcut('lettergeealt', [KeyCodes.G, Modifiers.ALT]);
  146. fire(KeyCodes.G, {altKey: true});
  147. listener.$verify();
  148. }
  149. function testAllowsMeta() {
  150. listener.shortcutFired('lettergeemeta');
  151. listener.$replay();
  152. handler.registerShortcut('lettergeemeta', [KeyCodes.G, Modifiers.META]);
  153. fire(KeyCodes.G, {metaKey: true});
  154. listener.$verify();
  155. }
  156. function testAllowsMultipleModifiers() {
  157. listener.shortcutFired('lettergeectrlaltshift');
  158. listener.$replay();
  159. handler.registerShortcut(
  160. 'lettergeectrlaltshift', KeyCodes.G,
  161. Modifiers.CTRL | Modifiers.ALT | Modifiers.SHIFT);
  162. fireAltGraphKey(KeyCodes.G, 0, {ctrlKey: true, altKey: true, shiftKey: true});
  163. listener.$verify();
  164. }
  165. function testAllowsMultipleModifiersSpecifiedAsString() {
  166. listener.shortcutFired('lettergeectrlaltshiftmeta');
  167. listener.$replay();
  168. handler.registerShortcut(
  169. 'lettergeectrlaltshiftmeta', 'ctrl+shift+alt+meta+g');
  170. fireAltGraphKey(
  171. KeyCodes.G, 0,
  172. {ctrlKey: true, altKey: true, shiftKey: true, metaKey: true});
  173. listener.$verify();
  174. }
  175. function testPreventsDefaultOnReturnFalse() {
  176. listener.shortcutFired('x');
  177. listener.$replay();
  178. handler.registerShortcut('x', 'x');
  179. var key = goog.events.listen(
  180. handler, goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
  181. function(event) { return false });
  182. assertFalse(
  183. 'return false in listener must prevent default', fire(KeyCodes.X));
  184. listener.$verify();
  185. goog.events.unlistenByKey(key);
  186. }
  187. function testPreventsDefaultWhenExceptionThrown() {
  188. handler.registerShortcut('x', 'x');
  189. handler.setAlwaysPreventDefault(true);
  190. goog.events.listenOnce(
  191. handler, goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
  192. function(event) { throw new Error('x'); });
  193. // We can't use the standard infrastructure to detect that
  194. // the event was preventDefaulted, because of the exception.
  195. var callCount = 0;
  196. stubs.set(goog.events.BrowserEvent.prototype, 'preventDefault', function() {
  197. callCount++;
  198. });
  199. var e = assertThrows(goog.partial(fire, KeyCodes.X));
  200. assertEquals('x', e.message);
  201. assertEquals(1, callCount);
  202. }
  203. function testDoesntFireWhenUserForgetsRequiredModifier() {
  204. listener.$replay(); // no events expected
  205. handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL);
  206. fire(KeyCodes.G);
  207. listener.$verify();
  208. }
  209. function testDoesntFireIfTooManyModifiersPressed() {
  210. listener.$replay(); // no events expected
  211. handler.registerShortcut('lettergeectrl', KeyCodes.G, Modifiers.CTRL);
  212. fire(KeyCodes.G, {ctrlKey: true, metaKey: true});
  213. listener.$verify();
  214. }
  215. function testDoesntFireIfAnyRequiredModifierForgotten() {
  216. listener.$replay(); // no events expected
  217. handler.registerShortcut(
  218. 'lettergeectrlaltshift', KeyCodes.G,
  219. Modifiers.CTRL | Modifiers.ALT | Modifiers.SHIFT);
  220. fire(KeyCodes.G, {altKey: true, shiftKey: true});
  221. listener.$verify();
  222. }
  223. function testAllowsMultiKeySequenceSpecifiedAsArray() {
  224. listener.shortcutFired('quitemacs');
  225. listener.$replay();
  226. handler.registerShortcut(
  227. 'quitemacs', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]);
  228. assertFalse(fire(KeyCodes.X, {ctrlKey: true}));
  229. fire(KeyCodes.C);
  230. listener.$verify();
  231. }
  232. function testAllowsMultiKeySequenceSpecifiedAsArguments() {
  233. listener.shortcutFired('quitvi');
  234. listener.$replay();
  235. handler.registerShortcut(
  236. 'quitvi', KeyCodes.SEMICOLON, Modifiers.SHIFT, KeyCodes.Q, Modifiers.NONE,
  237. KeyCodes.NUM_ONE, Modifiers.SHIFT);
  238. var shiftProperties = {shiftKey: true};
  239. assertFalse(fire(KeyCodes.SEMICOLON, shiftProperties));
  240. assertFalse(fire(KeyCodes.Q));
  241. fire(KeyCodes.NUM_ONE, shiftProperties);
  242. listener.$verify();
  243. }
  244. function testMultiKeyEventIsNotFiredIfUserIsTooSlow() {
  245. listener.$replay(); // no events expected
  246. handler.registerShortcut(
  247. 'quitemacs', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]);
  248. fire(KeyCodes.X, {ctrlKey: true});
  249. // Wait 3 seconds before hitting C. Although the actual limit is 1500
  250. // at time of writing, it's best not to over-specify functionality.
  251. mockClock.tick(3000);
  252. fire(KeyCodes.C);
  253. listener.$verify();
  254. }
  255. function testAllowsMultipleAHandlers() {
  256. listener.shortcutFired('quitvi');
  257. listener.shortcutFired('letterex');
  258. listener.shortcutFired('quitemacs');
  259. listener.$replay();
  260. // register 3 handlers in 3 diferent ways
  261. handler.registerShortcut(
  262. 'quitvi', KeyCodes.SEMICOLON, Modifiers.SHIFT, KeyCodes.Q, Modifiers.NONE,
  263. KeyCodes.NUM_ONE, Modifiers.SHIFT);
  264. handler.registerShortcut(
  265. 'quitemacs', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]);
  266. handler.registerShortcut('letterex', 'x');
  267. // quit vi
  268. var shiftProperties = {shiftKey: true};
  269. fire(KeyCodes.SEMICOLON, shiftProperties);
  270. fire(KeyCodes.Q);
  271. fire(KeyCodes.NUM_ONE, shiftProperties);
  272. // then press the letter x
  273. fire(KeyCodes.X);
  274. // then quit emacs
  275. fire(KeyCodes.X, {ctrlKey: true});
  276. fire(KeyCodes.C);
  277. listener.$verify();
  278. }
  279. function testCanRemoveOneHandler() {
  280. listener.shortcutFired('letterex');
  281. listener.$replay();
  282. // register 2 handlers, then remove quitvi
  283. handler.registerShortcut(
  284. 'quitvi', KeyCodes.COLON, Modifiers.NONE, KeyCodes.Q, Modifiers.NONE,
  285. KeyCodes.EXCLAMATION, Modifiers.NONE);
  286. handler.registerShortcut('letterex', 'x');
  287. handler.unregisterShortcut(
  288. KeyCodes.COLON, Modifiers.NONE, KeyCodes.Q, Modifiers.NONE,
  289. KeyCodes.EXCLAMATION, Modifiers.NONE);
  290. // call the "quit VI" keycodes, even though it is removed
  291. fire(KeyCodes.COLON);
  292. fire(KeyCodes.Q);
  293. fire(KeyCodes.EXCLAMATION);
  294. // press the letter x
  295. fire(KeyCodes.X);
  296. listener.$verify();
  297. }
  298. function testCanRemoveTwoHandlers() {
  299. listener.$replay(); // no events expected
  300. handler.registerShortcut(
  301. 'quitemacs', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]);
  302. handler.registerShortcut('letterex', 'x');
  303. handler.unregisterShortcut([KeyCodes.X, Modifiers.CTRL, KeyCodes.C]);
  304. handler.unregisterShortcut('x');
  305. fire(KeyCodes.X, {ctrlKey: true});
  306. fire(KeyCodes.C);
  307. fire(KeyCodes.X);
  308. listener.$verify();
  309. }
  310. function testIsShortcutRegistered_single() {
  311. assertFalse(handler.isShortcutRegistered('x'));
  312. handler.registerShortcut('letterex', 'x');
  313. assertTrue(handler.isShortcutRegistered('x'));
  314. handler.unregisterShortcut('x');
  315. assertFalse(handler.isShortcutRegistered('x'));
  316. }
  317. function testIsShortcutRegistered_multi() {
  318. assertFalse(handler.isShortcutRegistered('a'));
  319. assertFalse(handler.isShortcutRegistered('a b'));
  320. assertFalse(handler.isShortcutRegistered('a b c'));
  321. handler.registerShortcut('ab', 'a b');
  322. assertFalse(handler.isShortcutRegistered('a'));
  323. assertTrue(handler.isShortcutRegistered('a b'));
  324. assertFalse(handler.isShortcutRegistered('a b c'));
  325. handler.unregisterShortcut('a b');
  326. assertFalse(handler.isShortcutRegistered('a'));
  327. assertFalse(handler.isShortcutRegistered('a b'));
  328. assertFalse(handler.isShortcutRegistered('a b c'));
  329. }
  330. function testUnregister_subsequence() {
  331. // Unregistering a partial sequence should not orphan shortcuts further in the
  332. // sequence.
  333. handler.registerShortcut('abc', 'a b c');
  334. handler.unregisterShortcut('a b');
  335. assertTrue(handler.isShortcutRegistered('a b c'));
  336. }
  337. function testUnregister_supersequence() {
  338. // Unregistering a sequence that extends beyond a registered sequence should
  339. // do nothing.
  340. handler.registerShortcut('ab', 'a b');
  341. handler.unregisterShortcut('a b c');
  342. assertTrue(handler.isShortcutRegistered('a b'));
  343. }
  344. function testUnregister_partialMatchSequence() {
  345. // Unregistering a sequence that partially matches a registered sequence
  346. // should do nothing.
  347. handler.registerShortcut('abc', 'a b c');
  348. handler.unregisterShortcut('a b x');
  349. assertTrue(handler.isShortcutRegistered('a b c'));
  350. }
  351. function testUnregister_deadBranch() {
  352. // Unregistering a sequence should prune any dead branches in the tree.
  353. handler.registerShortcut('abc', 'a b c');
  354. handler.unregisterShortcut('a b c');
  355. // Default is not should not be prevented in the A key stroke because the A
  356. // branch has been removed from the tree.
  357. assertTrue(fire(KeyCodes.A));
  358. }
  359. /**
  360. * Registers a slew of keyboard shortcuts to test each primary category
  361. * of shortcuts.
  362. */
  363. function registerEnterSpaceXF1AltY() {
  364. // Enter and space are specially handled keys.
  365. handler.registerShortcut('enter', KeyCodes.ENTER);
  366. handler.registerShortcut('space', KeyCodes.SPACE);
  367. // 'x' should be treated as text in many contexts
  368. handler.registerShortcut('x', 'x');
  369. // F1 is a global shortcut.
  370. handler.registerShortcut('global', KeyCodes.F1);
  371. // Alt-Y has modifiers, which pass through most form elements.
  372. handler.registerShortcut('withAlt', 'alt+y');
  373. }
  374. /**
  375. * Fires enter, space, X, F1, and Alt-Y keys on a widget.
  376. * @param {Element} target The element on which to fire the events.
  377. */
  378. function fireEnterSpaceXF1AltY(target) {
  379. fire(KeyCodes.ENTER, undefined, target);
  380. fire(KeyCodes.SPACE, undefined, target);
  381. fire(KeyCodes.X, undefined, target);
  382. fire(KeyCodes.F1, undefined, target);
  383. fire(KeyCodes.Y, {altKey: true}, target);
  384. }
  385. function testIgnoreNonGlobalShortcutsInSelect() {
  386. var targetSelect = goog.dom.getElement('targetSelect');
  387. listener.shortcutFired('global');
  388. listener.shortcutFired('withAlt');
  389. listener.$replay();
  390. registerEnterSpaceXF1AltY();
  391. fireEnterSpaceXF1AltY(goog.dom.getElement('targetSelect'));
  392. listener.$verify();
  393. }
  394. function testIgnoreNonGlobalShortcutsInTextArea() {
  395. listener.shortcutFired('global');
  396. listener.shortcutFired('withAlt');
  397. listener.$replay();
  398. registerEnterSpaceXF1AltY();
  399. fireEnterSpaceXF1AltY(goog.dom.getElement('targetTextArea'));
  400. listener.$verify();
  401. }
  402. /**
  403. * Checks that the shortcuts are fired on each target.
  404. * @param {Array<string>} shortcuts A list of shortcut identifiers.
  405. * @param {Array<string>} targets A list of element IDs.
  406. * @param {function(Element)} fireEvents Function that fires events.
  407. */
  408. function expectShortcutsOnTargets(shortcuts, targets, fireEvents) {
  409. for (var i = 0, ii = targets.length; i < ii; i++) {
  410. for (var j = 0, jj = shortcuts.length; j < jj; j++) {
  411. listener.shortcutFired(shortcuts[j]);
  412. }
  413. listener.$replay();
  414. fireEvents(goog.dom.getElement(targets[i]));
  415. listener.$verify();
  416. listener.$reset();
  417. }
  418. }
  419. function testIgnoreShortcutsExceptEnterInTextInputFields() {
  420. var targets = [
  421. 'targetColor', 'targetDate', 'targetDateTime', 'targetDateTimeLocal',
  422. 'targetEmail', 'targetMonth', 'targetNumber', 'targetPassword',
  423. 'targetSearch', 'targetTel', 'targetText', 'targetTime', 'targetUrl',
  424. 'targetWeek'
  425. ];
  426. registerEnterSpaceXF1AltY();
  427. expectShortcutsOnTargets(
  428. ['enter', 'global', 'withAlt'], targets, fireEnterSpaceXF1AltY);
  429. }
  430. function testIgnoreSpaceInCheckBoxAndButton() {
  431. registerEnterSpaceXF1AltY();
  432. expectShortcutsOnTargets(
  433. ['enter', 'x', 'global', 'withAlt'], ['targetCheckBox', 'targetButton'],
  434. fireEnterSpaceXF1AltY);
  435. }
  436. function testIgnoreNonGlobalShortcutsInContentEditable() {
  437. // Don't set design mode in later IE as javascripts don't run when in
  438. // that mode.
  439. var setDesignMode =
  440. !goog.userAgent.IE || !goog.userAgent.isVersionOrHigher('9');
  441. try {
  442. if (setDesignMode) {
  443. document.designMode = 'on';
  444. }
  445. targetDiv.contentEditable = 'true';
  446. // Expect only global shortcuts.
  447. listener.shortcutFired('global');
  448. listener.$replay();
  449. registerEnterSpaceXF1AltY();
  450. fireEnterSpaceXF1AltY(targetDiv);
  451. listener.$verify();
  452. } finally {
  453. if (setDesignMode) {
  454. document.designMode = 'off';
  455. }
  456. targetDiv.contentEditable = 'false';
  457. }
  458. }
  459. function testSetAllShortcutsAreGlobal() {
  460. handler.setAllShortcutsAreGlobal(true);
  461. registerEnterSpaceXF1AltY();
  462. expectShortcutsOnTargets(
  463. ['enter', 'space', 'x', 'global', 'withAlt'], ['targetTextArea'],
  464. fireEnterSpaceXF1AltY);
  465. }
  466. function testSetModifierShortcutsAreGlobalFalse() {
  467. handler.setModifierShortcutsAreGlobal(false);
  468. registerEnterSpaceXF1AltY();
  469. expectShortcutsOnTargets(
  470. ['global'], ['targetTextArea'], fireEnterSpaceXF1AltY);
  471. }
  472. function testAltGraphKeyOnUSLayout() {
  473. // Windows does not assign printable characters to any ctrl+alt keys of
  474. // the US layout. This test verifies we fire shortcut events when typing
  475. // ctrl+alt keys on the US layout.
  476. listener.shortcutFired('letterOne');
  477. listener.shortcutFired('letterTwo');
  478. listener.shortcutFired('letterThree');
  479. listener.shortcutFired('letterFour');
  480. listener.shortcutFired('letterFive');
  481. if (goog.userAgent.WINDOWS) {
  482. listener.$replay();
  483. handler.registerShortcut('letterOne', 'ctrl+alt+1');
  484. handler.registerShortcut('letterTwo', 'ctrl+alt+2');
  485. handler.registerShortcut('letterThree', 'ctrl+alt+3');
  486. handler.registerShortcut('letterFour', 'ctrl+alt+4');
  487. handler.registerShortcut('letterFive', 'ctrl+alt+5');
  488. // Send key events on the English (United States) layout.
  489. fireAltGraphKey(KeyCodes.ONE, 0, {ctrlKey: true, altKey: true});
  490. fireAltGraphKey(KeyCodes.TWO, 0, {ctrlKey: true, altKey: true});
  491. fireAltGraphKey(KeyCodes.THREE, 0, {ctrlKey: true, altKey: true});
  492. fireAltGraphKey(KeyCodes.FOUR, 0, {ctrlKey: true, altKey: true});
  493. fireAltGraphKey(KeyCodes.FIVE, 0, {ctrlKey: true, altKey: true});
  494. listener.$verify();
  495. }
  496. }
  497. function testAltGraphKeyOnFrenchLayout() {
  498. // Windows assigns printable characters to ctrl+alt+[2-5] keys of the
  499. // French layout. This test verifies we fire shortcut events only when
  500. // we type ctrl+alt+1 keys on the French layout.
  501. listener.shortcutFired('letterOne');
  502. if (goog.userAgent.WINDOWS) {
  503. listener.$replay();
  504. handler.registerShortcut('letterOne', 'ctrl+alt+1');
  505. handler.registerShortcut('letterTwo', 'ctrl+alt+2');
  506. handler.registerShortcut('letterThree', 'ctrl+alt+3');
  507. handler.registerShortcut('letterFour', 'ctrl+alt+4');
  508. handler.registerShortcut('letterFive', 'ctrl+alt+5');
  509. // Send key events on the French (France) layout.
  510. fireAltGraphKey(KeyCodes.ONE, 0, {ctrlKey: true, altKey: true});
  511. fireAltGraphKey(KeyCodes.TWO, 0x0303, {ctrlKey: true, altKey: true});
  512. fireAltGraphKey(KeyCodes.THREE, 0x0023, {ctrlKey: true, altKey: true});
  513. fireAltGraphKey(KeyCodes.FOUR, 0x007b, {ctrlKey: true, altKey: true});
  514. fireAltGraphKey(KeyCodes.FIVE, 0x205b, {ctrlKey: true, altKey: true});
  515. listener.$verify();
  516. }
  517. }
  518. function testAltGraphKeyOnSpanishLayout() {
  519. // Windows assigns printable characters to ctrl+alt+[1-5] keys of the
  520. // Spanish layout. This test verifies we do not fire shortcut events at
  521. // all when typing ctrl+alt+[1-5] keys on the Spanish layout.
  522. if (goog.userAgent.WINDOWS) {
  523. listener.$replay();
  524. handler.registerShortcut('letterOne', 'ctrl+alt+1');
  525. handler.registerShortcut('letterTwo', 'ctrl+alt+2');
  526. handler.registerShortcut('letterThree', 'ctrl+alt+3');
  527. handler.registerShortcut('letterFour', 'ctrl+alt+4');
  528. handler.registerShortcut('letterFive', 'ctrl+alt+5');
  529. // Send key events on the Spanish (Spain) layout.
  530. fireAltGraphKey(KeyCodes.ONE, 0x007c, {ctrlKey: true, altKey: true});
  531. fireAltGraphKey(KeyCodes.TWO, 0x0040, {ctrlKey: true, altKey: true});
  532. fireAltGraphKey(KeyCodes.THREE, 0x0023, {ctrlKey: true, altKey: true});
  533. fireAltGraphKey(KeyCodes.FOUR, 0x0303, {ctrlKey: true, altKey: true});
  534. fireAltGraphKey(KeyCodes.FIVE, 0x20ac, {ctrlKey: true, altKey: true});
  535. listener.$verify();
  536. }
  537. }
  538. function testAltGraphKeyOnPolishLayout_withShift() {
  539. // Windows assigns printable characters to ctrl+alt+shift+A key in polish
  540. // layout. This test verifies that we do not fire shortcut events for A, but
  541. // does fire for Q which does not have a printable character.
  542. if (goog.userAgent.WINDOWS) {
  543. listener.shortcutFired('letterQ');
  544. listener.$replay();
  545. handler.registerShortcut('letterA', 'ctrl+alt+shift+A');
  546. handler.registerShortcut('letterQ', 'ctrl+alt+shift+Q');
  547. // Send key events on the Polish (Programmer) layout.
  548. assertTrue(fireAltGraphKey(
  549. KeyCodes.A, 0x0104, {ctrlKey: true, altKey: true, shiftKey: true}));
  550. assertFalse(fireAltGraphKey(
  551. KeyCodes.Q, 0, {ctrlKey: true, altKey: true, shiftKey: true}));
  552. listener.$verify();
  553. }
  554. }
  555. function testNumpadKeyShortcuts() {
  556. var testCases = [
  557. ['letterNumpad0', 'num-0', KeyCodes.NUM_ZERO],
  558. ['letterNumpad1', 'num-1', KeyCodes.NUM_ONE],
  559. ['letterNumpad2', 'num-2', KeyCodes.NUM_TWO],
  560. ['letterNumpad3', 'num-3', KeyCodes.NUM_THREE],
  561. ['letterNumpad4', 'num-4', KeyCodes.NUM_FOUR],
  562. ['letterNumpad5', 'num-5', KeyCodes.NUM_FIVE],
  563. ['letterNumpad6', 'num-6', KeyCodes.NUM_SIX],
  564. ['letterNumpad7', 'num-7', KeyCodes.NUM_SEVEN],
  565. ['letterNumpad8', 'num-8', KeyCodes.NUM_EIGHT],
  566. ['letterNumpad9', 'num-9', KeyCodes.NUM_NINE],
  567. ['letterNumpadMultiply', 'num-multiply', KeyCodes.NUM_MULTIPLY],
  568. ['letterNumpadPlus', 'num-plus', KeyCodes.NUM_PLUS],
  569. ['letterNumpadMinus', 'num-minus', KeyCodes.NUM_MINUS],
  570. ['letterNumpadPERIOD', 'num-period', KeyCodes.NUM_PERIOD],
  571. ['letterNumpadDIVISION', 'num-division', KeyCodes.NUM_DIVISION]
  572. ];
  573. for (var i = 0; i < testCases.length; ++i) {
  574. listener.shortcutFired(testCases[i][0]);
  575. }
  576. listener.$replay();
  577. // Register shortcuts for numpad keys and send numpad-key events.
  578. for (var i = 0; i < testCases.length; ++i) {
  579. handler.registerShortcut(testCases[i][0], testCases[i][1]);
  580. fire(testCases[i][2]);
  581. }
  582. listener.$verify();
  583. }
  584. function testGeckoShortcuts() {
  585. listener.shortcutFired('1');
  586. listener.$replay();
  587. handler.registerShortcut('1', 'semicolon');
  588. if (goog.userAgent.GECKO) {
  589. fire(goog.events.KeyCodes.FF_SEMICOLON);
  590. } else {
  591. fire(goog.events.KeyCodes.SEMICOLON);
  592. }
  593. listener.$verify();
  594. }
  595. function testWindows_multiKeyShortcuts() {
  596. if (goog.userAgent.WINDOWS) {
  597. listener.shortcutFired('1');
  598. listener.$replay();
  599. handler.registerShortcut('1', 'ctrl+alt+n c');
  600. fire(KeyCodes.N, {ctrlKey: true, altKey: true});
  601. fire(KeyCodes.C);
  602. listener.$verify();
  603. }
  604. }
  605. function testWindows_multikeyShortcuts_polishKey() {
  606. if (goog.userAgent.WINDOWS) {
  607. listener.$replay();
  608. handler.registerShortcut('announceCursorLocation', 'ctrl+alt+a l');
  609. // If a Polish key is a subsection of a keyboard shortcut, then
  610. // the key should still be written.
  611. assertTrue(fireAltGraphKey(
  612. KeyCodes.A, 0x0104, {ctrlKey: true, altKey: true}));
  613. listener.$verify();
  614. }
  615. }
  616. function testRegisterShortcut_modifierOnly() {
  617. assertThrows(
  618. 'Registering a shortcut with just modifiers should fail.',
  619. goog.bind(handler.registerShortcut, handler, 'name', 'Shift'));
  620. }
  621. function testParseStringShortcut_unknownKey() {
  622. assertThrows(
  623. 'Unknown keys should fail.',
  624. goog.bind(
  625. goog.ui.KeyboardShortcutHandler.parseStringShortcut, null,
  626. 'NotAKey'));
  627. }
  628. // Regression test for failure to reset keyCode between strokes.
  629. function testParseStringShortcut_resetKeyCode() {
  630. var strokes = goog.ui.KeyboardShortcutHandler.parseStringShortcut('A Shift');
  631. assertNull('The second stroke only has a modifier key.', strokes[1].keyCode);
  632. }