test_middle_click_paste.html (31312B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Test for paste with middle button click</title> 5 <script src="/tests/SimpleTest/SimpleTest.js"></script> 6 <script src="/tests/SimpleTest/EventUtils.js"></script> 7 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 8 </head> 9 <body> 10 <p id="display"></p> 11 <div id="content" style="display: none;"> 12 13 </div> 14 15 <div id="container"></div> 16 17 <textarea id="toCopyPlaintext" style="display: none;"></textarea> 18 <iframe id="toCopyHTMLContent" srcdoc="<body></body>" style="display: none;"></iframe> 19 20 <pre id="test"> 21 22 <script class="testbody" type="application/javascript"> 23 SimpleTest.waitForExplicitFinish(); 24 25 // TODO: This file should test complicated cases too. 26 // E.g., pasting into existing content, e.g., pasting invalid child 27 // element for the parent elements at insertion point. 28 29 async function copyPlaintext(aText) { 30 return new Promise(resolve => { 31 SimpleTest.waitForClipboard(aText, 32 () => { 33 let element = document.getElementById("toCopyPlaintext"); 34 element.style.display = "block"; 35 element.focus(); 36 element.value = aText; 37 synthesizeKey("a", {accelKey: true}); 38 synthesizeKey("c", {accelKey: true}); 39 }, 40 () => { 41 ok(true, `Succeeded to copy "${aText}" to clipboard`); 42 let element = document.getElementById("toCopyPlaintext"); 43 element.style.display = "none"; 44 resolve(); 45 }, 46 () => { 47 ok(false, `Failed to copy "${aText}" to clipboard`); 48 SimpleTest.finish(); 49 }); 50 }); 51 } 52 53 async function copyHTMLContent(aInnerHTML) { 54 let iframe = document.getElementById("toCopyHTMLContent"); 55 iframe.style.display = "block"; 56 iframe.contentDocument.body.scrollTop; 57 iframe.contentDocument.body.innerHTML = aInnerHTML; 58 iframe.contentWindow.focus(); 59 iframe.contentWindow.getSelection().selectAllChildren(iframe.contentDocument.body); 60 return new Promise(resolve => { 61 SimpleTest.waitForClipboard( 62 () => { return true; }, 63 () => { 64 synthesizeKey("c", {accelKey: true}, iframe.contentWindow); 65 }, 66 () => { 67 ok(true, `Succeeded to copy "${aInnerHTML}" to clipboard as HTML`); 68 iframe.style.display = "none"; 69 resolve(); 70 }, 71 () => { 72 ok(false, `Failed to copy "${aInnerHTML}" to clipboard`); 73 SimpleTest.finish(); 74 }, 75 "text/html"); 76 }); 77 } 78 79 function checkInputEvent(aEvent, aInputType, aData, aDataTransfer, aTargetRanges, aDescription) { 80 ok(aEvent instanceof InputEvent, 81 `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); 82 is(aEvent.cancelable, aEvent.type === "beforeinput", 83 `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`); 84 is(aEvent.bubbles, true, 85 `"${aEvent.type}" event should always bubble ${aDescription}`); 86 is(aEvent.inputType, aInputType, 87 `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`); 88 is(aEvent.data, aData, 89 `data of "${aEvent.type}" event should be ${aData} ${aDescription}`); 90 if (aDataTransfer === null) { 91 is(aEvent.dataTransfer, null, 92 `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); 93 } else { 94 for (let dataTransfer of aDataTransfer) { 95 is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data, 96 `dataTransfer of "${aEvent.type}" should have "${dataTransfer.data}" whose type is "${dataTransfer.type}" ${aDescription}`); 97 } 98 } 99 let targetRanges = aEvent.getTargetRanges(); 100 if (aTargetRanges.length === 0) { 101 is(targetRanges.length, 0, 102 `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`); 103 } else { 104 is(targetRanges.length, aTargetRanges.length, 105 `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`); 106 if (targetRanges.length == aTargetRanges.length) { 107 for (let i = 0; i < targetRanges.length; i++) { 108 is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, 109 `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); 110 is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, 111 `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); 112 is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, 113 `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); 114 is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, 115 `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); 116 } 117 } 118 } 119 } 120 121 async function doTextareaTests(aTextarea) { 122 let beforeInputEvents = []; 123 let inputEvents = []; 124 function onBeforeInput(aEvent) { 125 beforeInputEvents.push(aEvent); 126 } 127 function onInput(aEvent) { 128 inputEvents.push(aEvent); 129 } 130 aTextarea.addEventListener("beforeinput", onBeforeInput); 131 aTextarea.addEventListener("input", onInput); 132 133 await copyPlaintext("abc\ndef\nghi"); 134 aTextarea.focus(); 135 beforeInputEvents = []; 136 inputEvents = []; 137 synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); 138 is(aTextarea.value, 139 "> abc\n> def\n> ghi\n\n", 140 "Pasted each line should start with \"> \""); 141 is(beforeInputEvents.length, 1, 142 'One "beforeinput" event should be fired #1'); 143 checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\nghi", null, [], "#1"); 144 is(inputEvents.length, 1, 145 'One "input" event should be fired #1'); 146 checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\nghi", null, [], "#1"); 147 aTextarea.value = ""; 148 149 await copyPlaintext("> abc\n> def\n> ghi"); 150 aTextarea.focus(); 151 beforeInputEvents = []; 152 inputEvents = []; 153 synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); 154 is(aTextarea.value, 155 ">> abc\n>> def\n>> ghi\n\n", 156 "Pasted each line should be start with \">> \" when already quoted one level"); 157 is(beforeInputEvents.length, 1, 158 'One "beforeinput" event should be fired #2'); 159 checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n> ghi", null, [], "#2"); 160 is(inputEvents.length, 1, 161 'One "input" event should be fired #2'); 162 checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n> ghi", null, [], "#2"); 163 aTextarea.value = ""; 164 165 await copyPlaintext("> abc\n> def\n\nghi"); 166 aTextarea.focus(); 167 beforeInputEvents = []; 168 inputEvents = []; 169 synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); 170 is(aTextarea.value, 171 ">> abc\n>> def\n> \n> ghi\n\n", 172 "Pasted each line should be start with \">> \" when already quoted one level"); 173 is(beforeInputEvents.length, 1, 174 'One "beforeinput" event should be fired #3'); 175 checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n\nghi", null, [], "#3"); 176 is(inputEvents.length, 1, 177 'One "input" event should be fired #3'); 178 checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n\nghi", null, [], "#3"); 179 aTextarea.value = ""; 180 181 await copyPlaintext("abc\ndef\n\n"); 182 aTextarea.focus(); 183 beforeInputEvents = []; 184 inputEvents = []; 185 synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); 186 is(aTextarea.value, 187 "> abc\n> def\n> \n", 188 "If pasted text ends with \"\\n\", only the last line should not started with \">\""); 189 is(beforeInputEvents.length, 1, 190 'One "beforeinput" event should be fired #4'); 191 checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#4"); 192 is(inputEvents.length, 1, 193 'One "input" event should be fired #4'); 194 checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#4"); 195 aTextarea.value = ""; 196 197 await copyPlaintext("abc\ndef\n\n"); 198 aTextarea.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true}); 199 aTextarea.focus(); 200 beforeInputEvents = []; 201 inputEvents = []; 202 synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); 203 is(aTextarea.value, "", 204 'Pasting as quote should have been canceled if "paste" event was canceled'); 205 is(beforeInputEvents.length, 0, 206 'No "beforeinput" event should be fired since "paste" event was canceled #5'); 207 is(inputEvents.length, 0, 208 'No "input" event should be fired since "paste" was canceled #5'); 209 aTextarea.value = ""; 210 211 await copyPlaintext("abc\ndef\n\n"); 212 aTextarea.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true}); 213 aTextarea.focus(); 214 beforeInputEvents = []; 215 inputEvents = []; 216 synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); 217 is(aTextarea.value, "", 218 'Pasting as quote should have been canceled if "beforeinput" event was canceled'); 219 is(beforeInputEvents.length, 1, 220 'One "beforeinput" event should be fired #5'); 221 checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#6"); 222 is(inputEvents.length, 0, 223 'No "input" event should be fired since "beforeinput" was canceled #6'); 224 aTextarea.value = ""; 225 226 let pasteEventCount = 0; 227 function pasteEventLogger() { 228 pasteEventCount++; 229 } 230 aTextarea.addEventListener("paste", pasteEventLogger); 231 232 await copyPlaintext("abc"); 233 aTextarea.focus(); 234 document.body.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true}); 235 beforeInputEvents = []; 236 inputEvents = []; 237 synthesizeMouseAtCenter(aTextarea, {button: 1}); 238 is(aTextarea.value, "abc", 239 "If 'click' event is consumed at capturing phase of the <body>, paste should not be canceled"); 240 is(pasteEventCount, 1, 241 "If 'click' event is consumed at capturing phase of the <body>, 'paste' event should still be fired"); 242 is(beforeInputEvents.length, 1, 243 '"beforeinput" event should be fired when the "click" event is canceled'); 244 checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'when the "click" event is canceled'); 245 is(inputEvents.length, 1, 246 '"input" event should be fired when the "click" event is canceled'); 247 checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'when the "click" event is canceled'); 248 aTextarea.value = ""; 249 250 await copyPlaintext("abc"); 251 aTextarea.focus(); 252 aTextarea.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true}); 253 pasteEventCount = 0; 254 beforeInputEvents = []; 255 inputEvents = []; 256 synthesizeMouseAtCenter(aTextarea, {button: 1}); 257 is(aTextarea.value, "abc", 258 "Even if 'mouseup' event is consumed, paste should be done"); 259 is(pasteEventCount, 1, 260 "Even if 'mouseup' event is consumed, 'paste' event should be fired once"); 261 is(beforeInputEvents.length, 1, 262 'One "beforeinput" event should be fired even if "mouseup" event is canceled'); 263 checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'even if "mouseup" event is canceled'); 264 is(inputEvents.length, 1, 265 'One "input" event should be fired even if "mouseup" event is canceled'); 266 checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'even if "mouseup" event is canceled'); 267 aTextarea.value = ""; 268 269 await copyPlaintext("abc"); 270 aTextarea.focus(); 271 aTextarea.addEventListener("click", (event) => { event.preventDefault(); }, {once: true}); 272 pasteEventCount = 0; 273 beforeInputEvents = []; 274 inputEvents = []; 275 synthesizeMouseAtCenter(aTextarea, {button: 1}); 276 is(aTextarea.value, "abc", 277 "If 'click' event handler is added to the <textarea>, paste should not be canceled"); 278 is(pasteEventCount, 1, 279 "If 'click' event handler is added to the <textarea>, 'paste' event should be fired once"); 280 is(beforeInputEvents.length, 1, 281 'One "beforeinput" event should be fired even if "click" event is canceled in bubbling phase'); 282 checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'even if "click" event is canceled in bubbling phase'); 283 is(inputEvents.length, 1, 284 'One "input" event should be fired even if "click" event is canceled in bubbling phase'); 285 checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'even if "click" event is canceled in bubbling phase'); 286 aTextarea.value = ""; 287 288 await copyPlaintext("abc"); 289 aTextarea.focus(); 290 aTextarea.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true}); 291 pasteEventCount = 0; 292 beforeInputEvents = []; 293 inputEvents = []; 294 synthesizeMouseAtCenter(aTextarea, {button: 1}); 295 is(aTextarea.value, "", 296 "If 'auxclick' event is consumed, paste should be canceled"); 297 is(pasteEventCount, 0, 298 "If 'auxclick' event is consumed, 'paste' event should not be fired once"); 299 is(beforeInputEvents.length, 0, 300 'No "beforeinput" event should be fired if "auxclick" event is canceled'); 301 is(inputEvents.length, 0, 302 'No "input" event should be fired if "auxclick" event is canceled'); 303 aTextarea.value = ""; 304 305 await copyPlaintext("abc"); 306 aTextarea.focus(); 307 aTextarea.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true}); 308 pasteEventCount = 0; 309 beforeInputEvents = []; 310 inputEvents = []; 311 synthesizeMouseAtCenter(aTextarea, {button: 1}); 312 is(aTextarea.value, "", 313 "If 'paste' event is consumed, paste should be canceled"); 314 is(pasteEventCount, 1, 315 'One "paste" event should be fired for making it possible to consume'); 316 is(beforeInputEvents.length, 0, 317 'No "beforeinput" event should be fired if "paste" event is canceled'); 318 is(inputEvents.length, 0, 319 'No "input" event should be fired if "paste" event is canceled'); 320 aTextarea.value = ""; 321 322 await copyPlaintext("abc"); 323 aTextarea.focus(); 324 aTextarea.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true}); 325 pasteEventCount = 0; 326 beforeInputEvents = []; 327 inputEvents = []; 328 synthesizeMouseAtCenter(aTextarea, {button: 1}); 329 is(aTextarea.value, "", 330 "If 'beforeinput' event is consumed, paste should be canceled"); 331 is(pasteEventCount, 1, 332 'One "paste" event should be fired before "beforeinput" event is consumed'); 333 is(beforeInputEvents.length, 1, 334 'One "beforeinput" event should be fired for making it possible to consume'); 335 checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'when "beforeinput" is canceled in bubbling phase'); 336 is(inputEvents.length, 0, 337 'No "input" event should be fired if "paste" event is canceled'); 338 aTextarea.value = ""; 339 340 aTextarea.removeEventListener("paste", pasteEventLogger); 341 aTextarea.removeEventListener("beforeinput", onBeforeInput); 342 aTextarea.removeEventListener("input", onInput); 343 } 344 345 async function doContenteditableTests(aEditableDiv) { 346 let beforeInputEvents = []; 347 let inputEvents = []; 348 let selectionRanges = []; 349 function onBeforeInput(aEvent) { 350 beforeInputEvents.push(aEvent); 351 let selection = document.getSelection(); 352 selectionRanges = []; 353 for (let i = 0; i < selection.rangeCount; i++) { 354 let range = selection.getRangeAt(i); 355 selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, 356 endContainer: range.endContainer, endOffset: range.endOffset}); 357 } 358 } 359 function onInput(aEvent) { 360 inputEvents.push(aEvent); 361 } 362 aEditableDiv.addEventListener("beforeinput", onBeforeInput); 363 aEditableDiv.addEventListener("input", onInput); 364 365 await copyPlaintext("abc\ndef\nghi"); 366 aEditableDiv.focus(); 367 beforeInputEvents = []; 368 inputEvents = []; 369 synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true}); 370 is(aEditableDiv.innerHTML, 371 "<blockquote type=\"cite\">abc<br>def<br>ghi</blockquote>", 372 "Pasted plaintext should be in <blockquote> element and each linebreaker should be <br> element"); 373 is(beforeInputEvents.length, 1, 374 'One "beforeinput" event should be fired on the editing host'); 375 checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", null, 376 [{type: "text/plain", data: "abc\ndef\nghi"}], selectionRanges, "(contenteditable)"); 377 is(inputEvents.length, 1, 378 'One "input" event should be fired on the editing host'); 379 checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", null, 380 [{type: "text/plain", data: "abc\ndef\nghi"}], [], "(contenteditable)"); 381 aEditableDiv.innerHTML = ""; 382 383 let pasteEventCount = 0; 384 function pasteEventLogger() { 385 pasteEventCount++; 386 } 387 aEditableDiv.addEventListener("paste", pasteEventLogger); 388 389 await copyPlaintext("abc"); 390 aEditableDiv.focus(); 391 window.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true}); 392 beforeInputEvents = []; 393 inputEvents = []; 394 synthesizeMouseAtCenter(aEditableDiv, {button: 1}); 395 is(aEditableDiv.innerHTML, "abc", 396 "If 'click' event is consumed at capturing phase of the window, paste should not be canceled"); 397 is(pasteEventCount, 1, 398 "If 'click' event is consumed at capturing phase of the window, 'paste' event should be fired once"); 399 is(beforeInputEvents.length, 1, 400 '"beforeinput" event should still be fired when the "click" event is canceled (contenteditable)'); 401 checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, 402 [{type: "text/plain", data: "abc"}], selectionRanges, 'when the "click" event is canceled (contenteditable)'); 403 is(inputEvents.length, 1, 404 '"input" event should still be fired when the "click" event is canceled (contenteditable)'); 405 checkInputEvent(inputEvents[0], "insertFromPaste", null, 406 [{type: "text/plain", data: "abc"}], [], 'when the "click" event is canceled (contenteditable)'); 407 aEditableDiv.innerHTML = ""; 408 409 await copyPlaintext("abc"); 410 aEditableDiv.focus(); 411 aEditableDiv.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true}); 412 pasteEventCount = 0; 413 beforeInputEvents = []; 414 inputEvents = []; 415 synthesizeMouseAtCenter(aEditableDiv, {button: 1}); 416 is(aEditableDiv.innerHTML, "abc", 417 "Even if 'mouseup' event is consumed, paste should be done"); 418 is(pasteEventCount, 1, 419 "Even if 'mouseup' event is consumed, 'paste' event should be fired once"); 420 is(beforeInputEvents.length, 1, 421 'One "beforeinput" event should be fired even if "mouseup" event is canceled (contenteditable)'); 422 checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges, 423 'even if "mouseup" event is canceled (contenteditable)'); 424 is(inputEvents.length, 1, 425 'One "input" event should be fired even if "mouseup" event is canceled (contenteditable)'); 426 checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], [], 427 'even if "mouseup" event is canceled (contenteditable)'); 428 aEditableDiv.innerHTML = ""; 429 430 await copyPlaintext("abc"); 431 aEditableDiv.focus(); 432 aEditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true}); 433 pasteEventCount = 0; 434 beforeInputEvents = []; 435 inputEvents = []; 436 synthesizeMouseAtCenter(aEditableDiv, {button: 1}); 437 is(aEditableDiv.innerHTML, "abc", 438 "Even if 'click' event handler is added to the editing host, paste should not be canceled"); 439 is(pasteEventCount, 1, 440 "Even if 'click' event handler is added to the editing host, 'paste' event should be fired"); 441 is(beforeInputEvents.length, 1, 442 'One "beforeinput" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)'); 443 checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges, 444 'even if "click" event is canceled in bubbling phase (contenteditable)'); 445 is(inputEvents.length, 1, 446 'One "input" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)'); 447 checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], [], 448 'even if "click" event is canceled in bubbling phase (contenteditable)'); 449 aEditableDiv.innerHTML = ""; 450 451 await copyPlaintext("abc"); 452 aEditableDiv.focus(); 453 aEditableDiv.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true}); 454 pasteEventCount = 0; 455 beforeInputEvents = []; 456 inputEvents = []; 457 synthesizeMouseAtCenter(aEditableDiv, {button: 1}); 458 is(aEditableDiv.innerHTML, "", 459 "If 'auxclick' event is consumed, paste should be canceled"); 460 is(pasteEventCount, 0, 461 "If 'auxclick' event is consumed, 'paste' event should not be fired"); 462 is(beforeInputEvents.length, 0, 463 'No "beforeinput" event should be fired if "auxclick" event is canceled (contenteditable)'); 464 is(inputEvents.length, 0, 465 'No "input" event should be fired if "auxclick" event is canceled (contenteditable)'); 466 aEditableDiv.innerHTML = ""; 467 468 await copyPlaintext("abc"); 469 aEditableDiv.focus(); 470 aEditableDiv.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true}); 471 pasteEventCount = 0; 472 beforeInputEvents = []; 473 inputEvents = []; 474 synthesizeMouseAtCenter(aEditableDiv, {button: 1}); 475 is(aEditableDiv.innerHTML, "", 476 "If 'paste' event is consumed, paste should be canceled"); 477 is(pasteEventCount, 1, 478 'One "paste" event should be fired for making it possible to consume'); 479 is(beforeInputEvents.length, 0, 480 'No "beforeinput" event should be fired if "paste" event is canceled (contenteditable)'); 481 is(inputEvents.length, 0, 482 'No "input" event should be fired if "paste" event is canceled (contenteditable)'); 483 aEditableDiv.innerHTML = ""; 484 485 await copyPlaintext("abc"); 486 aEditableDiv.focus(); 487 aEditableDiv.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true}); 488 pasteEventCount = 0; 489 beforeInputEvents = []; 490 inputEvents = []; 491 synthesizeMouseAtCenter(aEditableDiv, {button: 1}); 492 is(aEditableDiv.innerHTML, "", 493 "If 'paste' event is consumed, paste should be canceled"); 494 is(pasteEventCount, 1, 495 'One "paste" event should be fired before "beforeinput" event'); 496 is(beforeInputEvents.length, 1, 497 'One "beforeinput" event should be fired for making it possible to consume (contenteditable)'); 498 checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges, 499 'when "beforeinput" will be canceled (contenteditable)'); 500 is(inputEvents.length, 0, 501 'No "input" event should be fired if "beforeinput" event is canceled (contenteditable)'); 502 aEditableDiv.innerHTML = ""; 503 504 // If clipboard event is disabled, InputEvent.dataTransfer should have only empty string. 505 await SpecialPowers.pushPrefEnv({"set": [["dom.event.clipboardevents.enabled", false]]}); 506 await copyPlaintext("abc"); 507 aEditableDiv.focus(); 508 pasteEventCount = 0; 509 beforeInputEvents = []; 510 inputEvents = []; 511 synthesizeMouseAtCenter(aEditableDiv, {button: 1}); 512 is(aEditableDiv.innerHTML, "abc", 513 "Even if clipboard event is disabled, paste should be done"); 514 is(pasteEventCount, 0, 515 "If clipboard event is disabled, 'paste' event shouldn't be fired once"); 516 is(beforeInputEvents.length, 1, 517 'One "beforeinput" event should be fired even if clipboard event is disabled (contenteditable)'); 518 checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: ""}], selectionRanges, 519 "when clipboard event is disabled (contenteditable)"); 520 is(inputEvents.length, 1, 521 'One "input" event should be fired even if clipboard event is disabled (contenteditable)'); 522 checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: ""}], [], 523 "when clipboard event is disabled (contenteditable)"); 524 await SpecialPowers.pushPrefEnv({"set": [["dom.event.clipboardevents.enabled", true]]}); 525 aEditableDiv.innerHTML = ""; 526 527 aEditableDiv.removeEventListener("paste", pasteEventLogger); 528 529 // Oddly, copyHTMLContent fails randomly only on Linux. Let's skip this. 530 if (navigator.platform.startsWith("Linux")) { 531 aEditableDiv.removeEventListener("input", onInput); 532 return; 533 } 534 535 await copyHTMLContent("<p>abc</p><p>def</p><p>ghi</p>"); 536 aEditableDiv.focus(); 537 beforeInputEvents = []; 538 inputEvents = []; 539 synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true}); 540 if (!navigator.appVersion.includes("Android")) { 541 is(aEditableDiv.innerHTML, 542 "<blockquote type=\"cite\"><p>abc</p><p>def</p><p>ghi</p></blockquote>", 543 "Pasted HTML content should be set to the <blockquote>"); 544 } else { 545 // Oddly, on Android, we use <br> elements for pasting <p> elements. 546 is(aEditableDiv.innerHTML, 547 "<blockquote type=\"cite\">abc<br><br>def<br><br>ghi</blockquote>", 548 "Pasted HTML content should be set to the <blockquote>"); 549 } 550 // On windows, HTML clipboard includes extra data. 551 // The values are from widget/windows/nsDataObj.cpp. 552 const kHTMLPrefix = (navigator.platform.includes("Win")) ? kTextHtmlPrefixClipboardDataWindows : ""; 553 const kHTMLPostfix = (navigator.platform.includes("Win")) ? kTextHtmlSuffixClipboardDataWindows : ""; 554 is(beforeInputEvents.length, 1, 555 'One "beforeinput" event should be fired when pasting HTML'); 556 checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", null, 557 [{type: "text/html", 558 data: `${kHTMLPrefix}<p>abc</p><p>def</p><p>ghi</p>${kHTMLPostfix}`}], 559 selectionRanges, 560 "when pasting HTML"); 561 is(inputEvents.length, 1, 562 'One "input" event should be fired when pasting HTML'); 563 checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", null, 564 [{type: "text/html", 565 data: `${kHTMLPrefix}<p>abc</p><p>def</p><p>ghi</p>${kHTMLPostfix}`}], 566 [], 567 "when pasting HTML"); 568 aEditableDiv.innerHTML = ""; 569 570 aEditableDiv.removeEventListener("beforeinput", onBeforeInput); 571 aEditableDiv.removeEventListener("input", onInput); 572 } 573 574 async function doNestedEditorTests(aEditableDiv) { 575 await copyPlaintext("CLIPBOARD TEXT"); 576 aEditableDiv.innerHTML = '<p id="p">foo</p><textarea id="textarea"></textarea>'; 577 aEditableDiv.focus(); 578 let textarea = document.getElementById("textarea"); 579 let pasteTarget = null; 580 function onPaste(aEvent) { 581 pasteTarget = aEvent.target; 582 } 583 document.addEventListener("paste", onPaste); 584 585 synthesizeMouseAtCenter(textarea, {button: 1}); 586 is(pasteTarget.getAttribute("id"), "textarea", 587 "Target of 'paste' event should be the clicked <textarea>"); 588 is(textarea.value, "CLIPBOARD TEXT", 589 "Clicking in <textarea> in an editable <div> should paste the clipboard text into the <textarea>"); 590 is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea"></textarea>', 591 "Pasting in the <textarea> shouldn't be handled by the HTMLEditor"); 592 593 textarea.value = ""; 594 textarea.readOnly = true; 595 pasteTarget = null; 596 synthesizeMouseAtCenter(textarea, {button: 1}); 597 is(pasteTarget, textarea, 598 "Target of 'paste' event should be the clicked <textarea> even if it's read-only"); 599 is(textarea.value, "", 600 "Clicking in read-only <textarea> in an editable <div> should not paste the clipboard text into the read-only <textarea>"); 601 // HTMLEditor thinks that read-only <textarea> is not modifiable. 602 // Therefore, HTMLEditor does not paste the text. 603 is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea" readonly=""></textarea>', 604 "Clicking in read-only <textarea> shouldn't cause pasting the clipboard text into its parent HTMLEditor"); 605 606 textarea.value = ""; 607 textarea.readOnly = false; 608 textarea.disabled = true; 609 pasteTarget = null; 610 synthesizeMouseAtCenter(textarea, {button: 1}); 611 // Although, this compares with <textarea>, I'm not sure it's proper event 612 // target because of disabled <textarea>. 613 todo_is(pasteTarget, textarea, 614 "Target of 'paste' event should be the clicked <textarea> even if it's disabled"); 615 is(textarea.value, "", 616 "Clicking in disabled <textarea> in an editable <div> should not paste the clipboard text into the disabled <textarea>"); 617 // HTMLEditor thinks that disabled <textarea> is not modifiable. 618 // Therefore, HTMLEditor does not paste the text. 619 is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea" disabled=""></textarea>', 620 "Clicking in disabled <textarea> shouldn't cause pasting the clipboard text into its parent HTMLEditor"); 621 622 document.removeEventListener("paste", onPaste); 623 aEditableDiv.innerHTML = ""; 624 } 625 626 async function doAfterRemoveOfClickedElementTest(aEditableDiv) { 627 await copyPlaintext("CLIPBOARD TEXT"); 628 aEditableDiv.innerHTML = '<p id="p">foo<span id="span">bar</span></p>'; 629 aEditableDiv.focus(); 630 let span = document.getElementById("span"); 631 let pasteTarget = null; 632 document.addEventListener("paste", (aEvent) => { pasteTarget = aEvent.target; }, {once: true}); 633 document.addEventListener("auxclick", (aEvent) => { 634 is(aEvent.target.getAttribute("id"), "span", 635 "Target of auxclick event should be the <span> element"); 636 span.parentElement.removeChild(span); 637 }, {once: true}); 638 synthesizeMouseAtCenter(span, {button: 1}); 639 is(pasteTarget.getAttribute("id"), "p", 640 "Target of 'paste' event should be the <p> element since <span> has gone"); 641 // XXX Currently, pasted to start of the <p> because EventStateManager 642 // do not recompute event target frame. 643 todo_is(aEditableDiv.innerHTML, '<p id="p">fooCLIPBOARD TEXT</p>', 644 "Clipbpard text should looks like replacing the <span> element"); 645 aEditableDiv.innerHTML = ""; 646 } 647 648 async function doNotStartAutoscrollInContentEditable(aEditableDiv) { 649 await SpecialPowers.pushPrefEnv({"set": [["general.autoScroll", true]]}); 650 await copyPlaintext("CLIPBOARD TEXT"); 651 aEditableDiv.innerHTML = '<p id="p">foo<span id="span">bar</span></p>'; 652 aEditableDiv.focus(); 653 let span = document.getElementById("span"); 654 synthesizeMouseAtCenter(span, {button: 1}); 655 ok(aEditableDiv.innerHTML.includes("CLIPBOARD TEXT"), 656 "Clipbpard text should be inserted"); 657 aEditableDiv.innerHTML = ""; 658 } 659 660 async function doTests() { 661 await SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", true], 662 ["middlemouse.contentLoadURL", false], 663 ["dom.event.clipboardevents.enabled", true]]}); 664 let container = document.getElementById("container"); 665 container.innerHTML = "<textarea id=\"editor\"></textarea>"; 666 await doTextareaTests(document.getElementById("editor")); 667 container.innerHTML = "<div id=\"editor\" contenteditable style=\"min-height: 1em;\"></div>"; 668 await doContenteditableTests(document.getElementById("editor")); 669 await doNestedEditorTests(document.getElementById("editor")); 670 await doAfterRemoveOfClickedElementTest(document.getElementById("editor")); 671 // NOTE: The following test sets `general.autoScroll` to true. 672 await doNotStartAutoscrollInContentEditable(document.getElementById("editor")); 673 SimpleTest.finish(); 674 } 675 676 SimpleTest.waitForFocus(doTests); 677 </script> 678 </pre> 679 </body> 680 </html>