/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; var isOSX = Services.appinfo.OS === "Darwin"; add_task(async function () { const shortcuts = new KeyShortcuts({ window, }); await testSimple(shortcuts); await testNonLetterCharacter(shortcuts); await testPlusCharacter(shortcuts); await testFunctionKey(shortcuts); await testMixup(shortcuts); await testLooseDigits(shortcuts); await testExactModifiers(shortcuts); await testLooseShiftModifier(shortcuts); await testStrictLetterShiftModifier(shortcuts); await testAltModifier(shortcuts); await testCommandOrControlModifier(shortcuts); await testCtrlModifier(shortcuts); await testInvalidShortcutString(shortcuts); await testNullShortcut(shortcuts); await testCmdShiftShortcut(shortcuts); await testTabCharacterShortcut(shortcuts); shortcuts.destroy(); await testTarget(); }); // Test helper to listen to the next key press for a given key, // returning a promise to help using Tasks. function once(shortcuts, key, listener) { let called = false; return new Promise(done => { const onShortcut = event => { shortcuts.off(key, onShortcut); ok(!called, "once listener called only once (i.e. off() works)"); called = true; listener(event); done(); }; shortcuts.on(key, onShortcut); }); } async function testSimple(shortcuts) { info("Test simple key shortcuts"); const onKey = once(shortcuts, "0", event => { is(event.key, "0"); // Display another key press to ensure that once() correctly stop listening EventUtils.synthesizeKey("0", {}, window); }); EventUtils.synthesizeKey("0", {}, window); await onKey; } async function testNonLetterCharacter(shortcuts) { info("Test non-naive character key shortcuts"); const onKey = once(shortcuts, "[", event => { is(event.key, "["); }); EventUtils.synthesizeKey("[", {}, window); await onKey; } async function testFunctionKey(shortcuts) { info("Test function key shortcuts"); const onKey = once(shortcuts, "F12", event => { is(event.key, "F12"); }); EventUtils.synthesizeKey("F12", { keyCode: 123 }, window); await onKey; } // Plus is special. It's keycode is the one for "=". That's because it requires // shift to be pressed and is behind "=" key. So it should be considered as a // character key async function testPlusCharacter(shortcuts) { info("Test 'Plus' key shortcuts"); const onKey = once(shortcuts, "Plus", event => { is(event.key, "+"); }); EventUtils.synthesizeKey("+", { keyCode: 61, shiftKey: true }, window); await onKey; } // Test they listeners are not mixed up between shortcuts async function testMixup(shortcuts) { info("Test possible listener mixup"); let hitFirst = false, hitSecond = false; const onFirstKey = once(shortcuts, "0", event => { is(event.key, "0"); hitFirst = true; }); const onSecondKey = once(shortcuts, "Alt+A", event => { is(event.key, "a"); ok(event.altKey); hitSecond = true; }); // Dispatch the first shortcut and expect only this one to be notified ok(!hitFirst, "First shortcut isn't notified before firing the key event"); EventUtils.synthesizeKey("0", {}, window); await onFirstKey; ok(hitFirst, "Got the first shortcut notified"); ok(!hitSecond, "No mixup, second shortcut is still not notified (1/2)"); // Wait an extra time, just to be sure this isn't racy await new Promise(done => { window.setTimeout(done, 0); }); ok(!hitSecond, "No mixup, second shortcut is still not notified (2/2)"); // Finally dispatch the second shortcut EventUtils.synthesizeKey("a", { altKey: true }, window); await onSecondKey; ok(hitSecond, "Got the second shortcut notified once it is actually fired"); } // On azerty keyboard, digits are only available by pressing Shift/Capslock, // but we accept them even if we omit doing that. async function testLooseDigits(shortcuts) { info("Test Loose digits"); let onKey = once(shortcuts, "0", event => { is(event.key, "à"); ok(!event.altKey); ok(!event.ctrlKey); ok(!event.metaKey); ok(!event.shiftKey); }); // Simulate a press on the "0" key, without shift pressed on a french // keyboard EventUtils.synthesizeKey("à", { keyCode: 48 }, window); await onKey; onKey = once(shortcuts, "0", event => { is(event.key, "0"); ok(!event.altKey); ok(!event.ctrlKey); ok(!event.metaKey); ok(event.shiftKey); }); // Simulate the same press with shift pressed EventUtils.synthesizeKey("0", { keyCode: 48, shiftKey: true }, window); await onKey; } // Test that shortcuts is notified only when the modifiers match exactly async function testExactModifiers(shortcuts) { info("Test exact modifiers match"); let hit = false; const onKey = once(shortcuts, "Alt+A", event => { is(event.key, "a"); ok(event.altKey); ok(!event.ctrlKey); ok(!event.metaKey); ok(!event.shiftKey); hit = true; }); // Dispatch with unexpected set of modifiers ok(!hit, "Shortcut isn't notified before firing the key event"); EventUtils.synthesizeKey( "a", { accelKey: true, altKey: true, shiftKey: true }, window ); EventUtils.synthesizeKey( "a", { accelKey: true, altKey: false, shiftKey: false }, window ); EventUtils.synthesizeKey( "a", { accelKey: false, altKey: false, shiftKey: true }, window ); EventUtils.synthesizeKey( "a", { accelKey: false, altKey: false, shiftKey: false }, window ); // Wait an extra time to let a chance to call the listener await new Promise(done => { window.setTimeout(done, 0); }); ok(!hit, "Listener isn't called when modifiers aren't exactly matching"); // Dispatch the expected modifiers EventUtils.synthesizeKey( "a", { accelKey: false, altKey: true, shiftKey: false }, window ); await onKey; ok(hit, "Got shortcut notified once it is actually fired"); } // Some keys are only accessible via shift and listener should also be called // even if the key didn't explicitely requested Shift modifier. // For example, `%` on french keyboards is only accessible via Shift. // Same thing for `@` on US keybords. async function testLooseShiftModifier(shortcuts) { info("Test Loose shift modifier"); let onKey = once(shortcuts, "%", event => { is(event.key, "%"); ok(!event.altKey); ok(!event.ctrlKey); ok(!event.metaKey); ok(event.shiftKey); }); EventUtils.synthesizeKey( "%", { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true }, window ); await onKey; onKey = once(shortcuts, "@", event => { is(event.key, "@"); ok(!event.altKey); ok(!event.ctrlKey); ok(!event.metaKey); ok(event.shiftKey); }); EventUtils.synthesizeKey( "@", { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true }, window ); await onKey; } // But Shift modifier is strict on all letter characters (a to Z) async function testStrictLetterShiftModifier(shortcuts) { info("Test strict shift modifier on letters"); let hitFirst = false; const onKey = once(shortcuts, "a", event => { is(event.key, "a"); ok(!event.altKey); ok(!event.ctrlKey); ok(!event.metaKey); ok(!event.shiftKey); hitFirst = true; }); const onShiftKey = once(shortcuts, "Shift+a", event => { is(event.key, "a"); ok(!event.altKey); ok(!event.ctrlKey); ok(!event.metaKey); ok(event.shiftKey); }); EventUtils.synthesizeKey("a", { shiftKey: true }, window); await onShiftKey; ok(!hitFirst, "Didn't fire the explicit shift+a"); EventUtils.synthesizeKey("a", { shiftKey: false }, window); await onKey; } async function testAltModifier(shortcuts) { info("Test Alt modifier"); const onKey = once(shortcuts, "Alt+F1", event => { is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); ok(event.altKey); ok(!event.ctrlKey); ok(!event.metaKey); ok(!event.shiftKey); }); EventUtils.synthesizeKey("VK_F1", { altKey: true }, window); await onKey; } async function testCommandOrControlModifier(shortcuts) { info("Test CommandOrControl modifier"); const onKey = once(shortcuts, "CommandOrControl+F1", event => { is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); ok(!event.altKey); if (isOSX) { ok(!event.ctrlKey); ok(event.metaKey); } else { ok(event.ctrlKey); ok(!event.metaKey); } ok(!event.shiftKey); }); const onKeyAlias = once(shortcuts, "CmdOrCtrl+F1", event => { is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); ok(!event.altKey); if (isOSX) { ok(!event.ctrlKey); ok(event.metaKey); } else { ok(event.ctrlKey); ok(!event.metaKey); } ok(!event.shiftKey); }); if (isOSX) { EventUtils.synthesizeKey("VK_F1", { metaKey: true }, window); } else { EventUtils.synthesizeKey("VK_F1", { ctrlKey: true }, window); } await onKey; await onKeyAlias; } async function testCtrlModifier(shortcuts) { info("Test Ctrl modifier"); const onKey = once(shortcuts, "Ctrl+F1", event => { is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); ok(!event.altKey); ok(event.ctrlKey); ok(!event.metaKey); ok(!event.shiftKey); }); const onKeyAlias = once(shortcuts, "Control+F1", event => { is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); ok(!event.altKey); ok(event.ctrlKey); ok(!event.metaKey); ok(!event.shiftKey); }); EventUtils.synthesizeKey("VK_F1", { ctrlKey: true }, window); await onKey; await onKeyAlias; } async function testCmdShiftShortcut(shortcuts) { if (!isOSX) { // This test is OSX only (Bug 1300458). return; } const onCmdKey = once(shortcuts, "CmdOrCtrl+[", event => { is(event.key, "["); ok(!event.altKey); ok(!event.ctrlKey); ok(event.metaKey); ok(!event.shiftKey); }); const onCmdShiftKey = once(shortcuts, "CmdOrCtrl+Shift+[", event => { is(event.key, "["); ok(!event.altKey); ok(!event.ctrlKey); ok(event.metaKey); ok(event.shiftKey); }); EventUtils.synthesizeKey("[", { metaKey: true, shiftKey: true }, window); EventUtils.synthesizeKey("[", { metaKey: true }, window); await onCmdKey; await onCmdShiftKey; } async function testTarget() { info("Test KeyShortcuts with target argument"); const target = document.createElementNS( "http://www.w3.org/1999/xhtml", "input" ); document.documentElement.appendChild(target); target.focus(); const shortcuts = new KeyShortcuts({ window, target, }); const onKey = once(shortcuts, "0", event => { is(event.key, "0"); is(event.target, target); }); EventUtils.synthesizeKey("0", {}, window); await onKey; target.remove(); shortcuts.destroy(); } function testInvalidShortcutString(shortcuts) { info("Test wrong shortcut string"); const shortcut = KeyShortcuts.parseElectronKey(window, "Cmmd+F"); ok( !shortcut, "Passing a invalid shortcut string should return a null object" ); shortcuts.on("Cmmd+F", function () {}); ok(true, "on() shouldn't throw when passing invalid shortcut string"); } // Can happen on localized builds where the value of the localized string is // empty, eg `toolbox.elementPicker.key=`. See Bug 1569572. function testNullShortcut(shortcuts) { info("Test null shortcut"); const shortcut = KeyShortcuts.parseElectronKey(window, null); ok(!shortcut, "Passing a null object should return a null object"); const stringified = KeyShortcuts.stringify(shortcut); is(stringified, "", "A null object should be stringified as an empty string"); shortcuts.on(null, function () {}); ok(true, "on() shouldn't throw when passing a null object"); } /** * Shift+Alt+I generates ^ key (`event.key`) on OSX and KeyShortcuts module * must ensure that this doesn't interfere with shortcuts CmdOrCtrl+Alt+Shift+I * for opening the Browser Toolbox and CmdOrCtrl+Alt+I for toggling the Toolbox. */ async function testTabCharacterShortcut(shortcuts) { if (!isOSX) { return; } info("Test tab character shortcut"); once(shortcuts, "CmdOrCtrl+Alt+I", event => { ok(false, "This handler must not be executed"); }); const onKey = once(shortcuts, "CmdOrCtrl+Alt+Shift+I", event => { info("Test for CmdOrCtrl+Alt+Shift+I"); is(event.key, "^"); is(event.keyCode, 73); }); // Simulate `CmdOrCtrl+Alt+Shift+I` shortcut. Note that EventUtils doesn't // generate `^` like real keyboard, so we need to pass it explicitly // and use proper keyCode for `I` character. EventUtils.synthesizeKey( "^", { code: "KeyI", key: "^", keyCode: 73, shiftKey: true, altKey: true, metaKey: true, }, window ); await onKey; }