diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /editor/libeditor/tests/test_dragdrop.html | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | editor/libeditor/tests/test_dragdrop.html | 3451 |
1 files changed, 3451 insertions, 0 deletions
diff --git a/editor/libeditor/tests/test_dragdrop.html b/editor/libeditor/tests/test_dragdrop.html new file mode 100644 index 0000000000..6295661faa --- /dev/null +++ b/editor/libeditor/tests/test_dragdrop.html @@ -0,0 +1,3451 @@ +<!doctype html> +<html> + +<head> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <div id="dropZone" + ondragenter="event.dataTransfer.dropEffect = 'copy'; event.preventDefault();" + ondragover="event.dataTransfer.dropEffect = 'copy'; event.preventDefault();" + ondrop="event.preventDefault();" + style="height: 4px; background-color: lemonchiffon;"></div> + <div id="container"></div> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function checkInputEvent(aEvent, aExpectedTarget, aInputType, aData, aDataTransfer, aTargetRanges, aDescription) { + ok(aEvent instanceof InputEvent, `${aDescription}: "${aEvent.type}" event should be dispatched with InputEvent interface`); + is(aEvent.cancelable, aEvent.type === "beforeinput", `${aDescription}: "${aEvent.type}" event should be ${aEvent.type === "beforeinput" ? "" : "never "}cancelable`); + is(aEvent.bubbles, true, `${aDescription}: "${aEvent.type}" event should always bubble`); + is(aEvent.target, aExpectedTarget, `${aDescription}: "${aEvent.type}" event should be fired on the <${aExpectedTarget.tagName.toLowerCase()}> element`); + is(aEvent.inputType, aInputType, `${aDescription}: inputType of "${aEvent.type}" event should be "${aInputType}" on the <${aExpectedTarget.tagName.toLowerCase()}> element`); + is(aEvent.data, aData, `${aDescription}: data of "${aEvent.type}" event should be ${aData} on the <${aExpectedTarget.tagName.toLowerCase()}> element`); + if (aDataTransfer === null) { + is(aEvent.dataTransfer, null, `${aDescription}: dataTransfer should be null on the <${aExpectedTarget.tagName.toLowerCase()}> element`); + } else { + for (let dataTransfer of aDataTransfer) { + let description = `${aDescription}: on the <${aExpectedTarget.tagName.toLowerCase()}> element`; + if (dataTransfer.todo) { + // XXX It seems that synthesizeDrop() don't emulate perfectly if caller specifies the data directly. + todo_is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data, + `${description}: dataTransfer of "${aEvent.type}" event should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`); + } else { + is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data, + `${description}: dataTransfer of "${aEvent.type}" event should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`); + } + } + } + let targetRanges = aEvent.getTargetRanges(); + if (aTargetRanges.length === 0) { + is(targetRanges.length, 0, + `${aDescription}: getTargetRange() of "${aEvent.type}" event should return empty array`); + } else { + is(targetRanges.length, aTargetRanges.length, + `${aDescription}: getTargetRange() of "${aEvent.type}" event should return static range array`); + if (targetRanges.length == aTargetRanges.length) { + for (let i = 0; i < targetRanges.length; i++) { + is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, + `${aDescription}: startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); + is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, + `${aDescription}: startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); + is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, + `${aDescription}: endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); + is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, + `${aDescription}: endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); + } + } + } +} + +// eslint-disable-next-line complexity +async function doTest() { + const container = document.getElementById("container"); + const dropZone = document.getElementById("dropZone"); + + let beforeinputEvents = []; + let inputEvents = []; + let dragEvents = []; + function onBeforeinput(event) { + beforeinputEvents.push(event); + } + function onInput(event) { + inputEvents.push(event); + } + document.addEventListener("beforeinput", onBeforeinput); + document.addEventListener("input", onInput); + + function preventDefaultDeleteByDrag(aEvent) { + if (aEvent.inputType === "deleteByDrag") { + aEvent.preventDefault(); + } + } + function preventDefaultInsertFromDrop(aEvent) { + if (aEvent.inputType === "insertFromDrop") { + aEvent.preventDefault(); + } + } + + const selection = window.getSelection(); + + const kIsMac = navigator.platform.includes("Mac"); + const kIsWin = navigator.platform.includes("Win"); + + const kNativeLF = kIsWin ? "\r\n" : "\n"; + + const kModifiersToCopy = { + ctrlKey: !kIsMac, + altKey: kIsMac, + } + + function comparePlainText(aGot, aExpected, aDescription) { + is(aGot.replace(/\r\n?/g, "\n"), aExpected, aDescription); + } + function compareHTML(aGot, aExpected, aDescription) { + is(aGot.replace(/\r\n?/g, "\n"), aExpected, aDescription); + } + + async function trySynthesizePlainDragAndDrop(aDescription, aOptions) { + try { + await synthesizePlainDragAndDrop(aOptions); + return true; + } catch (e) { + ok(false, `${aDescription}: Failed to emulate drag and drop (${e.message})`); + return false; + } + } + + // -------- Test dragging regular text + await (async function test_dragging_regular_text() { + const description = "dragging part of non-editable <span> element"; + container.innerHTML = '<span style="font-size: 24px;">Some Text</span>'; + const span = document.querySelector("div#container > span"); + selection.setBaseAndExtent(span.firstChild, 4, span.firstChild, 6); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(4, 6), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(4, 6)}<`), + `${description}: dataTransfer should have the parent inline element and only selected text as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging non-editable selection to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging non-editable selection to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <input> + await (async function test_dragging_text_from_input_element() { + const description = "dragging part of text in <input> element"; + container.innerHTML = '<input value="Drag Me">'; + const input = document.querySelector("div#container > input"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + input.setSelectionRange(1, 4); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + input.value.substring(1, 4), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <textarea> + await (async function test_dragging_text_from_textarea_element() { + const description = "dragging part of text in <textarea> element"; + container.innerHTML = "<textarea>Some Text To Drag</textarea>"; + const textarea = document.querySelector("div#container > textarea"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + textarea.setSelectionRange(1, 7); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + textarea.value.substring(1, 7), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from a contenteditable + await (async function test_dragging_text_from_contenteditable() { + const description = "dragging part of text in contenteditable element"; + container.innerHTML = "<p contenteditable>This is some <b>editable</b> text.</p>"; + const b = document.querySelector("div#container > p > b"); + selection.setBaseAndExtent(b.firstChild, 2, b.firstChild, 6); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + b.textContent.substring(2, 6), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + b.outerHTML.replace(/>.+</, `>${b.textContent.substring(2, 6)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("drop", onDrop); + })(); + + + for (const inputType of ["text", "search"]) { + // -------- Test dragging regular text of text/html to <input> + await (async function test_dragging_text_from_span_element_to_input_element() { + const description = `dragging text in non-editable <span> to <input type=${inputType}>`; + container.innerHTML = `<span>Static</span><input type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(2, 5), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(input.value, span.textContent.substring(2, 5), + `${description}: <input>.value should be modified`); + is(beforeinputEvents.length, 1, + `${description}: one "beforeinput" event should be fired on <input>`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(inputEvents.length, 1, + `${description}: one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <input>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging regular text of text/html to disabled <input> + await (async function test_dragging_text_from_span_element_to_disabled_input_element() { + const description = `dragging text in non-editable <span> to <input disabled type="${inputType}">`; + container.innerHTML = `<span>Static</span><input disabled type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(input.value, "", + `${description}: <input disable>.value should not be modified`); + is(beforeinputEvents.length, 0, + `${description}: no "beforeinput" event should be fired on <input disabled>`); + is(inputEvents.length, 0, + `${description}: no "input" event should be fired on <input disabled>`); + is(dragEvents.length, 0, + `${description}: no "drop" event should be fired on <input disabled>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging regular text of text/html to readonly <input> + await (async function test_dragging_text_from_span_element_to_readonly_input_element() { + const description = `dragging text in non-editable <span> to <input readonly type="${inputType}">`; + container.innerHTML = `<span>Static</span><input readonly type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(2, 5), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(input.value, "", + `${description}: <input readonly>.value should not be modified`); + is(beforeinputEvents.length, 0, + `${description}: no "beforeinput" event should be fired on <input readonly>`); + is(inputEvents.length, 0, + `${description}: no "input" event should be fired on <input readonly>`); + is(dragEvents.length, 0, + `${description}: no "drop" event should be fired on <input readonly>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging only text/html data (like from another app) to <input>. + await (async function test_dragging_only_html_text_to_input_element() { + const description = `dragging only text/html data to <input type="${inputType}>`; + container.innerHTML = `<span>Static</span><input type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/html data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}]], "copy"); + is(beforeinputEvents.length, 0, + `${description}: no "beforeinput" event should be fired on <input>`); + is(inputEvents.length, 0, + `${description}: no "input" event should be fired on <input>`); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging both text/plain and text/html data (like from another app) to <input>. + await (async function test_dragging_both_html_text_and_plain_text_to_input_element() { + const description = `dragging both text/plain and text/html data to <input type=${inputType}>`; + container.innerHTML = `<span>Static</span><input type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/plain data and text/html data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}, + {type: "text/plain", data: "Some Plain Text"}]], "copy"); + is(input.value, "Some Plain Text", + `${description}: The text/plain data should be inserted`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on <input> element`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", "Some Plain Text", null, [], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on <input> element`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", "Some Plain Text", null, [], + description); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging special text type from another app to <input> + await (async function test_dragging_only_moz_text_internal_to_input_element() { + const description = `dragging both text/x-moz-text-internal data to <input type="${inputType}">`; + container.innerHTML = `<span>Static</span><input type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/x-moz-text-internal data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, input, [[{type: "text/x-moz-text-internal", data: "Some Special Text"}]], "copy"); + is(input.value, "", + `${description}: <input>.value should not be modified with "text/x-moz-text-internal" data`); + // Note that even if editor does not handle given dataTransfer, web apps + // may handle it by itself. Therefore, editor should dispatch "beforeinput" + // event. + is(beforeinputEvents.length, 1, + `${description}: one "beforeinput" event should be fired when dropping "text/x-moz-text-internal" data into <input> element`); + // But unfortunately, on <input> and <textarea>, dataTransfer won't be set... + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", "", null, [], description); + is(inputEvents.length, 0, + `${description}: no "input" event should be fired when dropping "text/x-moz-text-internal" data into <input> element`); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging contenteditable to <input> + await (async function test_dragging_from_contenteditable_to_input_element() { + const description = `dragging text in contenteditable to <input type="${inputType}">`; + container.innerHTML = `<div contenteditable>Some <b>bold</b> text</div><input type="${inputType}">`; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(contenteditable.innerHTML, "Soext", + `${description}: Dragged range should be removed from contenteditable`); + is(input.value, "me bold t", + `${description}: <input>.value should be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable and <input>`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to <input> (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_input_element_and_canceling_delete_by_drag() { + const description = `dragging text in contenteditable to <input type="${inputType}"> (canceling "deleteByDrag")`; + container.innerHTML = `<div contenteditable>Some <b>bold</b> text</div><input type="${inputType}">`; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(contenteditable.innerHTML, "Some <b>bold</b> text", + `${description}: Dragged range shouldn't be removed from contenteditable`); + is(input.value, "me bold t", + `${description}: <input>.value should be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", "me bold t", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <input>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to <input> (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_input_element_and_canceling_insert_from_drop() { + const description = `dragging text in contenteditable to <input type="${inputType}"> (canceling "insertFromDrop")`; + container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><input>"; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(contenteditable.innerHTML, "Soext", + `${description}: Dragged range should be removed from contenteditable`); + is(input.value, "", + `${description}: <input>.value shouldn't be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <input>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + } + + // -------- Test dragging regular text of text/html to <input type="number"> + // + // FIXME(emilio): The -moz-appearance bit is just a hack to + // work around bug 1611720. + await (async function test_dragging_from_span_element_to_input_element_whose_type_number() { + const description = `dragging text in non-editable <span> to <input type="number">`; + container.innerHTML = `<span>123456</span><input type="number" style="-moz-appearance: textfield">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(2, 5), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(input.value, span.textContent.substring(2, 5), + `${description}: <input>.value should be modified`); + is(beforeinputEvents.length, 1, + `${description}: one "beforeinput" event should be fired on <input>`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(inputEvents.length, 1, + `${description}: one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <input>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging only text/plain data (like from another app) to contenteditable. + await (async function test_dragging_only_plain_text_to_contenteditable() { + const description = "dragging both text/plain and text/html data to contenteditable"; + container.innerHTML = '<span>Static</span><div contenteditable style="min-height: 3em;"></div>'; + const span = document.querySelector("div#container > span"); + const contenteditable = document.querySelector("div#container > div"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/plain data and text/html data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, contenteditable, [[{type: "text/plain", data: "Sample Text"}]], "copy"); + is(contenteditable.innerHTML, "Sample Text", + `${description}: The text/plain data should be inserted`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable element`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{todo: true, type: "text/plain", data: "Sample Text"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable element`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{todo: true, type: "text/plain", data: "Sample Text"}], + [], + description); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging only text/html data (like from another app) to contenteditable. + await (async function test_dragging_only_html_text_to_contenteditable() { + const description = "dragging only text/html data to contenteditable"; + container.innerHTML = '<span>Static</span><div contenteditable style="min-height: 3em;"></div>'; + const span = document.querySelector("div#container > span"); + const contenteditable = document.querySelector("div#container > div"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/plain data and text/html data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, contenteditable, [[{type: "text/html", data: "Sample <i>Italic</i> Text"}]], "copy"); + is(contenteditable.innerHTML, "Sample <i>Italic</i> Text", + `${description}: The text/plain data should be inserted`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable element`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{todo: true, type: "text/html", data: "Sample <i>Italic</i> Text"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable element`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{todo: true, type: "text/html", data: "Sample <i>Italic</i> Text"}], + [], + description); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging regular text of text/plain to <textarea> + await (async function test_dragging_from_span_element_to_textarea_element() { + const description = "dragging text in non-editable <span> to <textarea>"; + container.innerHTML = "<span>Static</span><textarea></textarea>"; + const span = document.querySelector("div#container > span"); + const textarea = document.querySelector("div#container > textarea"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(2, 5), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(textarea.value, span.textContent.substring(2, 5), + `${description}: <textarea>.value should be modified`); + is(beforeinputEvents.length, 1, + `${description}: one "beforeinput" event should be fired on <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(inputEvents.length, 1, + `${description}: one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + + // -------- Test dragging contenteditable to <textarea> + await (async function test_dragging_contenteditable_to_textarea_element() { + const description = "dragging text in contenteditable to <textarea>"; + container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(contenteditable.innerHTML, "Soext", + `${description}: Dragged range should be removed from contenteditable`); + is(textarea.value, "me bold t", + `${description}: <textarea>.value should be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable and <textarea>`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to <textarea> (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_textarea_and_canceling_delete_by_drag() { + const description = 'dragging text in contenteditable to <textarea> (canceling "deleteByDrag")'; + container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(contenteditable.innerHTML, "Some <b>bold</b> text", + `${description}: Dragged range shouldn't be removed from contenteditable`); + is(textarea.value, "me bold t", + `${description}: <textarea>.value should be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "me bold t", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to <textarea> (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_textarea_and_canceling_insert_from_drop() { + const description = 'dragging text in contenteditable to <textarea> (canceling "insertFromDrop")'; + container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(contenteditable.innerHTML, "Soext", + `${description}: Dragged range should be removed from contenteditable`); + is(textarea.value, "", + `${description}: <textarea>.value shouldn't be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test dragging contenteditable to same contenteditable + await (async function test_dragging_from_contenteditable_to_itself() { + const description = "dragging text in contenteditable to same contenteditable"; + container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const span = document.querySelector("div#container > div > span"); + const lastTextNode = span.firstChild; + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + } + ) + ) { + todo_is(contenteditable.innerHTML, "<b>bd</b> <span>MM<b>ol</b>MM</span>", + `${description}: dragged range should be removed from contenteditable`); + todo_isnot(contenteditable.innerHTML, "<b>bd</b> <span>MMMM</span><b>ol</b>", + `${description}: dragged range should be removed from contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: lastTextNode, startOffset: 4, + endContainer: lastTextNode, endOffset: 4}], // XXX unexpected + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to same contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_itself_and_canceling_delete_by_drag() { + const description = 'dragging text in contenteditable to same contenteditable (canceling "deleteByDrag")'; + container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const span = document.querySelector("div#container > div > span"); + const lastTextNode = span.firstChild; + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + } + ) + ) { + todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM<b>ol</b>MM</span>", + `${description}: dragged range shouldn't be removed from contenteditable`); + todo_isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>", + `${description}: dragged range shouldn't be removed from contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: lastTextNode, startOffset: 4, + endContainer: lastTextNode, endOffset: 4}], // XXX unexpected + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to same contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_itself_and_canceling_insert_from_drop() { + const description = 'dragging text in contenteditable to same contenteditable (canceling "insertFromDrop")'; + container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const span = document.querySelector("div#container > div > span"); + const lastTextNode = span.firstChild; + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bd</b> <span>MMMM</span>", + `${description}: dragged range should be removed from contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: lastTextNode, startOffset: 4, + endContainer: lastTextNode, endOffset: 4}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging contenteditable to same contenteditable + await (async function test_copy_dragging_from_contenteditable_to_itself() { + const description = "copy-dragging text in contenteditable to same contenteditable"; + container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; + document.documentElement.scrollTop; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const span = document.querySelector("div#container > div > span"); + const lastTextNode = span.firstChild; + selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + dragEvent: kModifiersToCopy, + } + ) + ) { + todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM<b>ol</b>MM</span>", + `${description}: dragged range shouldn't be removed from contenteditable`); + todo_isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>", + `${description}: dragged range shouldn't be removed from contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only 1 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: lastTextNode, startOffset: 4, + endContainer: lastTextNode, endOffset: 4}], // XXX unexpected + description); + is(inputEvents.length, 1, + `${description}: only 1 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to other contenteditable + await (async function test_dragging_from_contenteditable_to_other_contenteditable() { + const description = "dragging text in contenteditable to other contenteditable"; + container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bd</b>", + `${description}: dragged range should be removed from contenteditable`); + is(otherContenteditable.innerHTML, "<b>ol</b>", + `${description}: dragged content should be inserted into other contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to other contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_other_contenteditable_and_canceling_delete_by_drag() { + const description = 'dragging text in contenteditable to other contenteditable (canceling "deleteByDrag")'; + container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bold</b>", + `${description}: dragged range shouldn't be removed from contenteditable`); + is(otherContenteditable.innerHTML, "<b>ol</b>", + `${description}: dragged content should be inserted into other contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on other contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to other contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_other_contenteditable_and_canceling_insert_from_drop() { + const description = 'dragging text in contenteditable to other contenteditable (canceling "insertFromDrop")'; + container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bd</b>", + `${description}: dragged range should be removed from contenteditable`); + is(otherContenteditable.innerHTML, "", + `${description}: dragged content shouldn't be inserted into other contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging contenteditable to other contenteditable + await (async function test_copy_dragging_from_contenteditable_to_other_contenteditable() { + const description = "copy-dragging text in contenteditable to other contenteditable"; + container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bold</b>", + `${description}: dragged range shouldn't be removed from contenteditable`); + is(otherContenteditable.innerHTML, "<b>ol</b>", + `${description}: dragged content should be inserted into other contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on other contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on other contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging nested contenteditable to contenteditable + await (async function test_dragging_from_nested_contenteditable_to_contenteditable() { + const description = "dragging text in nested contenteditable to contenteditable"; + container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + const b = document.querySelector("div#container > div > div > p > b"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: contenteditable.firstChild, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bd</b></p></div>', + `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging nested contenteditable to contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_nested_contenteditable_to_contenteditable_canceling_delete_by_drag() { + const description = 'dragging text in nested contenteditable to contenteditable (canceling "deleteByDrag")'; + container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + const b = document.querySelector("div#container > div > div > p > b"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: contenteditable.firstChild, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bold</b></p></div>', + `${description}: dragged range should be copied from nested contenteditable to the contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging nested contenteditable to contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_nested_contenteditable_to_contenteditable_canceling_insert_from_drop() { + const description = 'dragging text in nested contenteditable to contenteditable (canceling "insertFromDrop")'; + container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + const b = document.querySelector("div#container > div > div > p > b"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: contenteditable.firstChild, + } + ) + ) { + is(contenteditable.innerHTML, '<p><br></p><div contenteditable="false"><p contenteditable=""><b>bd</b></p></div>', + `${description}: dragged range should be removed from nested contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on nested contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging nested contenteditable to contenteditable + await (async function test_copy_dragging_from_nested_contenteditable_to_contenteditable() { + const description = "copy-dragging text in nested contenteditable to contenteditable"; + container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > div > p > b"); + contenteditable.focus(); + selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: contenteditable.firstChild, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bold</b></p></div>', + `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to nested contenteditable + await (async function test_dragging_from_contenteditable_to_nested_contenteditable() { + const description = "dragging text in contenteditable to nested contenteditable"; + container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > p > b"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>bd</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', + `${description}: dragged range should be moved from contenteditable to nested contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable and nested contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to nested contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_nested_contenteditable_and_canceling_delete_by_drag() { + const description = 'dragging text in contenteditable to nested contenteditable (canceling "deleteByDrag")'; + container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > p > b"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>bold</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', + `${description}: dragged range should be copied from contenteditable to nested contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable and nested contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to nested contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_nested_contenteditable_and_canceling_insert_from_drop() { + const description = 'dragging text in contenteditable to nested contenteditable (canceling "insertFromDrop")'; + container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > p > b"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>bd</b></p><div contenteditable="false"><p contenteditable=""><br></p></div>', + `${description}: dragged range should be removed from contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging contenteditable to nested contenteditable + await (async function test_copy_dragging_from_contenteditable_to_nested_contenteditable() { + const description = "copy-dragging text in contenteditable to nested contenteditable"; + container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > p > b"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + contenteditable.focus(); + selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>bold</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', + `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to contenteditable + await (async function test_dragging_from_input_element_to_contenteditable() { + const description = "dragging text in <input> to contenteditable"; + container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const contenteditable = document.querySelector("div#container > div"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: contenteditable, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(contenteditable.innerHTML, "e Tex<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and contenteditable`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_input_element_to_contenteditable_and_canceling_delete_by_drag() { + const description = 'dragging text in <input> to contenteditable (canceling "deleteByDrag")'; + container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const contenteditable = document.querySelector("div#container > div"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: contenteditable, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(contenteditable.innerHTML, "e Tex<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <input> to contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_input_element_to_contenteditable_and_canceling_insert_from_drop() { + const description = 'dragging text in <input> to contenteditable (canceling "insertFromDrop")'; + container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const contenteditable = document.querySelector("div#container > div"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: contenteditable, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(contenteditable.innerHTML, "<br>", + `${description}: dragged content shouldn't be inserted into contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <input> to contenteditable + await (async function test_copy_dragging_from_input_element_to_contenteditable() { + const description = "copy-dragging text in <input> to contenteditable"; + container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const contenteditable = document.querySelector("div#container > div"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: contenteditable, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(contenteditable.innerHTML, "e Tex<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to contenteditable + await (async function test_dragging_from_textarea_element_to_contenteditable() { + const description = "dragging text in <textarea> to contenteditable"; + container.innerHTML = '<textarea>Line1\nLine2</textarea><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const contenteditable = document.querySelector("div#container > div"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: contenteditable, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + todo_is(contenteditable.innerHTML, "<div>e1</div><div>Li</div>", + `${description}: dragged content should be inserted into contenteditable`); + todo_isnot(contenteditable.innerHTML, "e1<br>Li<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: `e1${kNativeLF}Li`}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and contenteditable`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: `e1${kNativeLF}Li`}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test copy-dragging text in <textarea> to contenteditable + await (async function test_copy_dragging_from_textarea_element_to_contenteditable() { + const description = "copy-dragging text in <textarea> to contenteditable"; + container.innerHTML = '<textarea>Line1\nLine2</textarea><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const contenteditable = document.querySelector("div#container > div"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: contenteditable, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range should be removed from <textarea>`); + todo_is(contenteditable.innerHTML, "<div>e1</div><div>Li</div>", + `${description}: dragged content should be inserted into contenteditable`); + todo_isnot(contenteditable.innerHTML, "e1<br>Li<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: `e1${kNativeLF}Li`}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: `e1${kNativeLF}Li`}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to other <input> + await (async function test_dragging_from_input_element_to_other_input_element() { + const description = "dragging text in <input> to other <input>"; + container.innerHTML = '<input value="Some Text"><input>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const otherInput = document.querySelector("div#container > input + input"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: otherInput, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(otherInput.value, "e Tex", + `${description}: dragged content should be inserted into other <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and other <input>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other <input>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to other <input> (canceling "deleteByDrag") + await (async function test_dragging_from_input_element_to_other_input_element_and_canceling_delete_by_drag() { + const description = 'dragging text in <input> to other <input> (canceling "deleteByDrag")'; + container.innerHTML = '<input value="Some Text"><input>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const otherInput = document.querySelector("div#container > input + input"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: otherInput, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(otherInput.value, "e Tex", + `${description}: dragged content should be inserted into other <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on other <input>`); + checkInputEvent(inputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other <input>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <input> to other <input> (canceling "insertFromDrop") + await (async function test_dragging_from_input_element_to_other_input_element_and_canceling_insert_from_drop() { + const description = 'dragging text in <input> to other <input> (canceling "insertFromDrop")'; + container.innerHTML = '<input value="Some Text"><input>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const otherInput = document.querySelector("div#container > input + input"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: otherInput, + }, + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(otherInput.value, "", + `${description}: dragged content shouldn't be inserted into other <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other <input>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <input> to other <input> + await (async function test_copy_dragging_from_input_element_to_other_input_element() { + const description = "copy-dragging text in <input> to other <input>"; + container.innerHTML = '<input value="Some Text"><input>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const otherInput = document.querySelector("div#container > input + input"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: otherInput, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(otherInput.value, "e Tex", + `${description}: dragged content should be inserted into other <input>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on other <input>`); + checkInputEvent(beforeinputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on other <input>`); + checkInputEvent(inputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other <input>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to <textarea> + await (async function test_dragging_from_input_element_to_textarea_element() { + const description = "dragging text in <input> to other <textarea>"; + container.innerHTML = '<input value="Some Text"><textarea></textarea>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const textarea = document.querySelector("div#container > textarea"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: textarea, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(textarea.value, "e Tex", + `${description}: dragged content should be inserted into <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and <textarea>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to <textarea> (canceling "deleteByDrag") + await (async function test_dragging_from_input_element_to_textarea_element_and_canceling_delete_by_drag() { + const description = 'dragging text in <input> to other <textarea> (canceling "deleteByDrag")'; + container.innerHTML = '<input value="Some Text"><textarea></textarea>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const textarea = document.querySelector("div#container > textarea"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: textarea, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(textarea.value, "e Tex", + `${description}: dragged content should be inserted into <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <input> to <textarea> (canceling "insertFromDrop") + await (async function test_dragging_from_input_element_to_textarea_element_and_canceling_insert_from_drop() { + const description = 'dragging text in <input> to other <textarea> (canceling "insertFromDrop")'; + container.innerHTML = '<input value="Some Text"><textarea></textarea>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const textarea = document.querySelector("div#container > textarea"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: textarea, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(textarea.value, "", + `${description}: dragged content shouldn't be inserted into <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <input> to <textarea> + await (async function test_copy_dragging_from_input_element_to_textarea_element() { + const description = "copy-dragging text in <input> to <textarea>"; + container.innerHTML = '<input value="Some Text"><textarea></textarea>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const textarea = document.querySelector("div#container > textarea"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: textarea, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(textarea.value, "e Tex", + `${description}: dragged content should be inserted into <textarea>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to <input> + await (async function test_dragging_from_textarea_element_to_input_element() { + const description = "dragging text in <textarea> to <input>"; + container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const input = document.querySelector("div#container > input"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: input, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + is(input.value, "e1 Li", + `${description}: dragged content should be inserted into <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <textarea> and <input>`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to <input> (canceling "deleteByDrag") + await (async function test_dragging_from_textarea_element_to_input_element_and_delete_by_drag() { + const description = 'dragging text in <textarea> to <input> (canceling "deleteByDrag")'; + container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const input = document.querySelector("div#container > input"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: input, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range shouldn't be removed from <textarea>`); + is(input.value, "e1 Li", + `${description}: dragged content should be inserted into <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <textarea> to <input> (canceling "insertFromDrop") + await (async function test_dragging_from_textarea_element_to_input_element_and_canceling_insert_from_drop() { + const description = 'dragging text in <textarea> to <input> (canceling "insertFromDrop")'; + container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const input = document.querySelector("div#container > input"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: input, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + is(input.value, "", + `${description}: dragged content shouldn't be inserted into <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <textarea> to <input> + await (async function test_copy_dragging_from_textarea_element_to_input_element() { + const description = "copy-dragging text in <textarea> to <input>"; + container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const input = document.querySelector("div#container > input"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: input, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range shouldn't be removed from <textarea>`); + is(input.value, "e1 Li", + `${description}: dragged content should be inserted into <input>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on <input>`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to other <textarea> + await (async function test_dragging_from_textarea_element_to_other_textarea_element() { + const description = "dragging text in <textarea> to other <textarea>"; + container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const otherTextarea = document.querySelector("div#container > textarea + textarea"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: otherTextarea, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + is(otherTextarea.value, "e1\nLi", + `${description}: dragged content should be inserted into other <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <textarea> and other <textarea>`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to other <textarea> (canceling "deleteByDrag") + await (async function test_dragging_from_textarea_element_to_other_textarea_element_and_canceling_delete_by_drag() { + const description = 'dragging text in <textarea> to other <textarea> (canceling "deleteByDrag")'; + container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const otherTextarea = document.querySelector("div#container > textarea + textarea"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: otherTextarea, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range shouldn't be removed from <textarea>`); + is(otherTextarea.value, "e1\nLi", + `${description}: dragged content should be inserted into other <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on other <textarea>`); + checkInputEvent(inputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <textarea> to other <textarea> (canceling "insertFromDrop") + await (async function test_dragging_from_textarea_element_to_other_textarea_element_and_canceling_insert_from_drop() { + const description = 'dragging text in <textarea> to other <textarea> (canceling "insertFromDrop")'; + container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const otherTextarea = document.querySelector("div#container > textarea + textarea"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: otherTextarea, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + is(otherTextarea.value, "", + `${description}: dragged content shouldn't be inserted into other <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <textarea> to other <textarea> + await (async function test_copy_dragging_from_textarea_element_to_other_textarea_element() { + const description = "copy-dragging text in <textarea> to other <textarea>"; + container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const otherTextarea = document.querySelector("div#container > textarea + textarea"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: otherTextarea, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range shouldn't be removed from <textarea>`); + is(otherTextarea.value, "e1\nLi", + `${description}: dragged content should be inserted into other <textarea>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on other <textarea>`); + checkInputEvent(beforeinputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on other <textarea>`); + checkInputEvent(inputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging multiple-line text in contenteditable to <input> + await (async function test_dragging_multiple_line_text_in_contenteditable_to_input_element() { + const description = "dragging multiple-line text in contenteditable to <input>"; + container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><input>'; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + const selectionContainers = [contenteditable.firstChild.firstChild, contenteditable.firstChild.nextSibling.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 3, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", + `${description}: dataTransfer should have have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(contenteditable.innerHTML, "<div>Linne2</div>", + `${description}: dragged content should be removed from contenteditable`); + is(input.value, "e1 Li", + `${description}: dragged range should be inserted into <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 3, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test copy-dragging multiple-line text in contenteditable to <input> + await (async function test_copy_dragging_multiple_line_text_in_contenteditable_to_input_element() { + const description = "copy-dragging multiple-line text in contenteditable to <input>"; + container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><input>'; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3, + contenteditable.firstChild.nextSibling.firstChild, 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", + `${description}: dataTransfer should have have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, "<div>Line1</div><div>Line2</div>", + `${description}: dragged content should be removed from contenteditable`); + is(input.value, "e1 Li", + `${description}: dragged range should be inserted into <input>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging multiple-line text in contenteditable to <textarea> + await (async function test_dragging_multiple_line_text_in_contenteditable_to_textarea_element() { + const description = "dragging multiple-line text in contenteditable to <textarea>"; + container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><textarea></textarea>'; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + const selectionContainers = [contenteditable.firstChild.firstChild, contenteditable.firstChild.nextSibling.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 3, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", + `${description}: dataTransfer should have have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(contenteditable.innerHTML, "<div>Linne2</div>", + `${description}: dragged content should be removed from contenteditable`); + is(textarea.value, "e1\nLi", + `${description}: dragged range should be inserted into <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 3, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <textarea> and contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test copy-dragging multiple-line text in contenteditable to <textarea> + await (async function test_copy_dragging_multiple_line_text_in_contenteditable_to_textarea_element() { + const description = "copy-dragging multiple-line text in contenteditable to <textarea>"; + container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><textarea></textarea>'; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3, + contenteditable.firstChild.nextSibling.firstChild, 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", + `${description}: dataTransfer should have have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, "<div>Line1</div><div>Line2</div>", + `${description}: dragged content should be removed from contenteditable`); + is(textarea.value, "e1\nLi", + `${description}: dragged range should be inserted into <textarea>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <input> and reframing the <input> element before dragend. + await (async function test_dragging_from_input_element_and_reframing_input_element() { + const description = "dragging part of text in <input> element and reframing the <input> element before dragend"; + container.innerHTML = '<input value="Drag Me">'; + const input = document.querySelector("div#container > input"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + input.setSelectionRange(1, 4); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragStart = aEvent => { + input.style.display = "none"; + document.documentElement.scrollTop; + input.style.display = ""; + document.documentElement.scrollTop; + }; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + input.value.substring(1, 4), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("dragStart", onDragStart); + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("dragStart", onDragStart); + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <textarea> and reframing the <textarea> element before dragend. + await (async function test_dragging_from_textarea_element_and_reframing_textarea_element() { + const description = "dragging part of text in <textarea> element and reframing the <textarea> element before dragend"; + container.innerHTML = "<textarea>Some Text To Drag</textarea>"; + const textarea = document.querySelector("div#container > textarea"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + textarea.setSelectionRange(1, 7); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragStart = aEvent => { + textarea.style.display = "none"; + document.documentElement.scrollTop; + textarea.style.display = ""; + document.documentElement.scrollTop; + }; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + textarea.value.substring(1, 7), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("dragStart", onDragStart); + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("dragStart", onDragStart); + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <input> and reframing the <input> element before dragstart. + await (async function test_dragging_from_input_element_and_reframing_input_element_before_dragstart() { + const description = "dragging part of text in <input> element and reframing the <input> element before dragstart"; + container.innerHTML = '<input value="Drag Me">'; + const input = document.querySelector("div#container > input"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + input.setSelectionRange(1, 4); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onMouseMove = aEvent => { + input.style.display = "none"; + document.documentElement.scrollTop; + input.style.display = ""; + document.documentElement.scrollTop; + }; + const onMouseDown = aEvent => { + document.addEventListener("mousemove", onMouseMove, {once: true}); + } + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + input.value.substring(1, 4), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("mousedown", onMouseDown, {once: true}); + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <textarea> and reframing the <textarea> element before dragstart. + await (async function test_dragging_from_textarea_element_and_reframing_textarea_element_before_dragstart() { + const description = "dragging part of text in <textarea> element and reframing the <textarea> element before dragstart"; + container.innerHTML = "<textarea>Some Text To Drag</textarea>"; + const textarea = document.querySelector("div#container > textarea"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + textarea.setSelectionRange(1, 7); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onMouseMove = aEvent => { + textarea.style.display = "none"; + document.documentElement.scrollTop; + textarea.style.display = ""; + document.documentElement.scrollTop; + }; + const onMouseDown = aEvent => { + document.addEventListener("mousemove", onMouseMove, {once: true}); + } + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + textarea.value.substring(1, 7), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("mousedown", onMouseDown, {once: true}); + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("drop", onDrop); + })(); + + await (async function test_dragend_when_left_half_of_text_node_dragged_into_textarea() { + const description = "dragging left half of text in contenteditable into <textarea>"; + container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; + const editingHost = container.querySelector("[contenteditable]"); + const textNode = editingHost.querySelector("p").firstChild; + const textarea = container.querySelector("textarea"); + selection.setBaseAndExtent(textNode, 0, textNode, textNode.length / 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragEnd = aEvent => dragEvents.push(aEvent); + document.addEventListener("dragend", onDragEnd, {capture: true}); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + ok( + textNode.isConnected, + `${description}: the text node part of whose text is dragged should not be removed` + ); + is( + dragEvents.length, + 1, + `${description}: only one "dragend" event should be fired` + ); + is( + dragEvents[0]?.target, + textNode, + `${description}: "dragend" should be fired on the text node which the mouse button down on` + ); + } + document.removeEventListener("dragend", onDragEnd, {capture: true}); + })(); + + await (async function test_dragend_when_right_half_of_text_node_dragged_into_textarea() { + const description = "dragging right half of text in contenteditable into <textarea>"; + container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; + const editingHost = container.querySelector("[contenteditable]"); + const textNode = editingHost.querySelector("p").firstChild; + const textarea = container.querySelector("textarea"); + selection.setBaseAndExtent(textNode, textNode.length / 2, textNode, textNode.length); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragEnd = aEvent => dragEvents.push(aEvent); + document.addEventListener("dragend", onDragEnd, {capture: true}); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + ok( + textNode.isConnected, + `${description}: the text node part of whose text is dragged should not be removed` + ); + is( + dragEvents.length, + 1, + `${description}: only one "dragend" event should be fired` + ); + is( + dragEvents[0]?.target, + textNode, + `${description}: "dragend" should be fired on the text node which the mouse button down on` + ); + } + document.removeEventListener("dragend", onDragEnd, {capture: true}); + })(); + + await (async function test_dragend_when_middle_part_of_text_node_dragged_into_textarea() { + const description = "dragging middle of text in contenteditable into <textarea>"; + container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; + const editingHost = container.querySelector("[contenteditable]"); + const textNode = editingHost.querySelector("p").firstChild; + const textarea = container.querySelector("textarea"); + selection.setBaseAndExtent(textNode, "ab".length, textNode, "abcd".length); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragEnd = aEvent => dragEvents.push(aEvent); + document.addEventListener("dragend", onDragEnd, {capture: true}); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + ok( + textNode.isConnected, + `${description}: the text node part of whose text is dragged should not be removed` + ); + is( + dragEvents.length, + 1, + `${description}: only one "dragend" event should be fired` + ); + is( + dragEvents[0]?.target, + textNode, + `${description}: "dragend" should be fired on the text node which the mouse button down on` + ); + } + document.removeEventListener("dragend", onDragEnd, {capture: true}); + })(); + + await (async function test_dragend_when_all_of_text_node_dragged_into_textarea() { + const description = "dragging all of text in contenteditable into <textarea>"; + container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; + const editingHost = container.querySelector("[contenteditable]"); + const textNode = editingHost.querySelector("p").firstChild; + const textarea = container.querySelector("textarea"); + selection.setBaseAndExtent(textNode, 0, textNode, textNode.length); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragEnd = aEvent => dragEvents.push(aEvent); + document.addEventListener("dragend", onDragEnd, {capture: true}); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + ok( + !textNode.isConnected, + `${description}: the text node whose all text is dragged should've been removed from the contenteditable` + ); + is( + dragEvents.length, + 1, + `${description}: only one "dragend" event should be fired` + ); + is( + dragEvents[0]?.target, + editingHost, + `${description}: "dragend" should be fired on the editing host which is parent of the removed text node` + ); + } + document.removeEventListener("dragend", onDragEnd, {capture: true}); + })(); + + document.removeEventListener("beforeinput", onBeforeinput); + document.removeEventListener("input", onInput); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTest); + +</script> +</body> +</html> |