// 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<string>} shortcuts A list of shortcut identifiers.
 * @param {Array<string>} 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('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();
  }
}

function testWindows_multikeyShortcuts_repeatedKeyDoesntInterfere() {
  if (goog.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();
  }
}

function testWindows_multikeyShortcuts_polishKey() {
  if (goog.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();
  }
}

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);
}

function 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();
}
