test_texteditor_keyevent_handling.html (18123B)
1 <html> 2 <head> 3 <title>Test for key event handler of text editor</title> 4 <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> 5 <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> 6 <link rel="stylesheet" type="text/css" 7 href="chrome://mochikit/content/tests/SimpleTest/test.css" /> 8 </head> 9 <body> 10 <div id="display"> 11 <input type="text" id="inputField"> 12 <input type="password" id="passwordField"> 13 <textarea id="textarea"></textarea> 14 </div> 15 <div id="content" style="display: none"> 16 17 </div> 18 <pre id="test"> 19 </pre> 20 21 <script class="testbody" type="application/javascript"> 22 23 SimpleTest.waitForExplicitFinish(); 24 SimpleTest.waitForFocus(runTests, window); 25 26 var inputField = document.getElementById("inputField"); 27 var passwordField = document.getElementById("passwordField"); 28 var textarea = document.getElementById("textarea"); 29 30 const kIsMac = navigator.platform.includes("Mac"); 31 const kIsWin = navigator.platform.includes("Win"); 32 const kIsLinux = navigator.platform.includes("Linux"); 33 34 async function runTests() { 35 var fm = SpecialPowers.Services.focus; 36 37 var capturingPhase = { fired: false, prevented: false }; 38 var bubblingPhase = { fired: false, prevented: false }; 39 40 var listener = { 41 handleEvent: function _hv(aEvent) { 42 is(aEvent.type, "keypress", "unexpected event is handled"); 43 switch (aEvent.eventPhase) { 44 case aEvent.CAPTURING_PHASE: 45 capturingPhase.fired = true; 46 capturingPhase.prevented = aEvent.defaultPrevented; 47 break; 48 case aEvent.BUBBLING_PHASE: 49 bubblingPhase.fired = true; 50 bubblingPhase.prevented = aEvent.defaultPrevented; 51 aEvent.preventDefault(); // prevent the browser default behavior 52 break; 53 default: 54 ok(false, "event is handled in unexpected phase"); 55 } 56 }, 57 }; 58 59 function check(aDescription, 60 aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling) { 61 function getDesciption(aExpected) { 62 return aDescription + (aExpected ? " wasn't " : " was "); 63 } 64 65 is(capturingPhase.fired, aFiredOnCapture, 66 getDesciption(aFiredOnCapture) + "fired on capture phase"); 67 is(bubblingPhase.fired, aFiredOnBubbling, 68 getDesciption(aFiredOnBubbling) + "fired on bubbling phase"); 69 70 // If the event is fired on bubbling phase and it was already prevented 71 // on capture phase, it must be prevented on bubbling phase too. 72 if (capturingPhase.prevented) { 73 todo(false, aDescription + 74 " was consumed already, so, we cannot test the editor behavior actually"); 75 aPreventedOnBubbling = true; 76 } 77 78 is(bubblingPhase.prevented, aPreventedOnBubbling, 79 getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase"); 80 } 81 82 var parentElement = document.getElementById("display"); 83 SpecialPowers.wrap(parentElement).addEventListener("keypress", listener, { capture: true, mozSystemGroup: true }); 84 SpecialPowers.wrap(parentElement).addEventListener("keypress", listener, { capture: false, mozSystemGroup: true }); 85 86 async function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly) { 87 function reset(aText) { 88 capturingPhase.fired = false; 89 capturingPhase.prevented = false; 90 bubblingPhase.fired = false; 91 bubblingPhase.prevented = false; 92 aElement.value = aText; 93 } 94 95 if (document.activeElement) { 96 document.activeElement.blur(); 97 } 98 99 aDescription += ": "; 100 101 aElement.focus(); 102 is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus"); 103 104 // Backspace key: 105 // If native key bindings map the key combination to something, it's consumed. 106 // If editor is readonly, it doesn't consume. 107 // If editor is editable, it consumes backspace and shift+backspace. 108 // Otherwise, editor doesn't consume the event but the native key 109 // bindings on nsTextControlFrame may consume it. 110 reset(""); 111 synthesizeKey("KEY_Backspace"); 112 check(aDescription + "Backspace", true, true, true); 113 114 reset(""); 115 synthesizeKey("KEY_Backspace", {shiftKey: true}); 116 check(aDescription + "Shift+Backspace", true, true, true); 117 118 reset(""); 119 synthesizeKey("KEY_Backspace", {ctrlKey: true}); 120 // Win: cmd_deleteWordBackward 121 check(aDescription + "Ctrl+Backspace", 122 true, true, aIsReadonly || kIsWin); 123 124 reset(""); 125 synthesizeKey("KEY_Backspace", {altKey: true}); 126 // Win: cmd_undo 127 // Mac: cmd_deleteWordBackward 128 check(aDescription + "Alt+Backspace", 129 true, true, aIsReadonly || kIsWin || kIsMac); 130 131 reset(""); 132 synthesizeKey("KEY_Backspace", {metaKey: true}); 133 check(aDescription + "Meta+Backspace", true, true, aIsReadonly || kIsMac); 134 135 // Delete key: 136 // If native key bindings map the key combination to something, it's consumed. 137 // If editor is readonly, it doesn't consume. 138 // If editor is editable, delete is consumed. 139 // Otherwise, editor doesn't consume the event but the native key 140 // bindings on nsTextControlFrame may consume it. 141 reset(""); 142 synthesizeKey("KEY_Delete"); 143 // Linux: native handler 144 // Mac: cmd_deleteCharForward 145 check(aDescription + "Delete", 146 true, true, !aIsReadonly || kIsLinux || kIsMac); 147 148 reset(""); 149 // Win: cmd_cutOrDelete 150 // Linux: cmd_cut 151 // Mac: cmd_deleteCharForward 152 synthesizeKey("KEY_Delete", {shiftKey: true}); 153 check(aDescription + "Shift+Delete", 154 true, true, true); 155 156 reset(""); 157 synthesizeKey("KEY_Delete", {ctrlKey: true}); 158 // Win: cmd_deleteWordForward 159 // Linux: cmd_copy 160 check(aDescription + "Ctrl+Delete", 161 true, true, kIsWin || kIsLinux); 162 163 reset(""); 164 synthesizeKey("KEY_Delete", {altKey: true}); 165 // Mac: cmd_deleteWordForward 166 check(aDescription + "Alt+Delete", 167 true, true, kIsMac); 168 169 reset(""); 170 synthesizeKey("KEY_Delete", {metaKey: true}); 171 // Linux: native handler consumed. 172 check(aDescription + "Meta+Delete", 173 true, true, kIsLinux); 174 175 // XXX input.value returns "\n" when it's empty, so, we should use dummy 176 // value ("a") for the following tests. 177 178 // Return key: 179 // If editor is readonly, it doesn't consume. 180 // If editor is editable and not single line editor, it consumes Return 181 // and Shift+Return. 182 // Otherwise, editor doesn't consume the event. 183 reset("a"); 184 synthesizeKey("KEY_Enter"); 185 check(aDescription + "Return", 186 true, true, !aIsSingleLine && !aIsReadonly); 187 is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a", 188 aDescription + "Return"); 189 190 reset("a"); 191 synthesizeKey("KEY_Enter", {shiftKey: true}); 192 check(aDescription + "Shift+Return", 193 true, true, !aIsSingleLine && !aIsReadonly); 194 is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a", 195 aDescription + "Shift+Return"); 196 197 reset("a"); 198 synthesizeKey("KEY_Enter", {ctrlKey: true}); 199 check(aDescription + "Ctrl+Return", true, true, false); 200 is(aElement.value, "a", aDescription + "Ctrl+Return"); 201 202 reset("a"); 203 synthesizeKey("KEY_Enter", {altKey: true}); 204 check(aDescription + "Alt+Return", true, true, false); 205 is(aElement.value, "a", aDescription + "Alt+Return"); 206 207 reset("a"); 208 synthesizeKey("KEY_Enter", {metaKey: true}); 209 check(aDescription + "Meta+Return", true, true, false); 210 is(aElement.value, "a", aDescription + "Meta+Return"); 211 212 // Tab key: 213 // Editor consumes tab key event unless any modifier keys are pressed. 214 reset("a"); 215 synthesizeKey("KEY_Tab"); 216 check(aDescription + "Tab", true, true, false); 217 is(aElement.value, "a", aDescription + "Tab"); 218 is(SpecialPowers.unwrap(fm.focusedElement), aElement, 219 aDescription + "focus moved unexpectedly (Tab)"); 220 aElement.focus(); 221 222 reset("a"); 223 synthesizeKey("KEY_Tab", {shiftKey: true}); 224 check(aDescription + "Shift+Tab", true, true, false); 225 is(aElement.value, "a", aDescription + "Shift+Tab"); 226 is(SpecialPowers.unwrap(fm.focusedElement), aElement, 227 aDescription + "focus moved unexpectedly (Shift+Tab)"); 228 229 // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress 230 // event should never be fired. 231 reset("a"); 232 synthesizeKey("KEY_Tab", {ctrlKey: true}); 233 check(aDescription + "Ctrl+Tab", false, false, false); 234 is(aElement.value, "a", aDescription + "Ctrl+Tab"); 235 is(SpecialPowers.unwrap(fm.focusedElement), aElement, 236 aDescription + "focus moved unexpectedly (Ctrl+Tab)"); 237 238 reset("a"); 239 synthesizeKey("KEY_Tab", {altKey: true}); 240 check(aDescription + "Alt+Tab", true, true, false); 241 is(aElement.value, "a", aDescription + "Alt+Tab"); 242 is(SpecialPowers.unwrap(fm.focusedElement), aElement, 243 aDescription + "focus moved unexpectedly (Alt+Tab)"); 244 245 reset("a"); 246 synthesizeKey("KEY_Tab", {metaKey: true}); 247 check(aDescription + "Meta+Tab", true, true, false); 248 is(aElement.value, "a", aDescription + "Meta+Tab"); 249 is(SpecialPowers.unwrap(fm.focusedElement), aElement, 250 aDescription + "focus moved unexpectedly (Meta+Tab)"); 251 252 // Esc key: 253 // In all cases, esc key events are not consumed 254 reset("abc"); 255 synthesizeKey("KEY_Escape"); 256 check(aDescription + "Esc", true, true, false); 257 258 reset("abc"); 259 synthesizeKey("KEY_Escape", {shiftKey: true}); 260 check(aDescription + "Shift+Esc", true, true, false); 261 262 reset("abc"); 263 synthesizeKey("KEY_Escape", {ctrlKey: true}); 264 check(aDescription + "Ctrl+Esc", true, true, false); 265 266 reset("abc"); 267 synthesizeKey("KEY_Escape", {altKey: true}); 268 check(aDescription + "Alt+Esc", true, true, false); 269 270 reset("abc"); 271 synthesizeKey("KEY_Escape", {metaKey: true}); 272 check(aDescription + "Meta+Esc", true, true, false); 273 274 // typical typing tests: 275 reset(""); 276 sendString("M"); 277 check(aDescription + "M", true, true, !aIsReadonly); 278 sendString("o"); 279 check(aDescription + "o", true, true, !aIsReadonly); 280 sendString("z"); 281 check(aDescription + "z", true, true, !aIsReadonly); 282 sendString("i"); 283 check(aDescription + "i", true, true, !aIsReadonly); 284 sendString("l"); 285 check(aDescription + "l", true, true, !aIsReadonly); 286 sendString("l"); 287 check(aDescription + "l", true, true, !aIsReadonly); 288 sendString("a"); 289 check(aDescription + "a", true, true, !aIsReadonly); 290 sendString(" "); 291 check(aDescription + "' '", true, true, !aIsReadonly); 292 is(aElement.value, !aIsReadonly ? "Mozilla " : "", 293 aDescription + "typed \"Mozilla \""); 294 295 // typing non-BMP character: 296 async function test_typing_surrogate_pair( 297 aTestPerSurrogateKeyPress, 298 aTestIllFormedUTF16KeyValue = false 299 ) { 300 await SpecialPowers.pushPrefEnv({ 301 set: [ 302 ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress], 303 ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue], 304 ], 305 }); 306 reset(""); 307 let events = []; 308 function pushIntoEvents(aEvent) { 309 events.push(aEvent); 310 } 311 function getEventData(aKeyboardEventOrInputEvent) { 312 if (!aKeyboardEventOrInputEvent) { 313 return "{}"; 314 } 315 switch (aKeyboardEventOrInputEvent.type) { 316 case "keydown": 317 case "keypress": 318 case "keyup": 319 return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${ 320 aKeyboardEventOrInputEvent.key 321 }", charCode=0x${ 322 aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase() 323 } }`; 324 default: 325 return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${ 326 aKeyboardEventOrInputEvent.inputType 327 }", data="${aKeyboardEventOrInputEvent.data}" }`; 328 } 329 } 330 function getEventArrayData(aEvents) { 331 if (!aEvents.length) { 332 return "[]"; 333 } 334 let result = "[\n"; 335 for (const e of aEvents) { 336 result += ` ${getEventData(e)}\n`; 337 } 338 return result + "]"; 339 } 340 aElement.addEventListener("keydown", pushIntoEvents); 341 aElement.addEventListener("keypress", pushIntoEvents); 342 aElement.addEventListener("keyup", pushIntoEvents); 343 aElement.addEventListener("beforeinput", pushIntoEvents); 344 aElement.addEventListener("input", pushIntoEvents); 345 synthesizeKey("\uD842\uDFB7"); 346 aElement.removeEventListener("keydown", pushIntoEvents); 347 aElement.removeEventListener("keypress", pushIntoEvents); 348 aElement.removeEventListener("keyup", pushIntoEvents); 349 aElement.removeEventListener("beforeinput", pushIntoEvents); 350 aElement.removeEventListener("input", pushIntoEvents); 351 const settingDescription = 352 `aTestPerSurrogateKeyPress=${ 353 aTestPerSurrogateKeyPress 354 }, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`; 355 const allowIllFormedUTF16 = 356 aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue; 357 358 check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly); 359 is( 360 aElement.value, 361 !aIsReadonly ? "\uD842\uDFB7" : "", 362 `${aDescription}, ${ 363 settingDescription 364 }, The typed surrogate pair should've been inserted` 365 ); 366 if (aIsReadonly) { 367 is( 368 getEventArrayData(events), 369 getEventArrayData( 370 // eslint-disable-next-line no-nested-ternary 371 aTestPerSurrogateKeyPress 372 ? ( 373 allowIllFormedUTF16 374 ? [ 375 { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, 376 { type: "keypress", key: "\uD842", charCode: 0xD842 }, 377 { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, 378 { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, 379 ] 380 : [ 381 { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, 382 { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, 383 { type: "keypress", key: "", charCode: 0xDFB7 }, 384 { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, 385 ] 386 ) 387 : [ 388 { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, 389 { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, 390 { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, 391 ] 392 ), 393 `${aDescription}, ${ 394 settingDescription 395 }, Typing a surrogate pair in readonly editor should not cause input events` 396 ); 397 } else { 398 is( 399 getEventArrayData(events), 400 getEventArrayData( 401 // eslint-disable-next-line no-nested-ternary 402 aTestPerSurrogateKeyPress 403 ? ( 404 allowIllFormedUTF16 405 ? [ 406 { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, 407 { type: "keypress", key: "\uD842", charCode: 0xD842 }, 408 { type: "beforeinput", data: "\uD842", inputType: "insertText" }, 409 { type: "input", data: "\uD842", inputType: "insertText" }, 410 { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, 411 { type: "beforeinput", data: "\uDFB7", inputType: "insertText" }, 412 { type: "input", data: "\uDFB7", inputType: "insertText" }, 413 { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, 414 ] 415 : [ 416 { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, 417 { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, 418 { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, 419 { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, 420 { type: "keypress", key: "", charCode: 0xDFB7 }, 421 { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, 422 ] 423 ) 424 : [ 425 { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, 426 { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, 427 { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, 428 { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, 429 { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, 430 ] 431 ), 432 `${aDescription}, ${ 433 settingDescription 434 }, Typing a surrogate pair in editor should cause input events` 435 ); 436 } 437 } 438 await test_typing_surrogate_pair(true, true); 439 await test_typing_surrogate_pair(true, false); 440 await test_typing_surrogate_pair(false); 441 } 442 443 await doTest(inputField, "<input type=\"text\">", true, false); 444 445 inputField.setAttribute("readonly", "readonly"); 446 await doTest(inputField, "<input type=\"text\" readonly>", true, true); 447 448 await doTest(passwordField, "<input type=\"password\">", true, false); 449 450 passwordField.setAttribute("readonly", "readonly"); 451 await doTest(passwordField, "<input type=\"password\" readonly>", true, true); 452 453 await doTest(textarea, "<textarea>", false, false); 454 455 textarea.setAttribute("readonly", "readonly"); 456 await doTest(textarea, "<textarea readonly>", false, true); 457 458 SpecialPowers.wrap(parentElement).removeEventListener("keypress", listener, { capture: true, mozSystemGroup: true }); 459 SpecialPowers.wrap(parentElement).removeEventListener("keypress", listener, { capture: false, mozSystemGroup: true }); 460 461 SimpleTest.finish(); 462 } 463 464 </script> 465 </body> 466 467 </html>