input-events-typing.html (14684B)
1 <!DOCTYPE html> 2 <meta charset="utf-8"> 3 <title>Input Event typing tests</title> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script src="/resources/testdriver.js"></script> 7 <script src="/resources/testdriver-vendor.js"></script> 8 <script src="/resources/testdriver-actions.js"></script> 9 <div id="rich" contenteditable></div> 10 <textarea id="plain"></textarea> 11 <script> 12 let inputEventsLog = []; 13 const rich = document.getElementById('rich'); 14 const plain = document.getElementById('plain'); 15 16 function log(event) { 17 const clone = new event.constructor(event.type, event); 18 clone.state = rich.innerHTML; 19 inputEventsLog.push(clone); 20 } 21 22 function resetRich() { 23 inputEventsLog = []; 24 rich.innerHTML = ''; 25 } 26 27 function resetPlain() { 28 inputEventsLog = []; 29 plain.innerHTML = ''; 30 } 31 32 rich.addEventListener('beforeinput', log); 33 rich.addEventListener('input', log); 34 35 promise_test(async function() { 36 this.add_cleanup(resetRich); 37 rich.focus(); 38 const message = 'Hello'; 39 await test_driver.send_keys(rich, message); 40 // 10 events (5 beforeinput + 5 input events) 41 assert_equals(inputEventsLog.length, 10); 42 for (let i = 0; i < inputEventsLog.length; i += 2) { 43 const beforeInputEvent = inputEventsLog[i]; 44 const inputEvent = inputEventsLog[i + 1]; 45 assert_equals(beforeInputEvent.type, 'beforeinput'); 46 assert_equals(inputEvent.type, 'input'); 47 assert_equals(beforeInputEvent.inputType, 'insertText'); 48 assert_equals(inputEvent.inputType, 'insertText'); 49 assert_equals(beforeInputEvent.data, inputEvent.data); 50 assert_equals(inputEvent.data, message[i / 2]); 51 assert_equals(beforeInputEvent.state + message[i / 2], inputEvent.state); 52 } 53 }, 'It triggers beforeinput and input events on text typing'); 54 55 promise_test(async function() { 56 this.add_cleanup(resetRich); 57 rich.focus(); 58 await test_driver.send_keys(rich, "\uE006"); // Return 59 60 assert_equals(inputEventsLog.length, 2); 61 const beforeInputEvent = inputEventsLog[0]; 62 const inputEvent = inputEventsLog[1]; 63 assert_equals(beforeInputEvent.type, 'beforeinput'); 64 assert_equals(inputEvent.type, 'input'); 65 assert_equals(beforeInputEvent.inputType, 'insertParagraph'); 66 assert_equals(inputEvent.inputType, 'insertParagraph'); 67 assert_equals(beforeInputEvent.data, inputEvent.data); 68 }, 'It triggers beforeinput and input events on typing RETURN'); 69 70 promise_test(async function() { 71 this.add_cleanup(resetPlain); 72 const expectedResult = [ 73 // Pressing 'a' 74 'insertText', 75 // Return 76 'insertLineBreak' 77 ]; 78 const result = []; 79 80 plain.addEventListener("input", (inputEvent) => { 81 result.push(inputEvent.inputType); 82 }); 83 await test_driver.click(plain); 84 await test_driver.send_keys(plain,"a"); 85 await test_driver.send_keys(plain,'\uE006'); // Return 86 assert_equals(result.length, expectedResult.length); 87 expectedResult.forEach((er, index) => assert_equals(result[index], er)); 88 }, 'Newline character in plain text editing should get insertLinebreak input event'); 89 90 promise_test(async function() { 91 this.add_cleanup(resetRich); 92 rich.focus(); 93 await new test_driver.Actions() 94 .keyDown('\uE008') // Shift 95 .keyDown('\uE006') // Return 96 .keyUp('\uE006') 97 .keyUp('\uE008') 98 .send(); 99 100 assert_equals(inputEventsLog.length, 2); 101 const [beforeInputEvent, inputEvent] = inputEventsLog; 102 assert_equals(beforeInputEvent.type, 'beforeinput'); 103 assert_equals(inputEvent.type, 'input'); 104 assert_equals(beforeInputEvent.inputType, 'insertLineBreak'); 105 assert_equals(inputEvent.inputType, 'insertLineBreak'); 106 assert_equals(beforeInputEvent.data, inputEvent.data); 107 }, 'It triggers beforeinput and input events on typing Shift+RETURN'); 108 109 promise_test(async function() { 110 this.add_cleanup(resetRich); 111 rich.innerHTML = '<p>Preexisting <i id="caret">c</i>ontent</p>'; 112 const caret = document.querySelector('#caret'); 113 await test_driver.click(caret); 114 await test_driver.send_keys(caret, "\uE017"); // Delete 115 116 assert_equals(inputEventsLog.length, 2); 117 const [beforeInputEvent, inputEvent] = inputEventsLog; 118 assert_equals(beforeInputEvent.type, 'beforeinput'); 119 assert_equals(inputEvent.type, 'input'); 120 assert_equals(beforeInputEvent.inputType, 'deleteContentForward'); 121 assert_equals(inputEvent.inputType, 'deleteContentForward'); 122 assert_equals(beforeInputEvent.data, inputEvent.data); 123 }, 'It triggers beforeinput and input events on typing DELETE with pre-existing content'); 124 125 promise_test(async function() { 126 this.add_cleanup(resetRich); 127 rich.focus(); 128 await test_driver.send_keys(rich, "\uE017"); // Delete 129 assert_equals(inputEventsLog.length, 2); 130 const [beforeInputEvent, inputEvent] = inputEventsLog; 131 assert_equals(beforeInputEvent.type, 'beforeinput'); 132 assert_equals(inputEvent.type, 'input'); 133 assert_equals(beforeInputEvent.inputType, 'deleteContentForward'); 134 assert_equals(inputEvent.inputType, 'deleteContentForward'); 135 assert_equals(beforeInputEvent.data, inputEvent.data); 136 }, 'It triggers beforeinput and input events on typing DELETE with no pre-existing content'); 137 138 promise_test(async function() { 139 this.add_cleanup(resetRich); 140 rich.innerHTML = '<p>Preexisting <i id="caret">c</i>ontent</p>'; 141 142 await test_driver.click(document.querySelector('#caret')); 143 await test_driver.send_keys(rich, "\uE003"); // Back Space 144 145 assert_equals(inputEventsLog.length, 2); 146 const [beforeInputEvent, inputEvent] = inputEventsLog; 147 assert_equals(beforeInputEvent.type, 'beforeinput'); 148 assert_equals(inputEvent.type, 'input'); 149 assert_equals(beforeInputEvent.inputType, 'deleteContentBackward'); 150 assert_equals(inputEvent.inputType, 'deleteContentBackward'); 151 assert_equals(beforeInputEvent.data, inputEvent.data); 152 }, 'It triggers beforeinput and input events on typing BACK_SPACE with pre-existing content'); 153 154 promise_test(async function () { 155 this.add_cleanup(resetRich); 156 rich.innerHTML = '<p>Preexisting <i id="caret">C</i>ontent</p>'; 157 158 const expectedResult = [ 159 // Pressing 'a', 'b' 160 'insertText', 161 'insertText', 162 // Delete twice 163 'deleteContentForward', 164 'deleteContentForward', 165 // Pressing 'c', 'd' 166 'insertText', 167 'insertText', 168 // Backspace 169 'deleteContentBackward' 170 ]; 171 const result = []; 172 173 rich.addEventListener("input", (inputEvent) => { 174 result.push(inputEvent.inputType); 175 }); 176 177 await test_driver.click(document.querySelector('#caret')); // Preexisting |Content 178 await test_driver.send_keys(rich, "a"); // Preexisting a|Content 179 await test_driver.send_keys(rich, "b"); // Preexisting ab|Content 180 // Delete 181 await test_driver.send_keys(rich, "\uE017"); // Preexisting ab|ontent 182 // Delete 183 await test_driver.send_keys(rich, "\uE017"); // Preexisting ab|ntent 184 await test_driver.send_keys(rich, "c"); // Preexisting abc|ntent 185 await test_driver.send_keys(rich, "d"); // Preexisting abcd|ntent 186 // Backspace 187 await test_driver.send_keys(rich, "\uE003"); // Preexisting abc|ntent 188 189 assert_equals(result.length, expectedResult.length); 190 expectedResult.forEach((er, index) => assert_equals(result[index], er)); 191 }, 'Input events have correct inputType updated when different inputs are typed'); 192 193 promise_test(async function () { 194 this.add_cleanup(resetRich); 195 rich.innerHTML = '<p>Preexisting <i id="caret">c</i>ontent</p>'; 196 197 const expectedResult = [ 198 // Remove selected text with Backspace 199 'deleteContentBackward', 200 // Remove selected text with Delete 201 'deleteContentForward' 202 ]; 203 const result = []; 204 205 rich.addEventListener("input", (inputEvent) => { 206 result.push(inputEvent.inputType); 207 }); 208 209 const modifierKey = navigator.platform === "MacIntel" ? '\u2318' : '\uE009'; 210 211 // Click before "content" 212 await test_driver.click(document.querySelector('#caret')); // Preexisting |content 213 // Select text to the left 214 await new test_driver.Actions() 215 .keyDown(modifierKey) 216 .keyDown('\uE008') // Shift 217 .keyDown('\uE012') // Arrow Left 218 .keyUp('\uE012') 219 .keyUp('\uE008') 220 .keyUp(modifierKey) 221 .send(); // |Preexisting ^content 222 // Backspace 223 await test_driver.send_keys(rich, "\uE003"); // |content 224 // Select text to the right 225 await new test_driver.Actions() 226 .keyDown(modifierKey) 227 .keyDown('\uE008') // Shift 228 .keyDown('\uE014') // Arrow Right 229 .keyUp('\uE012') 230 .keyUp('\uE008') 231 .keyUp(modifierKey) 232 .send(); // ^content| 233 // Delete 234 await test_driver.send_keys(rich, "\uE017"); // | 235 236 assert_equals(result.length, expectedResult.length); 237 expectedResult.forEach((er, index) => assert_equals(result[index], er)); 238 }, 'Input events have correct inputType when selected text is removed with Backspace or Delete'); 239 240 promise_test(async function() { 241 this.add_cleanup(resetRich); 242 rich.focus(); 243 await test_driver.send_keys(rich, "\uE003"); // Back Space 244 245 assert_equals(inputEventsLog.length, 2); 246 const [beforeInputEvent, inputEvent] = inputEventsLog; 247 assert_equals(beforeInputEvent.type, 'beforeinput'); 248 assert_equals(inputEvent.type, 'input'); 249 assert_equals(beforeInputEvent.inputType, 'deleteContentBackward'); 250 assert_equals(inputEvent.inputType, 'deleteContentBackward'); 251 assert_equals(beforeInputEvent.data, inputEvent.data); 252 }, 'It triggers beforeinput and input events on typing BACK_SPACE with no pre-existing content'); 253 254 promise_test(async function() { 255 this.add_cleanup(resetRich); 256 rich.focus(); 257 await test_driver.send_keys(rich, "hello"); 258 259 // Decide whether to use Key.COMMAND (mac) or Key.CONTROL (everything else) 260 const modifierKey = navigator.platform === "MacIntel" ? '\u2318' : '\uE009'; 261 262 // Undo 263 await new test_driver.Actions() 264 .keyDown(modifierKey) 265 .keyDown('z') 266 .keyUp('z') 267 .keyUp(modifierKey) 268 .send(); 269 // Redo 270 await new test_driver.Actions() 271 .keyDown(modifierKey) 272 .keyDown('\uE008') // Shift 273 .keyDown('z') 274 .keyUp('z') 275 .keyUp('\uE008') 276 .keyUp(modifierKey) 277 .send(); 278 279 // Ignore the initial typing of 'hello' 280 const historyInputEventsLog = inputEventsLog.slice(10); 281 282 assert_equals(historyInputEventsLog.length, 4); 283 const inputTypes = ['historyUndo', 'historyRedo']; 284 for (let i = 0; i < historyInputEventsLog.length; i += 2) { 285 // We are increaisng i by 2 as there should always be matching beforeinput and input events. 286 const beforeInputEvent = historyInputEventsLog[i]; 287 const inputEvent = historyInputEventsLog[i + 1]; 288 assert_equals(beforeInputEvent.type, 'beforeinput'); 289 assert_equals(inputEvent.type, 'input'); 290 assert_equals(beforeInputEvent.inputType, inputTypes[i / 2]); 291 assert_equals(inputEvent.inputType, inputTypes[i / 2]); 292 assert_equals(beforeInputEvent.data, inputEvent.data); 293 } 294 }, 'It triggers beforeinput and input events on typing Undo and Redo key combinations with an existing history'); 295 296 promise_test(async function() { 297 this.add_cleanup(resetRich); 298 rich.focus(); 299 // Decide whether to use Key.COMMAND (mac) or Key.CONTROL (everything else) 300 const modifierKey = navigator.platform === "MacIntel" ? '\u2318' : '\uE009'; 301 302 // Undo 303 await new test_driver.Actions() 304 .keyDown(modifierKey) 305 .keyDown('z') 306 .keyUp('z') 307 .keyUp(modifierKey) 308 .send(); 309 // Redo 310 await new test_driver.Actions() 311 .keyDown(modifierKey) 312 .keyDown('\uE008') // Shift 313 .keyDown('z') 314 .keyUp('z') 315 .keyUp('\uE008') 316 .keyUp(modifierKey) 317 .send(); 318 319 assert_equals(inputEventsLog.length, 4); 320 const inputTypes = ['historyUndo', 'historyRedo']; 321 for (let i = 0; i < inputEventsLog.length; i += 2) { 322 const beforeInputEvent = inputEventsLog[i]; 323 const inputEvent = inputEventsLog[i + 1]; 324 assert_equals(beforeInputEvent.type, 'beforeinput'); 325 assert_equals(inputEvent.type, 'input'); 326 assert_equals(beforeInputEvent.inputType, inputTypes[i / 2]); 327 assert_equals(inputEvent.inputType, inputTypes[i / 2]); 328 assert_equals(beforeInputEvent.data, inputEvent.data); 329 } 330 }, 'It triggers beforeinput and input events on typing Undo and Redo key combinations without an existing history'); 331 332 promise_test(async function() { 333 this.add_cleanup(resetRich); 334 const expectedResult = [ 335 // Pressing 'a'. 336 'plain-keydown-a', 337 'plain-keypress-a', 338 'plain-beforeinput-a-null', 339 'plain-input-a-null', 340 'plain-keyup-a', 341 // Pressing Shift-'b'. 342 'plain-keydown-B', 343 'plain-keypress-B', 344 'plain-beforeinput-B-null', 345 'plain-input-B-null', 346 'plain-keyup-B', 347 // Pressing 'c'. 348 'rich-keydown-c', 349 'rich-keypress-c', 350 'rich-beforeinput-c-null', 351 'rich-input-c-null', 352 'rich-keyup-c', 353 // Pressing Shift-'d'. 354 'rich-keydown-D', 355 'rich-keypress-D', 356 'rich-beforeinput-D-null', 357 'rich-input-D-null', 358 'rich-keyup-D', 359 ]; 360 const result = []; 361 362 for (const eventType of ['beforeinput', 'input', 'keydown', 'keypress', 'keyup']) { 363 const listener = event => { 364 if (event.key === 'Shift') return; 365 const eventInfo = [event.target.id, event.type, event.data || event.key]; 366 if (event instanceof InputEvent) eventInfo.push(String(event.dataTransfer)); 367 result.push(eventInfo.join('-')); 368 } 369 rich.addEventListener(eventType, listener); 370 plain.addEventListener(eventType, listener); 371 } 372 373 plain.focus(); 374 await new test_driver.Actions() 375 .keyDown('a') 376 .keyUp('a') 377 .keyDown('\uE008') // Shift 378 .keyDown('b') 379 .keyUp('b') 380 .keyUp('\uE008') 381 .send(); 382 383 rich.focus(); 384 await new test_driver.Actions() 385 .keyDown('c') 386 .keyUp('c') 387 .keyDown('\uE008') // Shift 388 .keyDown('d') 389 .keyUp('d') 390 .keyUp('\uE008') 391 .send(); 392 393 assert_equals(result.length, expectedResult.length); 394 expectedResult.forEach((er, index) => assert_equals(result[index], er)); 395 }, 'InputEvents have correct data/order when typing on textarea and contenteditable'); 396 </script>