browser_key_shortcuts.js (12881B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 var isOSX = Services.appinfo.OS === "Darwin"; 7 8 add_task(async function () { 9 const shortcuts = new KeyShortcuts({ 10 window, 11 }); 12 13 await testSimple(shortcuts); 14 await testNonLetterCharacter(shortcuts); 15 await testPlusCharacter(shortcuts); 16 await testFunctionKey(shortcuts); 17 await testMixup(shortcuts); 18 await testLooseDigits(shortcuts); 19 await testExactModifiers(shortcuts); 20 await testLooseShiftModifier(shortcuts); 21 await testStrictLetterShiftModifier(shortcuts); 22 await testAltModifier(shortcuts); 23 await testCommandOrControlModifier(shortcuts); 24 await testCtrlModifier(shortcuts); 25 await testInvalidShortcutString(shortcuts); 26 await testNullShortcut(shortcuts); 27 await testCmdShiftShortcut(shortcuts); 28 await testTabCharacterShortcut(shortcuts); 29 shortcuts.destroy(); 30 31 await testTarget(); 32 }); 33 34 // Test helper to listen to the next key press for a given key, 35 // returning a promise to help using Tasks. 36 function once(shortcuts, key, listener) { 37 let called = false; 38 return new Promise(done => { 39 const onShortcut = event => { 40 shortcuts.off(key, onShortcut); 41 ok(!called, "once listener called only once (i.e. off() works)"); 42 called = true; 43 listener(event); 44 done(); 45 }; 46 shortcuts.on(key, onShortcut); 47 }); 48 } 49 50 async function testSimple(shortcuts) { 51 info("Test simple key shortcuts"); 52 53 const onKey = once(shortcuts, "0", event => { 54 is(event.key, "0"); 55 56 // Display another key press to ensure that once() correctly stop listening 57 EventUtils.synthesizeKey("0", {}, window); 58 }); 59 60 EventUtils.synthesizeKey("0", {}, window); 61 await onKey; 62 } 63 64 async function testNonLetterCharacter(shortcuts) { 65 info("Test non-naive character key shortcuts"); 66 67 const onKey = once(shortcuts, "[", event => { 68 is(event.key, "["); 69 }); 70 71 EventUtils.synthesizeKey("[", {}, window); 72 await onKey; 73 } 74 75 async function testFunctionKey(shortcuts) { 76 info("Test function key shortcuts"); 77 78 const onKey = once(shortcuts, "F12", event => { 79 is(event.key, "F12"); 80 }); 81 82 EventUtils.synthesizeKey("F12", { keyCode: 123 }, window); 83 await onKey; 84 } 85 86 // Plus is special. It's keycode is the one for "=". That's because it requires 87 // shift to be pressed and is behind "=" key. So it should be considered as a 88 // character key 89 async function testPlusCharacter(shortcuts) { 90 info("Test 'Plus' key shortcuts"); 91 92 const onKey = once(shortcuts, "Plus", event => { 93 is(event.key, "+"); 94 }); 95 96 EventUtils.synthesizeKey("+", { keyCode: 61, shiftKey: true }, window); 97 await onKey; 98 } 99 100 // Test they listeners are not mixed up between shortcuts 101 async function testMixup(shortcuts) { 102 info("Test possible listener mixup"); 103 104 let hitFirst = false, 105 hitSecond = false; 106 const onFirstKey = once(shortcuts, "0", event => { 107 is(event.key, "0"); 108 hitFirst = true; 109 }); 110 const onSecondKey = once(shortcuts, "Alt+A", event => { 111 is(event.key, "a"); 112 ok(event.altKey); 113 hitSecond = true; 114 }); 115 116 // Dispatch the first shortcut and expect only this one to be notified 117 ok(!hitFirst, "First shortcut isn't notified before firing the key event"); 118 EventUtils.synthesizeKey("0", {}, window); 119 await onFirstKey; 120 ok(hitFirst, "Got the first shortcut notified"); 121 ok(!hitSecond, "No mixup, second shortcut is still not notified (1/2)"); 122 123 // Wait an extra time, just to be sure this isn't racy 124 await new Promise(done => { 125 window.setTimeout(done, 0); 126 }); 127 ok(!hitSecond, "No mixup, second shortcut is still not notified (2/2)"); 128 129 // Finally dispatch the second shortcut 130 EventUtils.synthesizeKey("a", { altKey: true }, window); 131 await onSecondKey; 132 ok(hitSecond, "Got the second shortcut notified once it is actually fired"); 133 } 134 135 // On azerty keyboard, digits are only available by pressing Shift/Capslock, 136 // but we accept them even if we omit doing that. 137 async function testLooseDigits(shortcuts) { 138 info("Test Loose digits"); 139 let onKey = once(shortcuts, "0", event => { 140 is(event.key, "à"); 141 ok(!event.altKey); 142 ok(!event.ctrlKey); 143 ok(!event.metaKey); 144 ok(!event.shiftKey); 145 }); 146 // Simulate a press on the "0" key, without shift pressed on a french 147 // keyboard 148 EventUtils.synthesizeKey("à", { keyCode: 48 }, window); 149 await onKey; 150 151 onKey = once(shortcuts, "0", event => { 152 is(event.key, "0"); 153 ok(!event.altKey); 154 ok(!event.ctrlKey); 155 ok(!event.metaKey); 156 ok(event.shiftKey); 157 }); 158 // Simulate the same press with shift pressed 159 EventUtils.synthesizeKey("0", { keyCode: 48, shiftKey: true }, window); 160 await onKey; 161 } 162 163 // Test that shortcuts is notified only when the modifiers match exactly 164 async function testExactModifiers(shortcuts) { 165 info("Test exact modifiers match"); 166 167 let hit = false; 168 const onKey = once(shortcuts, "Alt+A", event => { 169 is(event.key, "a"); 170 ok(event.altKey); 171 ok(!event.ctrlKey); 172 ok(!event.metaKey); 173 ok(!event.shiftKey); 174 hit = true; 175 }); 176 177 // Dispatch with unexpected set of modifiers 178 ok(!hit, "Shortcut isn't notified before firing the key event"); 179 EventUtils.synthesizeKey( 180 "a", 181 { accelKey: true, altKey: true, shiftKey: true }, 182 window 183 ); 184 EventUtils.synthesizeKey( 185 "a", 186 { accelKey: true, altKey: false, shiftKey: false }, 187 window 188 ); 189 EventUtils.synthesizeKey( 190 "a", 191 { accelKey: false, altKey: false, shiftKey: true }, 192 window 193 ); 194 EventUtils.synthesizeKey( 195 "a", 196 { accelKey: false, altKey: false, shiftKey: false }, 197 window 198 ); 199 200 // Wait an extra time to let a chance to call the listener 201 await new Promise(done => { 202 window.setTimeout(done, 0); 203 }); 204 ok(!hit, "Listener isn't called when modifiers aren't exactly matching"); 205 206 // Dispatch the expected modifiers 207 EventUtils.synthesizeKey( 208 "a", 209 { accelKey: false, altKey: true, shiftKey: false }, 210 window 211 ); 212 await onKey; 213 ok(hit, "Got shortcut notified once it is actually fired"); 214 } 215 216 // Some keys are only accessible via shift and listener should also be called 217 // even if the key didn't explicitely requested Shift modifier. 218 // For example, `%` on french keyboards is only accessible via Shift. 219 // Same thing for `@` on US keybords. 220 async function testLooseShiftModifier(shortcuts) { 221 info("Test Loose shift modifier"); 222 let onKey = once(shortcuts, "%", event => { 223 is(event.key, "%"); 224 ok(!event.altKey); 225 ok(!event.ctrlKey); 226 ok(!event.metaKey); 227 ok(event.shiftKey); 228 }); 229 EventUtils.synthesizeKey( 230 "%", 231 { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true }, 232 window 233 ); 234 await onKey; 235 236 onKey = once(shortcuts, "@", event => { 237 is(event.key, "@"); 238 ok(!event.altKey); 239 ok(!event.ctrlKey); 240 ok(!event.metaKey); 241 ok(event.shiftKey); 242 }); 243 EventUtils.synthesizeKey( 244 "@", 245 { accelKey: false, altKey: false, ctrlKey: false, shiftKey: true }, 246 window 247 ); 248 await onKey; 249 } 250 251 // But Shift modifier is strict on all letter characters (a to Z) 252 async function testStrictLetterShiftModifier(shortcuts) { 253 info("Test strict shift modifier on letters"); 254 let hitFirst = false; 255 const onKey = once(shortcuts, "a", event => { 256 is(event.key, "a"); 257 ok(!event.altKey); 258 ok(!event.ctrlKey); 259 ok(!event.metaKey); 260 ok(!event.shiftKey); 261 hitFirst = true; 262 }); 263 const onShiftKey = once(shortcuts, "Shift+a", event => { 264 is(event.key, "a"); 265 ok(!event.altKey); 266 ok(!event.ctrlKey); 267 ok(!event.metaKey); 268 ok(event.shiftKey); 269 }); 270 EventUtils.synthesizeKey("a", { shiftKey: true }, window); 271 await onShiftKey; 272 ok(!hitFirst, "Didn't fire the explicit shift+a"); 273 274 EventUtils.synthesizeKey("a", { shiftKey: false }, window); 275 await onKey; 276 } 277 278 async function testAltModifier(shortcuts) { 279 info("Test Alt modifier"); 280 const onKey = once(shortcuts, "Alt+F1", event => { 281 is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); 282 ok(event.altKey); 283 ok(!event.ctrlKey); 284 ok(!event.metaKey); 285 ok(!event.shiftKey); 286 }); 287 EventUtils.synthesizeKey("VK_F1", { altKey: true }, window); 288 await onKey; 289 } 290 291 async function testCommandOrControlModifier(shortcuts) { 292 info("Test CommandOrControl modifier"); 293 const onKey = once(shortcuts, "CommandOrControl+F1", event => { 294 is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); 295 ok(!event.altKey); 296 if (isOSX) { 297 ok(!event.ctrlKey); 298 ok(event.metaKey); 299 } else { 300 ok(event.ctrlKey); 301 ok(!event.metaKey); 302 } 303 ok(!event.shiftKey); 304 }); 305 const onKeyAlias = once(shortcuts, "CmdOrCtrl+F1", event => { 306 is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); 307 ok(!event.altKey); 308 if (isOSX) { 309 ok(!event.ctrlKey); 310 ok(event.metaKey); 311 } else { 312 ok(event.ctrlKey); 313 ok(!event.metaKey); 314 } 315 ok(!event.shiftKey); 316 }); 317 if (isOSX) { 318 EventUtils.synthesizeKey("VK_F1", { metaKey: true }, window); 319 } else { 320 EventUtils.synthesizeKey("VK_F1", { ctrlKey: true }, window); 321 } 322 await onKey; 323 await onKeyAlias; 324 } 325 326 async function testCtrlModifier(shortcuts) { 327 info("Test Ctrl modifier"); 328 const onKey = once(shortcuts, "Ctrl+F1", event => { 329 is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); 330 ok(!event.altKey); 331 ok(event.ctrlKey); 332 ok(!event.metaKey); 333 ok(!event.shiftKey); 334 }); 335 const onKeyAlias = once(shortcuts, "Control+F1", event => { 336 is(event.keyCode, window.KeyboardEvent.DOM_VK_F1); 337 ok(!event.altKey); 338 ok(event.ctrlKey); 339 ok(!event.metaKey); 340 ok(!event.shiftKey); 341 }); 342 EventUtils.synthesizeKey("VK_F1", { ctrlKey: true }, window); 343 await onKey; 344 await onKeyAlias; 345 } 346 347 async function testCmdShiftShortcut(shortcuts) { 348 if (!isOSX) { 349 // This test is OSX only (Bug 1300458). 350 return; 351 } 352 353 const onCmdKey = once(shortcuts, "CmdOrCtrl+[", event => { 354 is(event.key, "["); 355 ok(!event.altKey); 356 ok(!event.ctrlKey); 357 ok(event.metaKey); 358 ok(!event.shiftKey); 359 }); 360 const onCmdShiftKey = once(shortcuts, "CmdOrCtrl+Shift+[", event => { 361 is(event.key, "["); 362 ok(!event.altKey); 363 ok(!event.ctrlKey); 364 ok(event.metaKey); 365 ok(event.shiftKey); 366 }); 367 368 EventUtils.synthesizeKey("[", { metaKey: true, shiftKey: true }, window); 369 EventUtils.synthesizeKey("[", { metaKey: true }, window); 370 371 await onCmdKey; 372 await onCmdShiftKey; 373 } 374 375 async function testTarget() { 376 info("Test KeyShortcuts with target argument"); 377 378 const target = document.createElementNS( 379 "http://www.w3.org/1999/xhtml", 380 "input" 381 ); 382 document.documentElement.appendChild(target); 383 target.focus(); 384 385 const shortcuts = new KeyShortcuts({ 386 window, 387 target, 388 }); 389 const onKey = once(shortcuts, "0", event => { 390 is(event.key, "0"); 391 is(event.target, target); 392 }); 393 EventUtils.synthesizeKey("0", {}, window); 394 await onKey; 395 396 target.remove(); 397 398 shortcuts.destroy(); 399 } 400 401 function testInvalidShortcutString(shortcuts) { 402 info("Test wrong shortcut string"); 403 404 const shortcut = KeyShortcuts.parseElectronKey("Cmmd+F"); 405 ok( 406 !shortcut, 407 "Passing a invalid shortcut string should return a null object" 408 ); 409 410 shortcuts.on("Cmmd+F", function () {}); 411 ok(true, "on() shouldn't throw when passing invalid shortcut string"); 412 } 413 414 // Can happen on localized builds where the value of the localized string is 415 // empty, eg `toolbox.elementPicker.key=`. See Bug 1569572. 416 function testNullShortcut(shortcuts) { 417 info("Test null shortcut"); 418 419 const shortcut = KeyShortcuts.parseElectronKey(null); 420 ok(!shortcut, "Passing a null object should return a null object"); 421 422 const stringified = KeyShortcuts.stringifyShortcut(shortcut); 423 is(stringified, "", "A null object should be stringified as an empty string"); 424 425 shortcuts.on(null, function () {}); 426 ok(true, "on() shouldn't throw when passing a null object"); 427 } 428 429 /** 430 * Shift+Alt+I generates ^ key (`event.key`) on OSX and KeyShortcuts module 431 * must ensure that this doesn't interfere with shortcuts CmdOrCtrl+Alt+Shift+I 432 * for opening the Browser Toolbox and CmdOrCtrl+Alt+I for toggling the Toolbox. 433 */ 434 async function testTabCharacterShortcut(shortcuts) { 435 if (!isOSX) { 436 return; 437 } 438 439 info("Test tab character shortcut"); 440 441 once(shortcuts, "CmdOrCtrl+Alt+I", () => { 442 ok(false, "This handler must not be executed"); 443 }); 444 445 const onKey = once(shortcuts, "CmdOrCtrl+Alt+Shift+I", event => { 446 info("Test for CmdOrCtrl+Alt+Shift+I"); 447 is(event.key, "^"); 448 is(event.keyCode, 73); 449 }); 450 451 // Simulate `CmdOrCtrl+Alt+Shift+I` shortcut. Note that EventUtils doesn't 452 // generate `^` like real keyboard, so we need to pass it explicitly 453 // and use proper keyCode for `I` character. 454 EventUtils.synthesizeKey( 455 "^", 456 { 457 code: "KeyI", 458 key: "^", 459 keyCode: 73, 460 shiftKey: true, 461 altKey: true, 462 metaKey: true, 463 }, 464 window 465 ); 466 467 await onKey; 468 }