diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/editing/other | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/editing/other')
65 files changed, 15532 insertions, 0 deletions
diff --git a/testing/web-platform/tests/editing/other/body-should-not-deleted-even-if-empty.html b/testing/web-platform/tests/editing/other/body-should-not-deleted-even-if-empty.html new file mode 100644 index 0000000000..050e780a26 --- /dev/null +++ b/testing/web-platform/tests/editing/other/body-should-not-deleted-even-if-empty.html @@ -0,0 +1,51 @@ +<html><head> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +</head><body><script> +"use strict" + +document.designMode = "on"; + +test(() => { + document.querySelector("script")?.remove(); + document.head?.remove(); + document.body.firstChild?.remove(); + document.body.appendChild(document.createElement("p")); + getSelection().collapse(document.querySelector("p"), 0); + document.querySelector("p").firstChild?.remove(); + document.execCommand("delete"); + assert_in_array( + document.documentElement?.outerHTML.replace(/\n/g, ""), + [ + "<html><body></body></html>", + "<html><body><br></body></html>", + "<html><body><p></p></body></html>", + "<html><body><p><br></p></body></html>" + ], + "Body element shouldn't be deleted even if it becomes empty" + ); +}, "Delete in empty paragraph shouldn't delete parent body and html elements even if they become empty by Backspace"); + +test(() => { + document.querySelector("script")?.remove(); + document.head?.remove(); + document.body.firstChild?.remove(); + document.body.appendChild(document.createElement("p")); + getSelection().collapse(document.querySelector("p"), 0); + document.querySelector("p").firstChild?.remove(); + document.execCommand("delete"); + assert_in_array( + document.documentElement?.outerHTML.replace(/\n/g, ""), + [ + "<html><body></body></html>", + "<html><body><br></body></html>", + "<html><body><p></p></body></html>", + "<html><body><p><br></p></body></html>" + ], + "Body element shouldn't be deleted even if it becomes empty" + ); + + document.designMode = "off"; +}, "Delete in empty paragraph shouldn't delete parent body and html elements even if they become empty by Delete"); + +</script></body></html>
\ No newline at end of file diff --git a/testing/web-platform/tests/editing/other/cefalse-boundaries-deletion.html b/testing/web-platform/tests/editing/other/cefalse-boundaries-deletion.html new file mode 100644 index 0000000000..de793bd6a3 --- /dev/null +++ b/testing/web-platform/tests/editing/other/cefalse-boundaries-deletion.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Selecting and deleting all from the cE=true element with cE=false element at the beginning</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../include/editor-test-utils.js"></script> + +<div contenteditable></div> + +<script> + const utils = new EditorTestUtils( document.querySelector( 'div[contenteditable]' ) ); + + test( () => { + utils.setupEditingHost( `<div contenteditable="false" id="cefalse-beginning"> </div> + <p id="paragraph-beginning">Lorem ipsum dolor sit amet.</p>` ); + + const cefalse = document.querySelector( '#cefalse-beginning' ); + const paragraph = document.querySelector( '#paragraph-beginning' ); + + utils.editingHost.focus(); + document.execCommand( 'selectAll' ); + document.execCommand( 'delete' ); + + assert_false( cefalse.isConnected, 'cE=false element should be removed' ); + assert_false( paragraph.isConnected, 'paragraph should be removed' ); + }, 'cE=false elements can be removed from the beginning of the cE=true elements' ); + + test( () => { + utils.setupEditingHost( `<p id="paragraph-end">Lorem ipsum dolor sit amet.</p> + <div contenteditable="false" id="cefalse-end"> </div>` ); + + const cefalse = document.querySelector( '#cefalse-end' ); + const paragraph = document.querySelector( '#paragraph-end' ); + + utils.editingHost.focus(); + document.execCommand( 'selectAll' ); + document.execCommand( 'delete' ); + + assert_false( cefalse.isConnected, 'cE=false element should be removed' ); + assert_false( paragraph.isConnected, 'paragraph should be removed' ); + }, 'cE=false elements can be removed from the end of the cE=true elements' ); + + test( () => { + utils.setupEditingHost( `<div contenteditable="false" id="cefalse-boundaries-beginning"> </div> + <p id="paragraph-boundaries">Lorem ipsum dolor sit amet.</p> + <div contenteditable="false" id="cefalse-boundaries-end"> </div>` ); + + const cefalseBeginning = document.querySelector( '#cefalse-boundaries-beginning' ); + const cefalseEnd = document.querySelector( '#cefalse-boundaries-end' ); + const paragraph = document.querySelector( '#paragraph-boundaries' ); + + utils.editingHost.focus(); + document.execCommand( 'selectAll' ); + document.execCommand( 'delete' ); + + assert_false( cefalseBeginning.isConnected, 'cE=false element at the beginning should be removed' ); + assert_false( cefalseEnd.isConnected, 'cE=false element at the end should be removed' ); + assert_false( paragraph.isConnected, 'paragraph should be removed' ); + }, 'cE=false elements can be removed from the boundaries of the cE=true elements' ); +</script> diff --git a/testing/web-platform/tests/editing/other/cloning-attributes-at-splitting-element.tentative.html b/testing/web-platform/tests/editing/other/cloning-attributes-at-splitting-element.tentative.html new file mode 100644 index 0000000000..3c0b1c9344 --- /dev/null +++ b/testing/web-platform/tests/editing/other/cloning-attributes-at-splitting-element.tentative.html @@ -0,0 +1,522 @@ +<!doctype html> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<title>Cloning attributes at splitting an element in contenteditable</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<div contenteditable></div> +<script> +"use strict"; + +document.execCommand("defaultParagraphSeparator", false, "div"); +const utils = + new EditorTestUtils(document.querySelector("div[contenteditable]")); + +// DO NOT USE multi-line comment in this file, then, you can comment out +// unnecessary tests when you need to attach the browser with a debugger. + +// When an element is being split, all attributes except id attribute should be +// cloned to the new element. +promise_test(async t => { + utils.setupEditingHost(`<div id="splittee">abc[]def</div>`); + const splittee = document.getElementById("splittee"); + await utils.sendEnterKey(); + test(() => { + assert_equals( + document.getElementById("splittee"), + splittee, + `The element instance returned by Document.getElementById shouldn't be changed after splitting the element (${t.name})` + ); + }); + test(() => { + assert_equals( + document.querySelectorAll("[id=splittee]").length, + 1, + `The new element created by splitting an element shouldn't have same id attribute value (${t.name})` + ); + }); +}, "Splitting <div id=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<div class="splittee">abc[]def</div>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("div"); + const rightNode = utils.editingHost.querySelector("div + div"); + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "splittee", + `The left element should keep having the class attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "splittee", + `The right element should keep having the class attribute (${t.name})` + ); + }); +}, "Splitting <div class=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<div data-foo="1" data-bar="2">abc[]def</div>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("div"); + const rightNode = utils.editingHost.querySelector("div + div"); + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "1", + `The left element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + leftNode.getAttribute("data-bar"), + "2", + `The left element should keep having the data-bar attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-foo"), + "1", + `The right element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + rightNode.getAttribute("data-bar"), + "2", + `The right element should keep having the data-bar attribute (${t.name})` + ); + }); +}, "Splitting <div data-foo=\"1\" data-bar=\"2\">"); + +// Same tests for list items since browsers may use different path to handle +// splitting a list item. +promise_test(async t => { + utils.setupEditingHost(`<ul><li id="splittee">abc[]def</li></ul>`); + const splittee = document.getElementById("splittee"); + await utils.sendEnterKey(); + test(() => { + assert_equals( + document.getElementById("splittee"), + splittee, + `The element instance returned by Document.getElementById shouldn't be changed after splitting the element (${t.name})` + ); + }); + test(() => { + assert_equals( + document.querySelectorAll("[id=splittee]").length, + 1, + `The new element created by splitting an element shouldn't have same id attribute value (${t.name})` + ); + }); +}, "Splitting <li id=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<ul><li class="splittee">abc[]def</li></ul>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("li"); + const rightNode = utils.editingHost.querySelector("li + li"); + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "splittee", + `The left element should keep having the class attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "splittee", + `The right element should keep having the class attribute (${t.name})` + ); + }); +}, "Splitting <li class=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<ul><li data-foo="1" data-bar="2">abc[]def</li></ul>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("li"); + const rightNode = utils.editingHost.querySelector("li + li"); + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "1", + `The left element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + leftNode.getAttribute("data-bar"), + "2", + `The left element should keep having the data-bar attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-foo"), + "1", + `The right element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + rightNode.getAttribute("data-bar"), + "2", + `The right element should keep having the data-bar attribute (${t.name})` + ); + }); +}, "Splitting <li data-foo=\"1\" data-bar=\"2\">"); + +// Same tests for heading since browsers may use different path to handle +// splitting a heading element. +promise_test(async t => { + utils.setupEditingHost(`<h3 id="p">abc[]def</h3>`); + const splittee = document.getElementById("splittee"); + await utils.sendEnterKey(); + test(() => { + assert_equals( + document.getElementById("splittee"), + splittee, + `The element instance returned by Document.getElementById shouldn't be changed after splitting the element (${t.name})` + ); + }); + test(() => { + assert_equals( + document.querySelectorAll("[id=p]").length, + 1, + `The new element created by splitting an element shouldn't have same id attribute value (${t.name})` + ); + }); +}, "Splitting <h3 id=\"p\">"); + +promise_test(async t => { + utils.setupEditingHost(`<h3 class="splittee">abc[]def</h3>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("h3"); + const rightNode = leftNode.nextSibling; + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "splittee", + `The left element should keep having the class attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "splittee", + `The right element should keep having the class attribute (${t.name})` + ); + }); +}, "Splitting <h3 class=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<h3 data-foo="1" data-bar="2">abc[]def</h3>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("h3"); + const rightNode = leftNode.nextSibling; + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "1", + `The left element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + leftNode.getAttribute("data-bar"), + "2", + `The left element should keep having the data-bar attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-foo"), + "1", + `The right element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + rightNode.getAttribute("data-bar"), + "2", + `The right element should keep having the data-bar attribute (${t.name})` + ); + }); +}, "Splitting <h3 data-foo=\"1\" data-bar=\"2\">"); + +// Same tests for <dt> since browsers may use different path to handle +// splitting a <dt>. +promise_test(async t => { + utils.setupEditingHost(`<dl><dt id="splittee">abc[]def</dt></dl>`); + const splittee = document.getElementById("splittee"); + await utils.sendEnterKey(); + test(() => { + assert_equals( + document.getElementById("splittee"), + splittee, + `The element instance returned by Document.getElementById shouldn't be changed after splitting the element (${t.name})` + ); + }); + test(() => { + assert_equals( + document.querySelectorAll("[id=splittee]").length, + 1, + `The new element created by splitting an element shouldn't have same id attribute value (${t.name})` + ); + }); +}, "Splitting <dt id=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<dl><dt class="splittee">abc[]def</dt></dl>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("dt"); + const rightNode = leftNode.nextSibling; + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "splittee", + `The left element should keep having the class attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "splittee", + `The right element should keep having the class attribute (${t.name})` + ); + }); +}, "Splitting <dt class=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<dl><dt data-foo="1" data-bar="2">abc[]def</dt></dl>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("dt"); + const rightNode = leftNode.nextSibling; + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "1", + `The left element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + leftNode.getAttribute("data-bar"), + "2", + `The left element should keep having the data-bar attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-foo"), + "1", + `The right element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + rightNode.getAttribute("data-bar"), + "2", + `The right element should keep having the data-bar attribute (${t.name})` + ); + }); +}, "Splitting <dt data-foo=\"1\" data-bar=\"2\">"); + +// Same tests for <dd> since browsers may use different path to handle +// splitting a <dd>. +promise_test(async t => { + utils.setupEditingHost(`<dl><dd id="splittee">abc[]def</dd></dl>`); + const splittee = document.getElementById("splittee"); + await utils.sendEnterKey(); + test(() => { + assert_equals( + document.getElementById("splittee"), + splittee, + `The element instance returned by Document.getElementById shouldn't be changed after splitting the element (${t.name})` + ); + }); + test(() => { + assert_equals( + document.querySelectorAll("[id=splittee]").length, + 1, + `The new element created by splitting an element shouldn't have same id attribute value (${t.name})` + ); + }); +}, "Splitting <dd id=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<dl><dd class="splittee">abc[]def</dd></dl>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("dd"); + const rightNode = leftNode.nextSibling; + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "splittee", + `The left element should keep having the class attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "splittee", + `The right element should keep having the class attribute (${t.name})` + ); + }); +}, "Splitting <dd class=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<dl><dd data-foo="1" data-bar="2">abc[]def</dd></dl>`); + await utils.sendEnterKey(); + const leftNode = utils.editingHost.querySelector("dd"); + const rightNode = leftNode.nextSibling; + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "1", + `The left element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + leftNode.getAttribute("data-bar"), + "2", + `The left element should keep having the data-bar attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-foo"), + "1", + `The right element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + rightNode.getAttribute("data-bar"), + "2", + `The right element should keep having the data-bar attribute (${t.name})` + ); + }); +}, "Splitting <dd data-foo=\"1\" data-bar=\"2\">"); + +// Same tests for inline elements. +promise_test(async t => { + utils.setupEditingHost(`<div id="splittee-parent"><span id="splittee">abc[]def</span></div>`); + const splittee = document.getElementById("splittee"); + const splitteeParent = document.getElementById("splittee-parent"); + await utils.sendEnterKey(); + test(() => { + assert_equals( + document.getElementById("splittee"), + splittee, + `The element instance returned by Document.getElementById shouldn't be changed after splitting the element (${t.name})` + ); + }); + test(() => { + assert_equals( + document.getElementById("splittee-parent"), + splitteeParent, + `The element instance returned by Document.getElementById shouldn't be changed after splitting the element (splittee-parent) (${t.name})` + ); + }); + test(() => { + assert_equals( + document.querySelectorAll("[id=splittee]").length, + 1, + `The new element created by splitting an element shouldn't have same id attribute value (${t.name})` + ); + }); + test(() => { + assert_equals( + document.querySelectorAll("[id=splittee-parent]").length, + 1, + `The new element created by splitting an element shouldn't have same id attribute value (splittee-parent) (${t.name})` + ); + }); +}, "Splitting <div id=\"splittee-parent\"> and <span id=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<div class="splittee-parent"><span class="splittee">abc[]def</span></div>`); + await utils.sendEnterKey(); + const leftParent = utils.editingHost.querySelector("div"); + const leftNode = leftParent.querySelector("span"); + const rightParent = utils.editingHost.querySelector("div + div"); + const rightNode = rightParent.querySelector("span"); + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "splittee", + `The left element should keep having the class attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + leftParent.getAttribute("class"), + "splittee-parent", + `The left element should keep having the class attribute (splittee-parent) (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "splittee", + `The right element should keep having the class attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightParent.getAttribute("class"), + "splittee-parent", + `The right element should keep having the class attribute (splittee-parent) (${t.name})` + ); + }); +}, "Splitting <div class=\"splittee-parent\"> and <span class=\"splittee\">"); + +promise_test(async t => { + utils.setupEditingHost(`<div data-foo="1" data-bar="2"><span data-foo="3" data-bar="4">abc[]def</span></div>`); + await utils.sendEnterKey(); + const leftParent = utils.editingHost.querySelector("div"); + const leftNode = leftParent.querySelector("span"); + const rightParent = utils.editingHost.querySelector("div + div"); + const rightNode = rightParent.querySelector("span"); + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "3", + `The left element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + leftNode.getAttribute("data-bar"), + "4", + `The left element should keep having the data-bar attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + leftParent.getAttribute("data-foo"), + "1", + `The left element should keep having the data-foo attribute (splittee-parent) (${t.name})` + ); + assert_equals( + leftParent.getAttribute("data-bar"), + "2", + `The left element should keep having the data-bar attribute (splittee-parent) (${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-foo"), + "3", + `The right element should keep having the data-foo attribute (${t.name})` + ); + assert_equals( + rightNode.getAttribute("data-bar"), + "4", + `The right element should keep having the data-bar attribute (${t.name})` + ); + }); + test(() => { + assert_equals( + rightParent.getAttribute("data-foo"), + "1", + `The right element should keep having the data-foo attribute (splittee-parent) (${t.name})` + ); + assert_equals( + rightParent.getAttribute("data-bar"), + "2", + `The right element should keep having the data-bar attribute (splittee-parent) (${t.name})` + ); + }); +}, "Splitting <div data-foo=\"1\" data-bar=\"2\"> and <span data-foo=\"3\" data-bar=\"4\">"); + +</script> diff --git a/testing/web-platform/tests/editing/other/delete-in-child-of-head.tentative.html b/testing/web-platform/tests/editing/other/delete-in-child-of-head.tentative.html new file mode 100644 index 0000000000..978cf83d47 --- /dev/null +++ b/testing/web-platform/tests/editing/other/delete-in-child-of-head.tentative.html @@ -0,0 +1,418 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?designMode=off&method=backspace"> +<meta name="variant" content="?designMode=off&method=forwarddelete"> +<meta name="variant" content="?designMode=on&method=backspace"> +<meta name="variant" content="?designMode=on&method=forwarddelete"> +<title>Join paragraphs in the head element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<iframe srcdoc=""></iframe> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const testingBackspace = searchParams.get("method") == "backspace"; +const commandName = testingBackspace ? "delete" : "forwarddelete"; +const testingDesignMode = searchParams.get("designMode") == "on"; + +const iframe = document.querySelector("iframe"); +const minimumSrcDoc = + "<html>" + + '<head style="display:block">' + + "<title>iframe</title>" + + "<script src='/resources/testdriver.js'></" + "script>" + + "<script src='/resources/testdriver-vendor.js'></" + "script>" + + "<script src='/resources/testdriver-actions.js'></" + "script>" + + "</head>" + + "<body><br></body>" + + "</html>"; + +async function initializeAndWaitForLoad(iframeElement, srcDocValue) { + const waitForLoad = + new Promise( + resolve => iframeElement.addEventListener("load", resolve, {once: true}) + ); + iframeElement.srcdoc = srcDocValue; + await waitForLoad; + if (testingDesignMode) { + iframeElement.contentDocument.designMode = "on"; + } else { + iframeElement.contentDocument.documentElement.setAttribute("contenteditable", ""); + } + iframeElement.contentWindow.focus(); + iframeElement.contentDocument.execCommand("defaultParagraphSeparator", false, "div"); +} + +function removeResourceScriptElements(node) { + node.querySelectorAll("script").forEach( + element => { + if (element.getAttribute("src")?.startsWith("/resources")) { + element.remove() + } + } + ); +} + +// DO NOT USE multi-line comment in this file, then, you can comment out +// unnecessary tests when you need to attach the browser with a debugger. + +// For backward compatibility, normal block elements in <head> should be +// joined by deletion. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div1 = childDoc.createElement("div"); + div1.innerHTML = "abc"; + const div2 = childDoc.createElement("div"); + div2.innerHTML = "def"; + childDoc.head.appendChild(div1); + childDoc.head.appendChild(div2); + // Now: <head><title>...</title><div>abc</div><div>def</div></head>... + childDoc.getSelection().collapse( + testingBackspace ? div2.firstChild : div1.firstChild, + testingBackspace ? 0 : div1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title><div>abcdef</div></head><body><br></body>', + '<head><title>iframe</title><div>abcdef<br></div></head><body><br></body>', + ], + "The <div> elements should be merged" + ); + assert_equals( + div1.isConnected ^ div2.isConnected, + 1, + "One <div> element should be removed, and the other should stay" + ); +}, `${commandName} in <div> elements in <head> should join them`); + +// The following void elements shouldn't be deleted for avoiding various +// affection to the document. +for (const tag of ["meta", "title", "style", "script", "link", "base", "template"]) { + promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div1 = childDoc.createElement("div"); + div1.innerHTML = "abc"; + const div2 = childDoc.createElement("div"); + div2.innerHTML = "def"; + const element = childDoc.createElement(tag); + childDoc.head.appendChild(div1); + childDoc.head.appendChild(element); + childDoc.head.appendChild(div2); + // Now: <head><title>...</title><div>abc</div><tag/><div>def</div></head>... + childDoc.getSelection().collapse( + testingBackspace ? div2.firstChild : div1.firstChild, + testingBackspace ? 0 : div1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + + if (["title", "style", "script", "template"].includes(tag)) { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title><div>abcdef</div><${tag}></${tag}></head><body><br></body>`, + `<head><title>iframe</title><div>abcdef<br></div><${tag}></${tag}></head><body><br></body>`, + `<head><title>iframe</title><${tag}></${tag}><div>abcdef</div></head><body><br></body>`, + `<head><title>iframe</title><${tag}></${tag}><div>abcdef<br></div></head><body><br></body>`, + ], + `The <div> elements should be merged without deleting <${tag}>` + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title><div>abcdef</div><${tag}></head><body><br></body>`, + `<head><title>iframe</title><div>abcdef<br></div><${tag}></head><body><br></body>`, + `<head><title>iframe</title><${tag}><div>abcdef</div></head><body><br></body>`, + `<head><title>iframe</title><${tag}><div>abcdef<br></div></head><body><br></body>`, + ], + `The <div> elements should be merged without deleting <${tag}>` + ); + } + }, `${commandName} around invisible <${tag}> should not delete it at joining paragraphs`); +} + +// Visible <script>, <style>, <title> elements shouldn't be joined +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const script1 = childDoc.createElement("script"); + script1.innerHTML = "// abc"; + script1.setAttribute("style", "display:block"); + const script2 = childDoc.createElement("script"); + script2.innerHTML = "// def"; + script2.setAttribute("style", "display:block"); + childDoc.head.appendChild(script1); + childDoc.head.appendChild(script2); + // Now: <head><title>...</title><script>// abc</ script><script>// def</ script></head>... + childDoc.getSelection().collapse( + testingBackspace ? script2.firstChild : script1.firstChild, + testingBackspace ? 0 : script1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + script1.removeAttribute("style"); + script2.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><script>// abc</" + "script><script>// def</" + "script></head><body><br></body>", + "Visible <script> elements shouldn't be merged" + ); +}, `${commandName} in visible <script> elements in <head> should not join them`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const style1 = childDoc.createElement("style"); + style1.innerHTML = "abc"; + style1.setAttribute("style", "display:block"); + const style2 = childDoc.createElement("style"); + style2.innerHTML = "def"; + style2.setAttribute("style", "display:block"); + childDoc.head.appendChild(style1); + childDoc.head.appendChild(style2); + // Now: <head><title>...</title><style>abc</style><style>def</style></head>... + childDoc.getSelection().collapse( + testingBackspace ? style2.firstChild : style1.firstChild, + testingBackspace ? 0 : style1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + style1.removeAttribute("style"); + style2.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><style>abc</style><style>def</style></head><body><br></body>", + "Visible <style> elements shouldn't be merged" + ); +}, `${commandName} in visible <style> elements in <head> should not join them`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const title1 = childDoc.createElement("title"); + title1.innerHTML = "abc"; + title1.setAttribute("style", "display:block"); + const title2 = childDoc.createElement("title"); + title2.innerHTML = "def"; + title2.setAttribute("style", "display:block"); + childDoc.head.appendChild(title1); + childDoc.head.appendChild(title2); + // Now: <head><title>...</title><title>abc</title><title>def</title></head>... + childDoc.getSelection().collapse( + testingBackspace ? title2.firstChild : title1.firstChild, + testingBackspace ? 0 : title1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + title1.removeAttribute("style"); + title2.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><title>abc</title><title>def</title></head><body><br></body>", + "Visible <title> elements shouldn't be merged" + ); +}, `${commandName} in visible <title> elements in <head> should not join them`); + +// Visible <script>, <style>, <title> shouldn't be joined with following <div> +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const script = childDoc.createElement("script"); + script.innerHTML = "// abc"; + script.setAttribute("style", "display:block"); + const div = childDoc.createElement("div"); + div.innerHTML = "// def"; + childDoc.head.appendChild(script); + childDoc.head.appendChild(div); + // Now: <head><title>...</title><script>// abc</ script><div>// def</div></head>... + childDoc.getSelection().collapse( + testingBackspace ? div.firstChild : script.firstChild, + testingBackspace ? 0 : script.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + script.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><script>// abc</" + "script><div>// def</div></head><body><br></body>", + "Visible <script> and <div> shouldn't be merged" + ); +}, `${commandName} at boundary of <script> and <div> in <head> should not join them`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const style = childDoc.createElement("style"); + style.innerHTML = "abc"; + style.setAttribute("style", "display:block"); + const div = childDoc.createElement("div"); + div.innerHTML = "def"; + childDoc.head.appendChild(style); + childDoc.head.appendChild(div); + // Now: <head><title>...</title><style>abc</style><div>def</div></head>... + childDoc.getSelection().collapse( + testingBackspace ? div.firstChild : style.firstChild, + testingBackspace ? 0 : style.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + style.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><style>abc</style><div>def</div></head><body><br></body>", + "Visible <style> and <div> shouldn't be merged" + ); +}, `${commandName} at boundary of <style> and <div> in <head> should not join them`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const title = childDoc.createElement("title"); + title.innerHTML = "abc"; + title.setAttribute("title", "display:block"); + const div = childDoc.createElement("div"); + div.innerHTML = "def"; + childDoc.head.appendChild(title); + childDoc.head.appendChild(div); + // Now: <head><title>...</title><title>abc</title><div>def</div></head>... + childDoc.getSelection().collapse( + testingBackspace ? div.firstChild : title.firstChild, + testingBackspace ? 0 : title.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + title.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><title>abc</title><div>def</div></head><body><br></body>", + "Visible <title> and <div> shouldn't be merged" + ); +}, `${commandName} at boundary of <title> and <div> in <head> should not join them`); + +// Visible <script>, <style>, <title> shouldn't be joined with preceding <div> +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div = childDoc.createElement("div"); + div.innerHTML = "// abc"; + const script = childDoc.createElement("script"); + script.innerHTML = "// def"; + script.setAttribute("style", "display:block"); + childDoc.head.appendChild(div); + childDoc.head.appendChild(script); + // Now: <head><title>...</title><div>// abc</div><script>// def</ script></head>... + childDoc.getSelection().collapse( + testingBackspace ? script.firstChild : div.firstChild, + testingBackspace ? 0 : div.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + script.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><div>// abc</div><script>// def</" + "script></head><body><br></body>", + "<div> and visible <script> shouldn't be merged" + ); +}, `${commandName} at boundary of <div> and <script> in <head> should not join them`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div = childDoc.createElement("div"); + div.innerHTML = "abc"; + const style = childDoc.createElement("style"); + style.innerHTML = "def"; + style.setAttribute("style", "display:block"); + childDoc.head.appendChild(div); + childDoc.head.appendChild(style); + // Now: <head><title>...</title><div>abc</div><style>def</style></head>... + childDoc.getSelection().collapse( + testingBackspace ? style.firstChild : div.firstChild, + testingBackspace ? 0 : div.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + style.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><div>abc</div><style>def</style></head><body><br></body>", + "<div> and visible <style> shouldn't be merged" + ); +}, `${commandName} at boundary of <div> and <style> in <head> should not join them`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div = childDoc.createElement("div"); + div.innerHTML = "abc"; + const title = childDoc.createElement("title"); + title.innerHTML = "def"; + title.setAttribute("style", "display:block"); + childDoc.head.appendChild(div); + childDoc.head.appendChild(title); + // Now: <head><title>...</title><div>abc</div><title>def</title></head>... + childDoc.getSelection().collapse( + testingBackspace ? title.firstChild : div.firstChild, + testingBackspace ? 0 : div.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + title.removeAttribute("style"); + + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><div>abc</div><title>def</title></head><body><br></body>", + "<div> and visible <title> shouldn't be merged" + ); +}, `${commandName} at boundary of <div> and <title> in <head> should not join them`); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/delete-in-child-of-html.tentative.html b/testing/web-platform/tests/editing/other/delete-in-child-of-html.tentative.html new file mode 100644 index 0000000000..4ae5446d1b --- /dev/null +++ b/testing/web-platform/tests/editing/other/delete-in-child-of-html.tentative.html @@ -0,0 +1,449 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?designMode=off&method=backspace"> +<meta name="variant" content="?designMode=off&method=forwarddelete"> +<meta name="variant" content="?designMode=on&method=backspace"> +<meta name="variant" content="?designMode=on&method=forwarddelete"> +<title>Join paragraphs outside the body</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<iframe srcdoc=""></iframe> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const testingBackspace = searchParams.get("method") == "backspace"; +const commandName = testingBackspace ? "delete" : "forwarddelete"; +const testingDesignMode = searchParams.get("designMode") == "on"; + +const iframe = document.querySelector("iframe"); +const minimumSrcDoc = + "<html>" + + "<head>" + + "<title>iframe</title>" + + "<script src='/resources/testdriver.js'></" + "script>" + + "<script src='/resources/testdriver-vendor.js'></" + "script>" + + "<script src='/resources/testdriver-actions.js'></" + "script>" + + "</head>" + + "<body><br></body>" + + "</html>"; + +async function initializeAndWaitForLoad(iframeElement, srcDocValue) { + const waitForLoad = + new Promise( + resolve => iframeElement.addEventListener("load", resolve, {once: true}) + ); + iframeElement.srcdoc = srcDocValue; + await waitForLoad; + if (testingDesignMode) { + iframeElement.contentDocument.designMode = "on"; + } else { + iframeElement.contentDocument.documentElement.setAttribute("contenteditable", ""); + } + iframeElement.contentWindow.focus(); + iframeElement.contentDocument.execCommand("defaultParagraphSeparator", false, "div"); +} + +function removeResourceScriptElements(node) { + node.querySelectorAll("script").forEach( + element => { + if (element.getAttribute("src")?.startsWith("/resources")) { + element.remove() + } + } + ); +} + +// DO NOT USE multi-line comment in this file, then, you can comment out +// unnecessary tests when you need to attach the browser with a debugger. + +// For backward compatibility, normal block elements outside <body> should be +// joined by deletion. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div1 = childDoc.createElement("div"); + div1.innerHTML = "abc"; + const div2 = childDoc.createElement("div"); + div2.innerHTML = "def"; + childDoc.documentElement.appendChild(div1); + childDoc.documentElement.appendChild(div2); + // Now: </head><body><br></body><div>abc</div><div>def</div> + childDoc.getSelection().collapse( + testingBackspace ? div2.firstChild : div1.firstChild, + testingBackspace ? 0 : div1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title></head><body><br></body><div>abcdef</div>', + '<head><title>iframe</title></head><body><br></body><div>abcdef<br></div>', + ], + "The <div> elements should be merged" + ); + assert_equals( + div1.isConnected ^ div2.isConnected, + 1, + "One <div> element should be removed, and the other should stay" + ); +}, `${commandName} in <div> elements after <body> should join them`); + +// Deleting around end of the <body> should merge the element after the +// <body> into the <body>. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + childDoc.body.innerHTML = "abc"; + const div = childDoc.createElement("div"); + div.innerHTML = "def"; + childDoc.documentElement.appendChild(div); + // Now: </head><body>abc</body><div>def</div> + childDoc.getSelection().collapse( + testingBackspace ? div.firstChild : childDoc.body.firstChild, + testingBackspace ? 0 : childDoc.body.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title></head><body>abcdef</body>', + '<head><title>iframe</title></head><body>abcdef<br></body>', + ], + "The text should be merged" + ); + assert_false( + div.isConnected, + "The <div> following <body> should be removed" + ); +}, `${commandName} should merge <div> after <body> into the <body>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div1 = childDoc.createElement("div"); + div1.innerHTML = "abc"; + const div2 = childDoc.createElement("div"); + div2.innerHTML = "def"; + childDoc.body.innerHTML = ""; + childDoc.body.appendChild(div1); + childDoc.documentElement.appendChild(div2); + // Now: </head><body><div>abc</div></body><div>def</div> + childDoc.getSelection().collapse( + testingBackspace ? div2.firstChild : div1.firstChild, + testingBackspace ? 0 : div1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title></head><body><div>abcdef</div></body>', + '<head><title>iframe</title></head><body><div>abcdef<br></div></body>', + ], + "The <div> elements should be merged" + ); + assert_true( + !div2.isConnected || (div2.isConnected && div2.parentNode == childDoc.body), + "The <div> following <body> should be removed or moved into the <body>" + ); +}, `${commandName} should merge <div> after <body> into the <div> in the <body>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div = childDoc.createElement("div"); + div.innerHTML = "abc"; + childDoc.documentElement.appendChild(div); + // Now: </head><body><br></body><div>abc</div> + childDoc.getSelection().collapse( + testingBackspace ? div.firstChild : childDoc.body, + 0 + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title></head><body>abc</body>', + '<head><title>iframe</title></head><body>abc<br></body>', + ], + "The <div> element should be merged into the <body>" + ); + assert_false( + div.isConnected, + "The <div> element should be removed" + ); +}, `${commandName} should merge <div> after <body> into the empty <body>`); + +// Deleting around start of the <body> should merge the element before the +// <body> into the <body>. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div = childDoc.createElement("div"); + div.innerHTML = "abc"; + childDoc.body.innerHTML = "def"; + childDoc.documentElement.insertBefore(div, childDoc.body); + // Now: </head><div>abc</div><body>def</body> + childDoc.getSelection().collapse( + testingBackspace ? childDoc.body.firstChild : div.firstChild, + testingBackspace ? 0 : div.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title></head><body>abcdef</body>', + '<head><title>iframe</title></head><body>abcdef<br></body>', + ], + "The text should be merged" + ); + assert_false( + div.isConnected, + "The <div> following <body> should be removed" + ); +}, `${commandName} should merge <div> before <body> into the <body>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div1 = childDoc.createElement("div"); + div1.innerHTML = "abc"; + const div2 = childDoc.createElement("div"); + div2.innerHTML = "def"; + childDoc.documentElement.insertBefore(div1, childDoc.body); + childDoc.body.innerHTML = ""; + childDoc.body.appendChild(div2); + // Now: </head><div>abc</div><body><div>def</div></body> + childDoc.getSelection().collapse( + testingBackspace ? div2.firstChild : div1.firstChild, + testingBackspace ? 0 : div1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title></head><body><div>abcdef</div></body>', + '<head><title>iframe</title></head><body><div>abcdef<br></div></body>', + ], + "The <div> elements should be merged" + ); + assert_true( + !div2.isConnected || (div2.isConnected && div2.parentNode == childDoc.body), + "The <div> following <body> should be removed or moved into the <body>" + ); +}, `${commandName} should merge <div> before <body> into the <div> in the <body>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div = childDoc.createElement("div"); + div.innerHTML = "abc"; + childDoc.documentElement.insertBefore(div, childDoc.body); + // Now: </head><div>abc</div><body><br></body> + childDoc.getSelection().collapse( + testingBackspace ? childDoc.body : div.firstChild, + testingBackspace ? 0: div.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title></head><body>abc</body>', + '<head><title>iframe</title></head><body>abc<br></body>', + ], + "The <div> element should be merged into the <body>" + ); + assert_false( + div.isConnected, + "The <div> element should be removed" + ); +}, `${commandName} should merge <div> before <body> into the empty <body>`); + +// Deleting around end of the <head> should not delete the <head> element. +if (testingBackspace) { + promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div = childDoc.createElement("div"); + div.innerHTML = "abc"; + childDoc.body.innerHTML = "def"; + childDoc.documentElement.insertBefore(div, childDoc.body); + // Now: </head><div>abc</div><body>def</body> + childDoc.getSelection().collapse(div.firstChild, 0); + await utils.sendBackspaceKey(); + removeResourceScriptElements(childDoc); + assert_equals( + childDoc.documentElement.innerHTML, + '<head><title>iframe</title></head><div>abc</div><body>def</body>', + "The <div> element should be merged into the <body>" + ); + assert_true( + div.isConnected, + "The <div> element should not be removed" + ); + }, `delete from <div> following invisible <head> element shouldn't delete the <head> element`); +} + +// Joining elements around <head> element should not delete the <head> element. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const utils = new EditorTestUtils(childDoc.documentElement); + const div1 = childDoc.createElement("div"); + div1.innerHTML = "abc"; + const div2 = childDoc.createElement("div"); + div2.innerHTML = "def"; + childDoc.documentElement.insertBefore(div1, childDoc.head); + childDoc.documentElement.insertBefore(div2, childDoc.body); + // Now: <div>abc</div><head>...</head><div>def</div><body><br></body> + childDoc.getSelection().collapse( + testingBackspace ? div2.firstChild : div1.firstChild, + testingBackspace ? 0 : div1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<div>abcdef</div><head><title>iframe</title></head><body><br></body>', + '<div>abcdef<br></div><head><title>iframe</title></head><body><br></body>', + '<head><title>iframe</title></head><div>abcdef</div><body><br></body>', + '<head><title>iframe</title></head><div>abcdef<br></div><body><br></body>', + ], + "The <div> element should be merged into the left <div> without deleting the <head>" + ); + assert_true( + div1.isConnected ^ div2.isConnected, + "One <div> element should be removed, but the other should stay" + ); +}, `${commandName} from <div> around invisible <head> element should not delete the <head>`); + + +// Same as <body> element boundary, allow joining across <head> elements if +// and only if both elements are normal elements. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + childDoc.head.setAttribute("style", "display:block"); + const utils = new EditorTestUtils(childDoc.documentElement); + const div1 = childDoc.createElement("div"); + div1.innerHTML = "abc"; + const div2 = childDoc.createElement("div"); + div2.innerHTML = "def"; + childDoc.head.appendChild(div1); + childDoc.documentElement.insertBefore(div2, childDoc.body); + // Now: <div>abc</div></head><div>def</div><body><br></body> + childDoc.getSelection().collapse( + testingBackspace ? div2.firstChild : div1.firstChild, + testingBackspace ? 0 : div1.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + assert_in_array( + childDoc.documentElement.innerHTML, + [ + '<head><title>iframe</title><div>abcdef</div></head><body><br></body>', + '<head><title>iframe</title><div>abcdef<br></div></head><body><br></body>', + ], + "The <div> element should be merged into the <div> in the <head>" + ); + assert_false( + div2.isConnected, + "The <div> element should be removed" + ); +}, `${commandName} from <div> following visible <head> element should be merged with the <div> in the <head>`); + +// However, don't allow to join with <script> and <style> elements because +// changing them may not be safe. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + childDoc.head.setAttribute("style", "display:block"); + const utils = new EditorTestUtils(childDoc.documentElement); + const style = childDoc.createElement("style"); + style.setAttribute("style", "display:block;white-space:pre"); + style.innerHTML = "abc"; + const div = childDoc.createElement("div"); + div.innerHTML = "def"; + childDoc.head.appendChild(style); + childDoc.documentElement.insertBefore(div, childDoc.body); + // Now: <style>abc</style></head><div>def</div><body><br></body> + childDoc.getSelection().collapse( + testingBackspace ? div.firstChild : style.firstChild, + testingBackspace ? 0 : style.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + style.removeAttribute("style"); + assert_equals( + childDoc.documentElement.innerHTML, + '<head><title>iframe</title><style>abc</style></head><div>def</div><body><br></body>', + "The <div> element should not be merged with the <style> in the <head>" + ); + assert_true( + div.isConnected, + "The <div> element should not be removed" + ); +}, `${commandName} from <div> following visible <head> element should be merged with the visible <style> in the <head>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + childDoc.head.setAttribute("style", "display:block"); + const utils = new EditorTestUtils(childDoc.documentElement); + const script = childDoc.createElement("script"); + script.setAttribute("style", "display:block;white-space:pre"); + script.innerHTML = "// abc"; + const div = childDoc.createElement("div"); + div.innerHTML = "def"; + childDoc.head.appendChild(script); + childDoc.documentElement.insertBefore(div, childDoc.body); + // Now: <script>// abc</ script></head><div>def</div><body><br></body> + childDoc.getSelection().collapse( + testingBackspace ? div.firstChild : script.firstChild, + testingBackspace ? 0 : script.firstChild.length + ); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + script.removeAttribute("style"); + assert_equals( + childDoc.documentElement.innerHTML, + '<head><title>iframe</title><script>// abc</' + 'script></head><div>def</div><body><br></body>', + "The <div> element should not be merged with the <script> in the <head>" + ); + assert_true( + div.isConnected, + "The <div> element should not be removed" + ); +}, `${commandName} from <div> following visible <script> element should be merged with the visible <script> in the <head>`); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/delete-in-last-definition-list-item-when-parent-list-is-editing-host.html b/testing/web-platform/tests/editing/other/delete-in-last-definition-list-item-when-parent-list-is-editing-host.html new file mode 100644 index 0000000000..e068f9bcd2 --- /dev/null +++ b/testing/web-platform/tests/editing/other/delete-in-last-definition-list-item-when-parent-list-is-editing-host.html @@ -0,0 +1,88 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?action=Backspace"> +<meta name="variant" content="?action=Delete"> +<title>Delete in last list item should not delete parent list if it's editing host</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<script> +"use strict"; + +const params = new URLSearchParams(location.search.substring(1)); +const backspace = params.get("action") == "Backspace"; + +addEventListener("load", () => { + document.body.innerHTML ="<dl contenteditable></dl>"; + const editingHost = document.querySelector("dl[contenteditable]"); + const utils = new EditorTestUtils(editingHost); + + function addPromiseTest(aTest) { + promise_test(async () => { + editingHost.focus(); + utils.setupEditingHost(aTest.innerHTML); + await (backspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + if (Array.isArray(aTest.expectedResult)) { + assert_in_array(editingHost.innerHTML, aTest.expectedResult); + } else { + assert_equals(editingHost.innerHTML, aTest.expectedResult); + } + assert_equals( + document.body.childNodes.length, + 1, + `The editing host should be the only child of <body> (got: "${document.body.innerHTML}")` + ); + assert_equals( + document.body.firstChild, + editingHost, + `The editing host should be the only child of <body> (got: "${document.body.innerHTML}")` + ); + }, `${backspace ? "Backspace" : "Delete"} in "<dl contenteditable>${aTest.innerHTML}</dl>"`); + } + + addPromiseTest({ + innerHTML: "<dt>{}</dt>", + expectedResult: ["<dt></dt>", "<dt><br></dt>"], + }); + addPromiseTest({ + innerHTML: "<dd>{}</dd>", + expectedResult: ["<dd></dd>", "<dd><br></dd>"], + }); + addPromiseTest({ + innerHTML: "<dd><ul><li>{}</li></ul></dd>", + expectedResult: ["<dd></dd>", "<dd><br></dd>"], + }); + addPromiseTest({ + innerHTML: "<dd><ol><li>{}</li></ol></dd>", + expectedResult: ["<dd></dd>", "<dd><br></dd>"], + }); + // If only sub-list in the editing host list element, the sub-list should be + // replaced with a list item. + addPromiseTest({ + innerHTML: "<ul><li>{}</li></ul>", + expectedResult: ["<dd></dd>", "<dd><br></dd>"], + }); + addPromiseTest({ + innerHTML: "<ol><li>{}</li></ol>", + expectedResult: ["<dd></dd>", "<dd><br></dd>"], + }); + addPromiseTest({ + innerHTML: "<dl><dt>{}</dt></dl>", + expectedResult: ["<dd></dd>", "<dd><br></dd>"], + }); + addPromiseTest({ + innerHTML: "<dl><dd>{}</dd></dl>", + expectedResult: ["<dd></dd>", "<dd><br></dd>"], + }); +}, {once:true}); +</script> +</head> +<body></body> +</html> diff --git a/testing/web-platform/tests/editing/other/delete-in-last-list-item-when-parent-list-is-editing-host.html b/testing/web-platform/tests/editing/other/delete-in-last-list-item-when-parent-list-is-editing-host.html new file mode 100644 index 0000000000..7d15943935 --- /dev/null +++ b/testing/web-platform/tests/editing/other/delete-in-last-list-item-when-parent-list-is-editing-host.html @@ -0,0 +1,87 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?list=ul&action=Backspace"> +<meta name="variant" content="?list=ul&action=Delete"> +<meta name="variant" content="?list=ol&action=Backspace"> +<meta name="variant" content="?list=ol&action=Delete"> +<title>Delete in last list item should not delete parent list if it's editing host</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<script> +"use strict"; + +const params = new URLSearchParams(location.search.substring(1)); +const backspace = params.get("action") == "Backspace"; +const list = params.get("list"); + +addEventListener("load", () => { + document.body.innerHTML =`<${list} contenteditable></${list}>`; + const editingHost = document.querySelector("[contenteditable]"); + const utils = new EditorTestUtils(editingHost); + + function addPromiseTest(aTest) { + promise_test(async () => { + editingHost.focus(); + utils.setupEditingHost(aTest.innerHTML); + await (backspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + if (Array.isArray(aTest.expectedResult)) { + assert_in_array(editingHost.innerHTML, aTest.expectedResult); + } else { + assert_equals(editingHost.innerHTML, aTest.expectedResult); + } + assert_equals( + document.body.childNodes.length, + 1, + `The editing host should be the only child of <body> (got: "${document.body.innerHTML}")` + ); + assert_equals( + document.body.firstChild, + editingHost, + `The editing host should be the only child of <body> (got: "${document.body.innerHTML}")` + ); + }, `${backspace ? "Backspace" : "Delete"} in "<${list} contenteditable>${aTest.innerHTML}</${list}>"`); + } + + addPromiseTest({ + innerHTML: "<li>{}</li>", + expectedResult: ["<li></li>", "<li><br></li>"], + }); + addPromiseTest({ + innerHTML: "<li><ul><li>{}</li></ul></li>", + expectedResult: ["<li></li>", "<li><br></li>"], + }); + addPromiseTest({ + innerHTML: "<li><ol><li>{}</li></ol></li>", + expectedResult: ["<li></li>", "<li><br></li>"], + }); + // If only sub-list in the editing host list element, the sub-list should be + // replaced with a list item. + addPromiseTest({ + innerHTML: "<ul><li>{}</li></ul>", + expectedResult: ["<li></li>", "<li><br></li>"], + }); + addPromiseTest({ + innerHTML: "<ol><li>{}</li></ol>", + expectedResult: ["<li></li>", "<li><br></li>"], + }); + addPromiseTest({ + innerHTML: "<dl><dt>{}</dt></dl>", + expectedResult: ["<li></li>", "<li><br></li>"], + }); + addPromiseTest({ + innerHTML: "<dl><dd>{}</dd></dl>", + expectedResult: ["<li></li>", "<li><br></li>"], + }); +}, {once:true}); +</script> +</head> +<body></body> +</html> diff --git a/testing/web-platform/tests/editing/other/delete.html b/testing/web-platform/tests/editing/other/delete.html new file mode 100644 index 0000000000..b9bd1437e3 --- /dev/null +++ b/testing/web-platform/tests/editing/other/delete.html @@ -0,0 +1,149 @@ +<!doctype html> +<meta charset=utf-8> +<title>Deletion tests</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div contenteditable></div> +<script> +var div = document.querySelector("div"); + +// Format: [start html, start pos, expected html, expected pos, command] +// Positions are a sequence of offsets starting from div, e.g., "1,2,0" +// translates to node = div.childNodes[1].childNodes[2], offset = 0. For a +// non-collapsed selection, use a hyphen, like "0,0-1,0". The selections are +// created with collapse() followed by extend() to allow reverse selections, so +// order is significant. +// +// Expected values can be arrays, in which case any is acceptable. +var tests = [ + ["<p><br></p><p><br></p>", "1,0", "<p><br></p>", "0,0", "delete"], + ["<p><br></p><p><br></p>", "0,0", "<p><br></p>", "0,0", "forwarddelete"], + + // Range + ["<p><br></p><p><br></p>", "0,0-1,0", "<p><br></p>", "0,0", "delete"], + ["<p><br></p><p><br></p>", "0,0-1,0", "<p><br></p>", "0,0", "forwarddelete"], + ["<p><br></p><p><br></p>", "1,0-0,0", "<p><br></p>", "0,0", "delete"], + ["<p><br></p><p><br></p>", "1,0-0,0", "<p><br></p>", "0,0", "forwarddelete"], + + // Different start values + ["<p>x<br></p><p><br></p>", "1,0", + // WebKit/Blink like to get rid of the extra <br> + ["<p>x<br></p>", "<p>x</p>"], + // The selection should really be collapsed inside the text node, but in the + // parent is close enough. + ["0,0,1", "0,1"], "delete"], + ["<p><br><br></p><p><br></p>", "1,0", "<p><br><br></p>", "0,1", "delete"], + ["<p><br></p><p><br><br></p>", "1,1", + "<p><br></p><p><br></p>", "1,0", "delete"], + ["<p><br><br><br></p>", "0,2", "<p><br><br></p>", "0,1", "delete"], + ["<p><br></p><p><br><br><br></p>", "1,2", + "<p><br></p><p><br><br></p>", "1,1", "delete"], + ["<p><br><br></p><p><br><br></p>", "1,1", + "<p><br><br></p><p><br></p>", "1,0", "delete"], + ["<p><br></p><br>", "1", "<p><br></p>", "0,0", "delete"], + + // The trailing \n in these cases is actually significant, because it was + // necessary to trigger an actual Gecko bug (somehow!). + ["<p><br></p><p><br></p>\n", "1,0", "<p><br></p>\n", "0,0", "delete"], + ["<p><br></p><p><br></p>\n", "0,0", "<p><br></p>\n", "0,0", "forwarddelete"], + ["\n<p><tt>x</tt></p><p><tt><br></tt></p><p><tt><br></tt></p>\n", "3,0,0", + "\n<p><tt>x</tt></p><p><tt><br></tt></p>\n", "2,0,0", "delete"], +]; + +div.focus(); + +for (var i = 0; i < tests.length; i++) { + test(function() { + var test = tests[i]; + div.innerHTML = test[0]; + setSelection(test[1]); + + document.execCommand(test[4], false, ""); + + if (typeof test[2] == "string") { + assert_equals(div.innerHTML, test[2], "innerHTML"); + } else { + assert_in_array(div.innerHTML, test[2], "innerHTML"); + } + + var actualSel = recordSelection(); + var expectedSel = []; + if (typeof test[3] == "string") { + test[3] = [test[3]]; + } + for (var j = 0; j < test[3].length; j++) { + setSelection(test[3][j]); + expectedSel.push(recordSelection()); + } + assertSelectionEquals(actualSel, expectedSel, test[2]); + }, i + ": " + format_value(tests[i][0]) + " " + tests[i][1] + + " " + tests[i][4]); +} + +function setSelection(selstr) { + var parts = selstr.split("-"); + var collapsePoint = getPointFromArray(parts[0].split(",")); + getSelection().collapse(collapsePoint[0], collapsePoint[1]); + + if (parts[1]) { + var extendPoint = getPointFromArray(parts[1].split(",")); + getSelection().extend(extendPoint[0], extendPoint[1]); + } +} + +function getPointFromArray(offsets) { + var retNode = div, retOffset; + var offset; + while (offset = offsets.shift()) { + if (!offsets.length) { + retOffset = offset; + } else { + retNode = retNode.childNodes[offset]; + } + } + return [retNode, retOffset]; +} + +function recordSelection() { + return [getSelection().anchorNode, getSelection().anchorOffset, + getSelection().focusNode, getSelection().focusOffset]; +} + +function assertSelectionEquals(actual, expected, html) { + if (typeof expected == "string") { + expected = [expected]; + } + var pass = false; + for (var i = 0; i < expected.length; i++) { + if (expected[i][0] === actual[0] && + expected[i][1] === actual[1] && + expected[i][2] === actual[2] && + expected[i][3] === actual[3]) { + pass = true; + break; + } + } + + assert_true(pass, "Wrong selection, expected " + formatSel(expected) + + ", got " + formatSel(actual) + + " (in HTML " + format_value(html) + ")"); +} + +function formatSel(arr) { + if (arr.length == 1) { + arr = arr[0]; + } + if (Array.isArray(arr[0])) { + var ret = []; + for (var i = 0; i < arr.length; i++) { + ret.push(formatSel(arr[i])); + } + return ret.join(" or "); + } + if (arr[0] == arr[2] && arr[1] == arr[3]) { + return "collapsed (" + format_value(arr[0]) + ", " + arr[1] + ")"; + } + return "(" + format_value(arr[0]) + ", " + arr[1] + + ")-(" + format_value(arr[2]) + ", " + arr[3] + ")"; +} +</script> diff --git a/testing/web-platform/tests/editing/other/design-mode-textarea-crash.html b/testing/web-platform/tests/editing/other/design-mode-textarea-crash.html new file mode 100644 index 0000000000..d9f01e4165 --- /dev/null +++ b/testing/web-platform/tests/editing/other/design-mode-textarea-crash.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> +onload = function() { + let x2 = document.getElementById("x2"); + x2.select(); + x2.placeholder = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + document.designMode = "on"; +} +</script> +<textarea id="x2" inputmode="text" spellcheck="false" rows="32" readonly=""> +<data id="x56" part="part1" accesskey="" translate="no"> +Baa1101 11 +</data> +</textarea> diff --git a/testing/web-platform/tests/editing/other/edit-in-textcontrol-immediately-after-hidden.tentative.html b/testing/web-platform/tests/editing/other/edit-in-textcontrol-immediately-after-hidden.tentative.html new file mode 100644 index 0000000000..2cdffd6e2c --- /dev/null +++ b/testing/web-platform/tests/editing/other/edit-in-textcontrol-immediately-after-hidden.tentative.html @@ -0,0 +1,138 @@ +<!doctype html> +<meta charset=utf-8> +<meta name="variant" content="?editor=input&hide-target=editor"> +<meta name="variant" content="?editor=textarea&hide-target=editor"> +<meta name="variant" content="?editor=input&hide-target=parent"> +<meta name="variant" content="?editor=textarea&hide-target=parent"> +<title>Testing edit action in zombie editor</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<body> +<script> +"use strict"; + +const params = new URLSearchParams(location.search); + +/** + * The expected results is based on Chrome 93. + * The behavior is reasonable because JS API which requires focus and user input + * does not work if it's hidden. + */ + +function init() { + const div = document.createElement("div"); + const editor = document.createElement(params.get("editor")); + const hideTarget = params.get("hide-target") == "editor" ? editor : div; + editor.value = "default value"; + div.appendChild(editor); + document.body.appendChild(div); + return [ hideTarget, editor ]; +} + +function finalize(editor) { + editor.blur(); + editor.parentNode.remove(); + document.body.getBoundingClientRect(); +} + +promise_test(async () => { + await new Promise(resolve => addEventListener("load", resolve, {once: true})); +}, "Wait for load event"); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.select(); + hideTarget.style.display = "none"; + document.execCommand("insertText", false, "typed value"); + assert_equals(editor.value, "default value", "The value shouldn't be modified by \"insertText\" command"); + } finally { + finalize(editor); + } +}, `execCommand("insertText", false, "typed value") in <${params.get("editor")}>`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.select(); + hideTarget.style.display = "none"; + document.execCommand("delete"); + assert_equals(editor.value, "default value", "The value shouldn't be modified by \"delete\" command"); + } finally { + finalize(editor); + } +}, `execCommand("delete") in <${params.get("editor")}>`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.select(); + const waitForKeyDown = new Promise(resolve => { + editor.addEventListener("keydown", () => { + hideTarget.style.display = "none"; + resolve(); + }); + }); + await new test_driver.Actions() + .keyDown("a").keyUp("a") + .send(); + assert_equals( + editor.value, + "default value", + "The value shouldn't be modified by user input if \"keydown\" event listener destroyed the editor" + ); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}> is hidden by "keydown" event listener`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.select(); + const waitForKeyDown = new Promise(resolve => { + editor.addEventListener("keypress", () => { + hideTarget.style.display = "none"; + resolve(); + }); + }); + await new test_driver.Actions() + .keyDown("a").keyUp("a") + .send(); + assert_equals( + editor.value, + "default value", + "The value shouldn't be modified by user input if \"keypress\" event listener destroyed the editor" + ); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}> is hidden by "keypress" event listener`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.select(); + const waitForKeyDown = new Promise(resolve => { + editor.addEventListener("beforeinput", () => { + hideTarget.style.display = "none"; + resolve(); + }); + }); + await new test_driver.Actions() + .keyDown("a").keyUp("a") + .send(); + assert_equals( + editor.value, + "default value", + "The value shouldn't be modified by user input if \"beforeinput\" event listener destroyed the editor" + ); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}> is hidden by "beforeinput" event listener`); +</script> +</body> diff --git a/testing/web-platform/tests/editing/other/editable-state-and-focus-in-assigned-slot-dom.tentative.html b/testing/web-platform/tests/editing/other/editable-state-and-focus-in-assigned-slot-dom.tentative.html new file mode 100644 index 0000000000..ae8b145283 --- /dev/null +++ b/testing/web-platform/tests/editing/other/editable-state-and-focus-in-assigned-slot-dom.tentative.html @@ -0,0 +1,31 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing editable state and focus in slotted editable element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="host"> +<div contenteditable></div> +</div> +<script> +const text = 'A'; +test(() => { + const host = document.querySelector('#host'); + const shadowRoot = host.attachShadow({ mode: "open" }); + const slot = document.createElement("slot"); + shadowRoot.appendChild(slot); + const editable = document.querySelector('div[contenteditable]'); + editable.focus(); + document.execCommand('insertText', false, text); + editable.blur(); + assert_equals(editable.innerText, text); + editable.focus(); + document.execCommand('insertText', false, text); + assert_equals(editable.innerText, text + text); +}); +</script> +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/editing/other/editable-state-and-focus-in-shadow-dom-in-designMode.tentative.html b/testing/web-platform/tests/editing/other/editable-state-and-focus-in-shadow-dom-in-designMode.tentative.html new file mode 100644 index 0000000000..88e6d29129 --- /dev/null +++ b/testing/web-platform/tests/editing/other/editable-state-and-focus-in-shadow-dom-in-designMode.tentative.html @@ -0,0 +1,252 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing editable state and focus in shadow DOM in design mode</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<h3>open</h3> +<my-shadow data-mode="open"></my-shadow> +<h3>closed</h3> +<my-shadow data-mode="closed"></my-shadow> + +<script> +"use strict"; + +document.designMode = "on"; +const utils = new EditorTestUtils(document.body); + +class MyShadow extends HTMLElement { + #defaultInnerHTML = + "<style>:focus { outline: 3px red solid; }</style>" + + "<div>text" + + "<div contenteditable=\"\">editable</div>" + + "<object tabindex=\"0\">object</object>" + + "<p tabindex=\"0\">paragraph</p>" + + "</div>"; + #shadowRoot; + + constructor() { + super(); + this.#shadowRoot = this.attachShadow({mode: this.getAttribute("data-mode")}); + this.#shadowRoot.innerHTML = this.#defaultInnerHTML; + } + + reset() { + this.#shadowRoot.innerHTML = this.#defaultInnerHTML; + this.#shadowRoot.querySelector("div").getBoundingClientRect(); + } + + focusText() { + this.focus(); + const div = this.#shadowRoot.querySelector("div"); + getSelection().collapse(div.firstChild || div, 0); + } + + focusContentEditable() { + this.focus(); + const contenteditable = this.#shadowRoot.querySelector("div[contenteditable]"); + contenteditable.focus(); + getSelection().collapse(contenteditable.firstChild || contenteditable, 0); + } + + focusObject() { + this.focus(); + this.#shadowRoot.querySelector("object[tabindex]").focus(); + } + + focusParagraph() { + this.focus(); + const tabbableP = this.#shadowRoot.querySelector("p[tabindex]"); + tabbableP.focus(); + getSelection().collapse(tabbableP.firstChild || tabbableP, 0); + } + + getInnerHTML() { + return this.#shadowRoot.innerHTML; + } + + getDefaultInnerHTML() { + return this.#defaultInnerHTML; + } + + getFocusedElementName() { + return this.#shadowRoot.querySelector(":focus")?.tagName.toLocaleLowerCase() || ""; + } + + getSelectedRange() { + // XXX There is no standardized way to retrieve selected ranges in + // shadow trees, therefore, we use non-standardized API for now + // since the main purpose of this test is checking the behavior of + // selection changes in shadow trees, not checking the selection API. + const selection = + this.#shadowRoot.getSelection !== undefined + ? this.#shadowRoot.getSelection() + : getSelection(); + return selection.getRangeAt(0); + } +} + +customElements.define("my-shadow", MyShadow); + +function getRangeDescription(range) { + function getNodeDescription(node) { + if (!node) { + return "null"; + } + switch (node.nodeType) { + case Node.TEXT_NODE: + case Node.COMMENT_NODE: + case Node.CDATA_SECTION_NODE: + return `${node.nodeName} "${node.data}"`; + case Node.ELEMENT_NODE: + return `<${node.nodeName.toLowerCase()}>`; + default: + return `${node.nodeName}`; + } + } + if (range === null) { + return "null"; + } + if (range === undefined) { + return "undefined"; + } + return range.startContainer == range.endContainer && + range.startOffset == range.endOffset + ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})` + : `(${getNodeDescription(range.startContainer)}, ${ + range.startOffset + }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`; +} + +promise_test(async () => { + await new Promise(resolve => addEventListener("load", resolve, {once: true})); + assert_true(true, "Load event is fired"); +}, "Waiting for load"); + +/** + * The expected result of this test is based on Blink and Gecko's behavior. + */ + +for (const mode of ["open", "closed"]) { + const host = document.querySelector(`my-shadow[data-mode=${mode}]`); + promise_test(async (t) => { + host.reset(); + host.focusText(); + test(() => { + assert_equals( + host.getFocusedElementName(), + "", + `No element should have focus after ${t.name}` + ); + }, `Focus after ${t.name}`); + await utils.sendKey("A"); + test(() => { + assert_equals( + host.getInnerHTML(), + host.getDefaultInnerHTML(), + `The shadow DOM shouldn't be modified after ${t.name}` + ); + }, `Typing "A" after ${t.name}`); + }, `Collapse selection into text in the ${mode} shadow DOM`); + + promise_test(async (t) => { + host.reset(); + host.focusContentEditable(); + test(() => { + assert_equals( + host.getFocusedElementName(), + "div", + `<div contenteditable> should have focus after ${t.name}` + ); + }, `Focus after ${t.name}`); + await utils.sendKey("A"); + test(() => { + assert_equals( + host.getInnerHTML(), + host.getDefaultInnerHTML().replace("<div contenteditable=\"\">", "<div contenteditable=\"\">A"), + `The shadow DOM shouldn't be modified after ${t.name}` + ); + }, `Typing "A" after ${t.name}`); + }, `Collapse selection into text in <div contenteditable> in the ${mode} shadow DOM`); + + promise_test(async (t) => { + host.reset(); + host.focusObject(); + test(() => { + assert_equals( + host.getFocusedElementName(), + "object", + `The <object> element should have focus after ${t.name}` + ); + }, `Focus after ${t.name}`); + await utils.sendKey("A"); + test(() => { + assert_equals( + host.getInnerHTML(), + host.getDefaultInnerHTML(), + `The shadow DOM shouldn't be modified after ${t.name}` + ); + }, `Typing "A" after ${t.name}`); + }, `Set focus to <object> in the ${mode} shadow DOM`); + + promise_test(async (t) => { + host.reset(); + host.focusParagraph(); + test(() => { + assert_equals( + host.getFocusedElementName(), + "p", + `The <p tabindex="0"> element should have focus after ${t.name}` + ); + }, `Focus after ${t.name}`); + await utils.sendKey("A"); + test(() => { + assert_equals( + host.getInnerHTML(), + host.getDefaultInnerHTML(), + `The shadow DOM shouldn't be modified after ${t.name}` + ); + }, `Typing "A" after ${t.name}`); + }, `Set focus to <p tabindex="0"> in the ${mode} shadow DOM`); + + promise_test(async (t) => { + host.reset(); + host.focusParagraph(); + await utils.sendSelectAllShortcutKey(); + assert_in_array( + getRangeDescription(host.getSelectedRange()), + [ + // Feel free to add reasonable select all result in the <my-shadow>. + "(#document-fragment, 0) - (#document-fragment, 2)", + "(#text \"text\", 0) - (#text \"paragraph\", 9)", + ], + `Only all children of the ${mode} shadow DOM should be selected` + ); + getSelection().collapse(document.body, 0); + }, `SelectAll in the ${mode} shadow DOM`); + + promise_test(async (t) => { + host.reset(); + host.focusContentEditable(); + await utils.sendSelectAllShortcutKey(); + assert_in_array( + getRangeDescription(host.getSelectedRange()), + [ + // Feel free to add reasonable select all result in the <div contenteditable>. + "(<div>, 0) - (<div>, 1)", + "(#text \"editable\", 0) - (#text \"editable\", 8)", + ] + ); + getSelection().collapse(document.body, 0); + }, `SelectAll in the <div contenteditable> in the ${mode} shadow DOM`); +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/editing-around-select-element.tentative.html b/testing/web-platform/tests/editing/other/editing-around-select-element.tentative.html new file mode 100644 index 0000000000..9182216efd --- /dev/null +++ b/testing/web-platform/tests/editing/other/editing-around-select-element.tentative.html @@ -0,0 +1,310 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test execCommand with selection around select element</title> +<meta name="timeout" content="long"> +<meta name="variant" content="?delete"> +<meta name="variant" content="?forwardDelete"> +<meta name="variant" content="?insertText"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +"use strict"; + +const command = document.location.search.substring(1); +const insertText = command === "insertText" ? "XYZ" : ""; + +/** + * Typically, browsers do not allow to move caret or select part of <select>, + * <option> and <optgroup>, but Selection API can do it (but browsers don't + * show the result). In this case, any elements under `<select>` element + * shouldn't be modified (deleted) for avoiding unexpected data loss for the + * users. + */ + +promise_test(async () => { + await new Promise(resolve => { + addEventListener("load", resolve, {once: true}); + }); +}); + +function addPromiseTest(desc, initFunc, expectedResults) { + promise_test(async () => { + initFunc(); + document.execCommand(command, false, insertText); + if (Array.isArray(expectedResults)) { + assert_in_array(document.body.innerHTML.replace(/(=""|<br>)/g, ""), expectedResults); + } else { + assert_equals(document.body.innerHTML.replace(/(=""|<br>)/g, ""), expectedResults); + } + }, `execCommand(${command}, false, "${insertText}") in ${desc}`); +} + +for (const multiple of ["", " multiple"]) { + addPromiseTest( + `<div contenteditable><p>ab[c</p><select${multiple}><option>d]ef</option></select></div>: shouldn't modify in <option>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select></div>`; + getSelection().setBaseAndExtent( + document.querySelector("p").firstChild, + 2, + document.querySelector("option").firstChild, + 1 + ); + }, + [ + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select></div>`, + `<div contenteditable><p>ab${insertText}</p><select${multiple}><option>def</option></select></div>`, + ] + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}><option>d[]ef</option></select></div>: shouldn't modify in <option>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select></div>`; + getSelection().collapse( + document.querySelector("option").firstChild, + 1 + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select></div>`, + ); + + addPromiseTest( + `<div contenteditable><select${multiple}><option>ab[c</option></select><p>d]ef</p></div>: shouldn't modify in <option>`, + () => { + document.body.innerHTML = + `<div contenteditable><select${multiple}><option>abc</option></select><p>def</p></div>`; + getSelection().setBaseAndExtent( + document.querySelector("option").firstChild, + 2, + document.querySelector("p").firstChild, + 1 + ); + }, + [ + `<div contenteditable><select${multiple}><option>abc</option></select><p>def</p></div>`, + `<div contenteditable><select${multiple}><option>abc</option></select><p>${insertText}ef</p></div>`, + ] + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}><option>{}def</option></select><p>ghi</p></div>: shouldn't modify in <option>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select><p>ghi</p></div>`; + getSelection().collapse( + document.querySelector("option"), + 0 + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select><p>ghi</p></div>` + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}><option>def{}</option></select><p>ghi</p></div>: shouldn't modify in <option>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select><p>ghi</p></div>`; + getSelection().collapse( + document.querySelector("option"), + 1 + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select><p>ghi</p></div>` + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}><option>{def}</option></select><p>ghi</p></div>: shouldn't modify in <option>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select><p>ghi</p></div>`; + getSelection().selectAllChildren( + document.querySelector("option") + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><option>def</option></select><p>ghi</p></div>` + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}><option>{def</option><option>ghi}</option></select><p>jkl</p></div>: shouldn't join <option>s`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>`; + getSelection().setBaseAndExtent( + document.querySelector("option"), + 0, + document.querySelector("option + option"), + 1, + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>` + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}>{<option>def</option>}<option>ghi</option></select><p>jkl</p></div>: shouldn't delete <option>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>`; + getSelection().setBaseAndExtent( + document.querySelector("select"), + 0, + document.querySelector("select"), + 1, + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>` + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}><option>def</option>{<option>ghi</option>}</select><p>jkl</p></div>: shouldn't delete <option>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>`; + getSelection().setBaseAndExtent( + document.querySelector("select"), + 1, + document.querySelector("select"), + 2, + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>` + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}>{<option>def</option><option>ghi</option>}</select><p>jkl</p></div>: shouldn't delete <option>s nor <select${multiple}>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>`; + getSelection().selectAllChildren( + document.querySelector("select") + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>` + ); + + addPromiseTest( + `<div contenteditable><p>abc</p><select${multiple}><optgroup>{<option>def</option><option>ghi</option>}</optgroup></select><p>jkl</p></div>: shouldn't delete <option>, <optgroup> nor <select${multiple}>`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><optgroup><option>def</option><option>ghi</option></optgroup></select><p>jkl</p></div>`; + getSelection().selectAllChildren( + document.querySelector("optgroup") + ); + }, + `<div contenteditable><p>abc</p><select${multiple}><optgroup><option>def</option><option>ghi</option></optgroup></select><p>jkl</p></div>` + ); + + addPromiseTest( + `<div contenteditable><p>abc</p>{<select${multiple}><option>def</option><option>ghi</option></select>}<p>jkl</p></div>: <select${multiple}> element itself should be removable`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>`; + getSelection().setBaseAndExtent( + document.querySelector("div"), + 1, + document.querySelector("div"), + 2, + ); + }, + [ + `<div contenteditable><p>abc</p>${insertText}<p>jkl</p></div>`, + `<div contenteditable><p>abc${insertText}</p><p>jkl</p></div>`, + `<div contenteditable><p>abc</p><p>${insertText}jkl</p></div>`, + ] + ); + + addPromiseTest( + `<div contenteditable><p>abc</p>{<select${multiple}><optgroup><option>def</option><option>ghi</option></optgroup></select>}<p>jkl</p></div>: <select${multiple}> element itself should be removable`, + () => { + document.body.innerHTML = + `<div contenteditable><p>abc</p><select${multiple}><option>def</option><option>ghi</option></select><p>jkl</p></div>`; + getSelection().setBaseAndExtent( + document.querySelector("div"), + 1, + document.querySelector("div"), + 2, + ); + }, + [ + `<div contenteditable><p>abc</p>${insertText}<p>jkl</p></div>`, + `<div contenteditable><p>abc${insertText}</p><p>jkl</p></div>`, + `<div contenteditable><p>abc</p><p>${insertText}jkl</p></div>`, + ] + ); + + addPromiseTest( + `<select${multiple} contenteditable>{<option>abc</option><option>def</option>}</select>: shouldn't delete <option>s`, + () => { + document.body.innerHTML = + `<select${multiple} contenteditable><option>abc</option><option>def</option></select>`; + getSelection().selectAllChildren( + document.querySelector("select") + ); + }, + `<select${multiple} contenteditable><option>abc</option><option>def</option></select>` + ); + + addPromiseTest( + `<select${multiple}><option contenteditable>{abc}</option><option>def</option></select>: shouldn't modify <option>`, + () => { + document.body.innerHTML = + `<select${multiple}><option contenteditable>abc</option><option>def</option></select>`; + getSelection().selectAllChildren( + document.querySelector("option") + ); + }, + `<select${multiple}><option contenteditable>abc</option><option>def</option></select>` + ); + + addPromiseTest( + `<select${multiple}><optgroup contenteditable>{<option>abc</option><option>def</option>}</optgroup></select>: shouldn't delete <option>s`, + () => { + document.body.innerHTML = + `<select${multiple}><optgroup contenteditable><option>abc</option><option>def</option></optgroup></select>`; + getSelection().selectAllChildren( + document.querySelector("optgroup") + ); + }, + `<select${multiple}><optgroup contenteditable><option>abc</option><option>def</option></optgroup></select>` + ); + + addPromiseTest( + `<select${multiple}><optgroup contenteditable><option>{abc}</option><option>def</option></optgroup></select>: shouldn't delete <option>s nor optgroup`, + () => { + document.body.innerHTML = + `<select${multiple}><optgroup contenteditable><option>abc</option><option>def</option></optgroup></select>`; + getSelection().selectAllChildren( + document.querySelector("option") + ); + }, + `<select${multiple}><optgroup contenteditable><option>abc</option><option>def</option></optgroup></select>` + ); +} + +addPromiseTest( + "<optgroup contenteditable><option>{abc}</option><option>def</option></optgroup>: shouldn't delete <option>s nor optgroup", + () => { + document.body.innerHTML = + "<optgroup contenteditable><option>abc</option><option>def</option></optgroup>"; + getSelection().selectAllChildren( + document.querySelector("option") + ); + }, + `<optgroup contenteditable><option>abc</option><option>def</option></optgroup>` +); + +addPromiseTest( + "<option contenteditable>{abc}</option>: shouldn't modify <option>", + () => { + document.body.innerHTML = + "<option contenteditable>abc</option>"; + getSelection().selectAllChildren( + document.querySelector("option") + ); + }, + `<option contenteditable>abc</option>` +); +</script> +<body></body>
\ No newline at end of file diff --git a/testing/web-platform/tests/editing/other/editing-div-outside-body.html b/testing/web-platform/tests/editing/other/editing-div-outside-body.html new file mode 100644 index 0000000000..03064eb612 --- /dev/null +++ b/testing/web-platform/tests/editing/other/editing-div-outside-body.html @@ -0,0 +1,163 @@ +<!doctype html> +<html> +<meta charset=utf-8> +<meta name="variant" content="?designMode"> +<meta name="variant" content="?body"> +<meta name="variant" content="?html"> +<meta name="variant" content="?div-in-body"> +<meta name="variant" content="?nothing"> +<title>Test editing outside of body element</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<script> +"use strict"; + +// This test creates an editable <div> element, append it to the <html>, +// i.e., after the <body>, and do something in it. + +const tests = [ + { + command: "insertText", + arg: "abc", + initial: "<div>[]<br></div>", + expected: ["<div>abc</div>", "<div>abc<br></div>"], + }, + { + command: "delete", + initial: "<div>abc[]</div>", + expected: ["<div>ab</div>", "<div>ab<br></div>"], + }, + { + command: "forwardDelete", + initial: "<div>[]abc</div>", + expected: ["<div>bc</div>", "<div>bc<br></div>"], + }, + { + command: "insertParagraph", + initial: "<div>ab[]c</div>", + expected: [ + "<div>ab</div><div>c</div>", + "<div>ab<br></div><div>c</div>", + "<div>ab</div><div>c<br></div>", + "<div>ab<br></div><div>c<br></div>", + ], + }, + { + command: "insertLineBreak", + initial: "<div>ab[]c</div>", + expected: ["<div>ab<br>c</div>", "<div>ab<br>c<br></div>"], + }, + { + command: "bold", + initial: "<div>a[b]c</div>", + expected: ["<div>a<b>b</b>c</div>", "<div>a<b>b</b>c<br></div>"], + }, + { + command: "italic", + initial: "<div>a[b]c</div>", + expected: ["<div>a<i>b</i>c</div>", "<div>a<i>b</i>c<br></div>"], + }, + { + command: "createLink", + arg: "another.html", + initial: "<div>a[b]c</div>", + expected: [ + "<div>a<a href=\"another.html\">b</a>c</div>", + "<div>a<a href=\"another.html\">b</a>c<br></div>", + ], + }, + { + command: "unlink", + initial: "<div>a[<a href=\"another.html\">b</a>]c</div>", + expected: ["<div>abc</div>", "<div>abc<br></div>"], + }, + { + command: "insertHTML", + arg: "<hr>", + initial: "<div>a[b]c</div>", + expected: [ + "<div>a<hr>c</div>", + "<div>a<br><hr>c</div>", + "<div>a<hr>c<br></div>", + "<div>a<br><hr>c<br></div>", + ], + }, + // TODO: Add more commands. +]; + +let editingHost = () => { + switch (document.location.search) { + case "?designMode": + document.designMode = "on"; + return document.documentElement; + case "?body": + document.body.setAttribute("contenteditable", "true"); + return document.body; + case "?html": + document.documentElement.setAttribute("contenteditable", "true"); + return document.documentElement; + case "?div-in-body": + return document.querySelector("div[contenteditable]"); + case "?nothing": + return null; + } +}; + +let div; + +promise_test(async () => { + await new Promise(resolve => { + addEventListener( + "load", + () => { + assert_true(true, "load event is fired"); + resolve(); + }, + { once: true } + ); + }); + + div = document.createElement("div"); + if (editingHost != document.documentElement) { + div.setAttribute("contenteditable", "true"); + editingHost = div; + } + document.documentElement.appendChild(div); + assert_equals(document.documentElement.lastChild, div, + "The test target should be last child of the <html>"); +}, "Waiting for load event"); + +function addPromiseTest(testName, testFunc) { + promise_test(async () => { + editingHost.focus(); + await testFunc(new EditorTestUtils(div)); + }, testName); +} + +for (const test of tests) { + addPromiseTest( + `Test for execCommand("${test.command}", false, ${ + typeof test.arg == "string" ? `"${test.arg}"` : test.arg + }) in "${test.initial}"`, + async (utils) => { + utils.setupEditingHost(test.initial); + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + document.execCommand(test.command, false, test.arg); + if (Array.isArray(test.expected)) { + assert_in_array(div.innerHTML, test.expected, + "The editing result is different from expected one"); + } else { + assert_equals(div.innerHTML, test.expected, + "The editing result is different from expected one"); + } + } + ); +} +</script> +</head> +<body></body> +</html> diff --git a/testing/web-platform/tests/editing/other/editing-style-of-range-around-void-element-child.tentative.html b/testing/web-platform/tests/editing/other/editing-style-of-range-around-void-element-child.tentative.html new file mode 100644 index 0000000000..864074c1a6 --- /dev/null +++ b/testing/web-platform/tests/editing/other/editing-style-of-range-around-void-element-child.tentative.html @@ -0,0 +1,157 @@ +<!doctype html> +<html> +<meta charset=utf-8> +<title>Test toggling style a range which starts from or ends by a child of a void element</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> +<div contenteditable></div> +<script> +"use strict"; + +/** + * The expected behavior is based on Chromium, but this is edge case tests. + * So, failures of this are not important unless crash. + */ + +const editor = document.querySelector("div[contenteditable]"); + +promise_test(async () => { + await new Promise(resolve => { + addEventListener("load", () => { + assert_true(true, "The document is loaded"); + resolve(); + }, { once: true }); + }); +}, "Waiting for load..."); + +promise_test(async () => { + editor.innerHTML = "<meta>bar"; + const meta = editor.querySelector("meta"); + const text = document.createTextNode("foo"); + meta.append(text); + assert_equals(meta.firstChild, text); + getSelection().setBaseAndExtent(meta.firstChild, 1, meta.nextSibling, 1); + document.execCommand("bold"); + assert_in_array( + editor.innerHTML, + [ + "<meta><b>b</b>ar", + "<meta><b>b</b>ar<br>", + ] + ); +}, "Try to apply style from void element child"); + +promise_test(async () => { + editor.innerHTML = "foo<meta>"; + const meta = editor.querySelector("meta"); + const text = document.createTextNode("bar"); + meta.append(text); + assert_equals(meta.firstChild, text); + getSelection().setBaseAndExtent(editor.firstChild, 1, meta.firstChild, 2); + document.execCommand("bold"); + assert_in_array( + editor.innerHTML, + [ + "f<b>oo</b><meta>", + "f<b>oo</b><meta><br>", + ] + ); +}, "Try to apply style by void element child"); + +promise_test(async () => { + editor.innerHTML = "<meta>"; + const meta = editor.querySelector("meta"); + const text = document.createTextNode("foo"); + meta.append(text); + assert_equals(meta.firstChild, text); + getSelection().setBaseAndExtent(meta.firstChild, 1, meta.firstChild, 2); + document.execCommand("bold"); + assert_in_array( + editor.innerHTML, + [ + "<meta>", + "<meta><br>", + ] + ); +}, "Try to apply style in void element child"); + +promise_test(async () => { + editor.innerHTML = "<meta>bar"; + const meta = editor.querySelector("meta"); + meta.setAttribute("style", "font-weight: bold"); + const text = document.createTextNode("foo"); + meta.append(text); + assert_equals(meta.firstChild, text); + getSelection().setBaseAndExtent(meta.firstChild, 1, meta.nextSibling, 1); + document.execCommand("bold"); + assert_in_array( + editor.innerHTML, + [ + "<meta><b>b</b>ar", + "<meta><b>b</b>ar<br>", + "<meta style=\"\"><b>b</b>ar", + "<meta style=\"\"><b>b</b>ar<br>", + ] + ); +}, "Try to remove style from void element child"); + +promise_test(async () => { + editor.innerHTML = "<meta>bar"; + const meta = editor.querySelector("meta"); + meta.setAttribute("style", "font-weight: bold"); + const text = document.createTextNode("foo"); + meta.append(text); + assert_equals(meta.firstChild, text); + getSelection().setBaseAndExtent(meta.firstChild, meta.firstChild.length, meta.nextSibling, 1); + document.execCommand("bold"); + assert_in_array( + editor.innerHTML, + [ + "<meta><b>b</b>ar", + "<meta><b>b</b>ar<br>", + "<meta style=\"\"><b>b</b>ar", + "<meta style=\"\"><b>b</b>ar<br>", + ] + ); +}, "Try to remove style from end of void element child"); + +promise_test(async () => { + editor.innerHTML = "foo<meta>"; + const meta = editor.querySelector("meta"); + const text = document.createTextNode("bar"); + meta.append(text); + assert_equals(meta.firstChild, text); + getSelection().setBaseAndExtent(editor.firstChild, 1, meta.firstChild, 2); + document.execCommand("bold"); + assert_in_array( + editor.innerHTML, + [ + "f<b>oo</b><meta>", + "f<b>oo</b><meta><br>", + "f<b>oo</b><meta style=\"\">", + "f<b>oo</b><meta style=\"\"><br>", + ] + ); +}, "Try to remove style by void element child"); + +promise_test(async () => { + editor.innerHTML = "foo<meta>"; + const meta = editor.querySelector("meta"); + const text = document.createTextNode("bar"); + meta.append(text); + assert_equals(meta.firstChild, text); + getSelection().setBaseAndExtent(editor.firstChild, 1, meta.firstChild, 0); + document.execCommand("bold"); + assert_in_array( + editor.innerHTML, + [ + "f<b>oo</b><meta>", + "f<b>oo</b><meta><br>", + "f<b>oo</b><meta style=\"\">", + "f<b>oo</b><meta style=\"\"><br>", + ] + ); +}, "Try to remove style by start of void element child"); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/editing/other/empty-elements-insertion.html b/testing/web-platform/tests/editing/other/empty-elements-insertion.html new file mode 100644 index 0000000000..c654521354 --- /dev/null +++ b/testing/web-platform/tests/editing/other/empty-elements-insertion.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Placing selection and typing inside empty elements</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<style> + #border { + display: inline-block; + border: 2px red solid; + } + + #padding { + display: inline-block; + padding: 1em; + } + + #unstyled-before::before, + #unstyled-after::after, + #unstyled-both::before, + #unstyled-both::after { + content: ''; + display: inline-block; + border: 2px red solid; + } +</style> +<div contenteditable></div> + +<script> + const utils = new EditorTestUtils( document.querySelector( 'div[contenteditable]' ) ); + + promise_test( async () => { + utils.setupEditingHost( `<p><strong id="border"></strong></p>` ); + + const target = document.querySelector( '#border' ); + const actions = new test_driver.Actions(); + await actions + .pointerMove( 0, 0, { origin: target } ) + .pointerDown( { button: actions.ButtonType.LEFT } ) + .pointerUp( { button: actions.ButtonType.LEFT } ) + .send(); + document.execCommand( 'insertText', false, 'a' ); + assert_greater_than( target.innerHTML.length, 0, 'The text should be inserted into the styled <strong> element' ); + }, 'Insert text into the inline element styled with border' ); + + promise_test( async () => { + utils.setupEditingHost( `<p><strong id="padding"></strong></p>` ); + + const target = document.querySelector( '#padding' ); + const actions = new test_driver.Actions(); + await actions + .pointerMove( 0, 0, { origin: target } ) + .pointerDown( { button: actions.ButtonType.LEFT } ) + .pointerUp( { button: actions.ButtonType.LEFT } ) + .send(); + document.execCommand( 'insertText', false, 'a' ); + assert_greater_than( target.innerHTML.length, 0, 'The text should be inserted into the styled <strong> element' ); + }, 'Insert text into the inline element styled with padding' ); + + promise_test( async () => { + utils.setupEditingHost( `<p><strong id="unstyled"></strong></p>` ); + + const target = document.querySelector( '#unstyled' ); + const actions = new test_driver.Actions(); + await actions + .pointerMove( 0, 0, { origin: target } ) + .pointerDown( { button: actions.ButtonType.LEFT } ) + .pointerUp( { button: actions.ButtonType.LEFT } ) + .send(); + document.execCommand( 'insertText', false, 'a' ); + assert_greater_than( target.innerHTML.length, 0, 'The text should be inserted into the unstyled <strong> element' ); + }, 'Insert text into the unstyled inline element' ); + + promise_test( async () => { + utils.setupEditingHost( `<p><strong id="unstyled-before"></strong></p>` ); + + const target = document.querySelector( '#unstyled-before' ); + const actions = new test_driver.Actions(); + await actions + .pointerMove( 0, 0, { origin: target } ) + .pointerDown( { button: actions.ButtonType.LEFT } ) + .pointerUp( { button: actions.ButtonType.LEFT } ) + .send(); + document.execCommand( 'insertText', false, 'a' ); + assert_greater_than( target.innerHTML.length, 0, 'The text should be inserted into the <strong> element' ); + }, 'Insert text into the unstyled inline element with the styled ::before pseudoelement' ); + + promise_test( async () => { + utils.setupEditingHost( `<p><strong id="unstyled-after"></strong></p>` ); + + const target = document.querySelector( '#unstyled-after' ); + const actions = new test_driver.Actions(); + await actions + .pointerMove( 0, 0, { origin: target } ) + .pointerDown( { button: actions.ButtonType.LEFT } ) + .pointerUp( { button: actions.ButtonType.LEFT } ) + .send(); + document.execCommand( 'insertText', false, 'a' ); + assert_greater_than( target.innerHTML.length, 0, 'The text should be inserted into the <strong> element' ); + }, 'Insert text into the unstyled inline element with the styled ::after pseudoelement' ); + + promise_test( async () => { + utils.setupEditingHost( `<p><strong id="unstyled-both"></strong></p>` ); + + const target = document.querySelector( '#unstyled-both' ); + const actions = new test_driver.Actions(); + await actions + .pointerMove( 0, 0, { origin: target } ) + .pointerDown( { button: actions.ButtonType.LEFT } ) + .pointerUp( { button: actions.ButtonType.LEFT } ) + .send(); + document.execCommand( 'insertText', false, 'a' ); + assert_greater_than( target.innerHTML.length, 0, 'The text should be inserted into the <strong> element' ); + }, 'Insert text into the unstyled inline element with the styled ::before and ::after pseudoelements' ); +</script> diff --git a/testing/web-platform/tests/editing/other/exec-command-never-throw-exceptions.tentative.html b/testing/web-platform/tests/editing/other/exec-command-never-throw-exceptions.tentative.html new file mode 100644 index 0000000000..1b77b15ab0 --- /dev/null +++ b/testing/web-platform/tests/editing/other/exec-command-never-throw-exceptions.tentative.html @@ -0,0 +1,89 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test that execCommand and related methods never throw exceptions if HTML or XHTML document</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div contenteditable=""></div> +<script> +"use strict"; + +const editor = document.querySelector("div[contenteditable]"); + +test(function execCommand_with_unknown_command() { + editor.innerHTML = "abc"; + editor.focus(); + try { + let done = document.execCommand("unknown-command", false, "foo"); + assert_equals(done, false, + "Should return false without throwing exception"); + } catch (e) { + assert_true(false, + "Shouldn't throw exception for unknown command"); + } +}, "Testing execCommand with unknown command"); + +test(function queryCommandEnabled_with_unknown_command() { + editor.innerHTML = "abc"; + editor.focus(); + try { + let enabled = document.queryCommandEnabled("unknown-command"); + assert_equals(enabled, false, + "Should return false without throwing exception"); + } catch (e) { + assert_true(false, + "Shouldn't throw exception for unknown command"); + } +}, "Testing queryCommandEnabled with unknown command"); + +test(function queryCommandIndeterm_with_unknown_command() { + editor.innerHTML = "abc"; + editor.focus(); + try { + let indeterminate = document.queryCommandIndeterm("unknown-command"); + assert_equals(indeterminate, false, + "Should return false without throwing exception"); + } catch (e) { + assert_true(false, + "Shouldn't throw exception for unknown command"); + } +}, "Testing queryCommandIndeterm with unknown command"); + +test(function queryCommandState_with_unknown_command() { + editor.innerHTML = "abc"; + editor.focus(); + try { + let state = document.queryCommandState("unknown-command"); + assert_equals(state, false, + "Should return false without throwing exception"); + } catch (e) { + assert_true(false, + "Shouldn't throw exception for unknown command"); + } +}, "Testing queryCommandState with unknown command"); + +test(function queryCommandSupported_with_unknown_command() { + editor.innerHTML = "abc"; + editor.focus(); + try { + let supported = document.queryCommandSupported("unknown-command"); + assert_equals(supported, false, + "Should return false without throwing exception"); + } catch (e) { + assert_true(false, + "Shouldn't throw exception for unknown command"); + } +}, "Testing queryCommandSupported with unknown command"); + +test(function queryCommandValue_with_unknown_command() { + editor.innerHTML = "abc"; + editor.focus(); + try { + let value = document.queryCommandValue("unknown-command"); + assert_equals(value, "", + "Should return empty string without throwing exception"); + } catch (e) { + assert_true(false, + "Shouldn't throw exception for unknown command"); + } +}, "Testing queryCommandValue with unknown command"); +</script> diff --git a/testing/web-platform/tests/editing/other/exec-command-with-text-editor.tentative.html b/testing/web-platform/tests/editing/other/exec-command-with-text-editor.tentative.html new file mode 100644 index 0000000000..e3b0931f16 --- /dev/null +++ b/testing/web-platform/tests/editing/other/exec-command-with-text-editor.tentative.html @@ -0,0 +1,655 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test that execCommand with <input> or <textarea></title> +<meta name="variant" content="?type=text"> +<meta name="variant" content="?type=textarea"> +<meta name="variant" content="?type=password"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id="container"></div> +<script> +"use strict"; + +setup({explicit_done: true}); +const testingType = new URLSearchParams(document.location.search).get("type"); + +/** + * This test checks whether document.execCommand() does something expected or + * not in <input> and <textarea> with/without contenteditable parent. Although + * this is not standardized even by any drafts. So, this test uses expected + * values which may be expected by web developers. + */ +function runTests() { + let container = document.getElementById("container"); + switch (testingType) { + case "text": + case "password": + container.innerHTML = `Here <b>is</b> Text: <input id="target" type="${testingType}">`; + runTest(document.getElementById("target"), `In <input type="${testingType}">`); + container.setAttribute("contenteditable", "true"); + container.innerHTML = `Here <b>is</b> Text: <input id="target" type="${testingType}">`; + runTest(document.getElementById("target"), `In <input type="${testingType}"> in contenteditable`); + break; + case "textarea": + container.innerHTML = "Here <b>is</b> Text: <textarea id=\"target\"></textarea>"; + runTest(document.getElementById("target"), "In <textarea>"); + container.setAttribute("contenteditable", "true"); + container.innerHTML = "Here <b>is</b> Text: <textarea id=\"target\"></textarea>"; + runTest(document.getElementById("target"), "In <textarea> in contenteditable"); + break; + } + done(); +} + +function runTest(aTarget, aDescription) { + const kIsTextArea = testingType == "textarea"; + const kTests = [ + /** + * command: command name of execCommand(). + * param: param for the command. i.e., the 3rd param of execCommand(). + * value: initial value of <input> or <textarea>. must have a pair of + * "[" and "]" for specifying selection range. + * expectedValue: expected value of <input> or <textarea> after calling + * execCommand() with command and param. must have a + * pair of "[" and "]" for specifying selection range. + * expectedExecCommandResult: expected bool result of execCommand(). + * expectedCommandSupported: expected bool result of queryCommandSupported(). + * expectedCommandEnabled: expected bool result of queryCommandEnabled(). + * beforeinputExpected: if "beforeinput" event shouldn't be fired, set + * null. otherwise, expected inputType value and + * target element. + * inputExpected: if "input" event shouldn't be fired, set null. + * otherwise, expected inputType value and target element. + */ + {command: "getHTML", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: false, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "bold", param: "bold", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "italic", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "underline", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "strikethrough", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "superscript", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + // Should return true for web apps implementing custom editor. + {command: "cut", param: null, + value: "ab[]c", expectedValue: "ab[]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "cut", param: null, + value: "a[b]c", expectedValue: "a[]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "deleteByCut", target: aTarget }, + }, + // Should return true for web apps implementing custom editor. + {command: "copy", param: null, + value: "abc[]d", expectedValue: "abc[]d", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "copy", param: null, + value: "a[bc]d", expectedValue: "a[bc]d", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, inputExpected: null, + }, + {command: "paste", param: null, + value: "a[]c", expectedValue: "a[bc]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "insertFromPaste", target: aTarget }, + }, + {command: "delete", param: null, + value: "ab[]c", expectedValue: "a[]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "deleteContentBackward", target: aTarget }, + }, + {command: "delete", param: null, + value: "a[b]c", expectedValue: "a[]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "deleteContentBackward", target: aTarget }, + }, + {command: "forwarddelete", param: null, + value: "a[b]c", expectedValue: "a[]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "deleteContentForward", target: aTarget }, + }, + {command: "forwarddelete", param: null, + value: "a[]bc", expectedValue: "a[]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "deleteContentForward", target: aTarget }, + }, + {command: "selectall", param: null, + value: "a[b]c", expectedValue: "[abc]", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, inputExpected: null, + }, + // Setting value should forget any transactions. + {command: "undo", param: null, + value: "[a]bc", expectedValue: "[a]bc", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "undo", param: null, + value: "a[b]c", expectedValue: "a[b]c", + initFunc: () => { + document.execCommand("delete", false, null); + }, + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "historyUndo", target: aTarget }, + }, + // Setting value should forget any transactions. + {command: "redo", param: null, + value: "[a]bc", expectedValue: "[a]bc", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "redo", param: null, + value: "a[b]c", expectedValue: "a[]c", + initFunc: () => { + document.execCommand("delete", false, null); + document.execCommand("undo", false, null); + }, + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "historyRedo", target: aTarget }, + }, + {command: "indent", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "outdent", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "backcolor", param: "#000000", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "forecolor", param: "#000000", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "hilitecolor", param: "#000000", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "fontname", param: "DummyFont", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "fontsize", param: "5", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "increasefontsize", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: false, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "decreasefontsize", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: false, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "inserthorizontalrule", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "createlink", param: "foo.html", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "insertimage", param: "no-image.png", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "inserthtml", param: "<b>inserted</b>", + value: "a[b]c", expectedValue: "ainserted[]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "insertText", target: aTarget }, + }, + {command: "inserttext", param: "**inserted**", + value: "a[b]c", expectedValue: "a**inserted**[]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "insertText", target: aTarget }, + }, + {command: "inserttext", param: "", + value: "a[b]c", expectedValue: "a[]c", + expectedExecCommandResult: true, + expectedCommandSupported: true, + expectedCommandEnabled: true, + beforeinputExpected: null, + inputExpected: { inputType: "insertText", target: aTarget }, + }, + {command: "justifyleft", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "justifyright", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "justifycenter", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "justifyfull", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "removeformat", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "unlink", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "insertorderedlist", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "insertunorderedlist", param: null, + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "insertparagraph", param: null, + value: "a[b]c", expectedValue: kIsTextArea ? "a\n[]c" : "a[b]c", + expectedExecCommandResult: kIsTextArea, + expectedCommandSupported: true, + expectedCommandEnabled: kIsTextArea, + beforeinputExpected: null, + inputExpected: kIsTextArea ? { inputType: "insertParagraph", target: aTarget } : null, + }, + {command: "insertlinebreak", param: null, + value: "a[b]c", expectedValue: kIsTextArea ? "a\n[]c" : "a[b]c", + expectedExecCommandResult: kIsTextArea, + expectedCommandSupported: true, + expectedCommandEnabled: kIsTextArea, + beforeinputExpected: null, + inputExpected: kIsTextArea ? { inputType: "insertLineBreak", target: aTarget } : null, + }, + {command: "formatblock", param: "div", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: true, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "heading", param: "h1", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: false, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + }, + {command: "styleWithCSS", param: "true", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: container.isContentEditable, + expectedCommandSupported: true, + expectedCommandEnabled: container.isContentEditable, + beforeinputExpected: null, inputExpected: null, + additionalCheckFunc: aDescription => { + test( + () => assert_equals(document.queryCommandState("styleWithCSS"), container.isContentEditable), + `${aDescription}: styleWithCSS state should be ${container.isContentEditable} when ${ + kIsTextArea ? "<textarea>" : "<input>" + } has focus` + ); + aTarget.blur(); + container.focus(); + getSelection().collapse(container, 0); + test( + () => assert_equals(document.queryCommandState("styleWithCSS"), container.isContentEditable), + `${aDescription}: styleWithCSS state should be ${container.isContentEditable} when ${ + kIsTextArea ? "<textarea>" : "<input>" + } does not have focus` + ); + }, + }, + {command: "styleWithCSS", param: "false", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: container.isContentEditable, + expectedCommandSupported: true, + expectedCommandEnabled: container.isContentEditable, + beforeinputExpected: null, inputExpected: null, + additionalCheckFunc: aDescription => { + test( + () => assert_equals(document.queryCommandState("styleWithCSS"), false), + `${aDescription}: styleWithCSS state should be false when ${ + kIsTextArea ? "<textarea>" : "<input>" + } has focus` + ); + aTarget.blur(); + container.focus(); + getSelection().collapse(container, 0); + test( + () => assert_equals(document.queryCommandState("styleWithCSS"), false), + `${aDescription}: styleWithCSS state should be false when ${ + kIsTextArea ? "<textarea>" : "<input>" + } does not have focus` + ); + }, + }, + {command: "contentReadOnly", param: "true", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: false, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + additionalCheckFunc: aDescription => { + test( + () => assert_equals(document.queryCommandState("contentReadOnly"), false), + `${aDescription}: contentReadOnly state should be true when ${ + kIsTextArea ? "<textarea>" : "<input>" + } has focus` + ); + test( + () => assert_equals(aTarget.readOnly, false), + `${aDescription}: readonly property should be true` + ); + aTarget.blur(); + container.focus(); + getSelection().collapse(container, 0); + test( + () => assert_equals(document.queryCommandState("contentReadOnly"), false), + `${aDescription}: contentReadOnly state should be false when ${ + kIsTextArea ? "<textarea>" : "<input>" + } does not have focus` + ); + }, + }, + {command: "contentReadOnly", param: "false", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: false, + expectedCommandSupported: false, + expectedCommandEnabled: false, + beforeinputExpected: null, inputExpected: null, + additionalCheckFunc: aDescription => { + test( + () => assert_equals(document.queryCommandState("contentReadOnly"), false), + `${aDescription}: contentReadOnly state should be false when ${ + kIsTextArea ? "<textarea>" : "<input>" + } has focus` + ); + test( + () => assert_equals(aTarget.readOnly, false), + `${aDescription}: readonly property should be false` + ); + aTarget.blur(); + container.focus(); + getSelection().collapse(container, 0); + test( + () => assert_equals(document.queryCommandState("contentReadOnly"), false), + `${aDescription}: contentReadOnly state should be false when ${ + kIsTextArea ? "<textarea>" : "<input>" + } does not have focus` + ); + }, + }, + {command: "defaultParagraphSeparator", param: "p", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: container.isContentEditable, + expectedCommandSupported: true, + expectedCommandEnabled: container.isContentEditable, + beforeinputExpected: null, inputExpected: null, + additionalCheckFunc: aDescription => { + test( + () => + assert_equals( + document.queryCommandValue("defaultParagraphSeparator"), + container.isContentEditable ? "p" : "div" + ) + , + `${aDescription}: defaultParagraphSeparator value should be "p" when ${ + kIsTextArea ? "<textarea>" : "<input>" + } has focus` + ); + aTarget.blur(); + container.focus(); + getSelection().collapse(container, 0); + test( + () => + assert_equals( + document.queryCommandValue("defaultParagraphSeparator"), + container.isContentEditable ? "p" : "div" + ), + `${aDescription}: defaultParagraphSeparator value should be "p" when ${ + kIsTextArea ? "<textarea>" : "<input>" + } does not have focus` + ); + }, + }, + {command: "defaultParagraphSeparator", param: "div", + value: "a[b]c", expectedValue: "a[b]c", + expectedExecCommandResult: container.isContentEditable, + expectedCommandSupported: true, + expectedCommandEnabled: container.isContentEditable, + beforeinputExpected: null, inputExpected: null, + additionalCheckFunc: aDescription => { + test( + () => assert_equals(document.queryCommandValue("defaultParagraphSeparator"), "div"), + `${aDescription}: defaultParagraphSeparator value should be "div" when ${ + kIsTextArea ? "<textarea>" : "<input>" + } has focus` + ); + aTarget.blur(); + container.focus(); + getSelection().collapse(container, 0); + test( + () => assert_equals(document.queryCommandValue("defaultParagraphSeparator"), "div"), + `${aDescription}: defaultParagraphSeparator value should be "div" when ${ + kIsTextArea ? "<textarea>" : "<input>" + } does not have focus` + ); + }, + }, + ]; + + for (const kTest of kTests) { + const kDescription = + `${aDescription}, execCommand("${kTest.command}", false, ${kTest.param}), ${kTest.value})`; + aTarget.value = "dummy value to ensure the following value setting clear the undo history"; + let value = kTest.value.replace(/[\[\]]/g, ""); + aTarget.value = value; + aTarget.focus(); + aTarget.selectionStart = kTest.value.indexOf("["); + aTarget.selectionEnd = kTest.value.indexOf("]") - 1; + + test( + () => assert_equals(document.queryCommandSupported(kTest.command), kTest.expectedCommandSupported), + `${kDescription}: The command should ${ + kTest.expectedCommandSupported ? "be" : "not be" + } supported` + ); + test( + () => assert_equals(document.queryCommandEnabled(kTest.command), kTest.expectedCommandEnabled), + `${kDescription}: The command should ${ + kTest.expectedCommandEnabled ? "be" : "not be" + } enabled` + ); + + if (!document.queryCommandSupported(kTest.command) || !kTest.expectedCommandSupported) { + continue; + } + + if (kTest.initFunc) { + kTest.initFunc(); + } + + let beforeinput = null; + function onBeforeinput(event) { + beforeinput = event; + } + window.addEventListener("beforeinput", onBeforeinput, {capture: true}); + let input = null; + function onInput(event) { + input = event; + } + window.addEventListener("input", onInput, {capture: true}); + let ret; + test(function () { + ret = document.execCommand(kTest.command, false, kTest.param); + assert_equals(ret, kTest.expectedExecCommandResult); + }, `${kDescription}: execCommand() should return ${kTest.expectedExecCommandResult}`); + test(function () { + let value = aTarget.value.substring(0, aTarget.selectionStart) + + "[" + + aTarget.value.substring(aTarget.selectionStart, aTarget.selectionEnd) + + "]" + + aTarget.value.substring(aTarget.selectionEnd); + assert_equals(value, kTest.expectedValue); + }, `${kDescription}: ${kIsTextArea ? "<textarea>" : "<input>"}.value should be "${kTest.expectedValue}"`); + test(function () { + assert_equals(beforeinput?.inputType, kTest.beforeinputExpected?.inputType); + }, `${kDescription}: beforeinput.inputType should be ${kTest.beforeinputExpected?.inputType}`); + test(function () { + assert_equals(beforeinput?.target, kTest.beforeinputExpected?.target); + }, `${kDescription}: beforeinput.target should be ${kTest.beforeinputExpected?.target}`); + test(function () { + assert_equals(input?.inputType, kTest.inputExpected?.inputType); + }, `${kDescription}: input.inputType should be ${kTest.inputExpected?.inputType}`); + test(function () { + assert_equals(input?.target, kTest.inputExpected?.target); + }, `${kDescription}: input.target should be ${kTest.inputExpected?.target}`); + if (kTest.additionalCheckFunc) { + kTest.additionalCheckFunc(kDescription); + } + window.removeEventListener("beforeinput", onBeforeinput, {capture: true}); + window.removeEventListener("input", onInput, {capture: true}); + } +} + +window.addEventListener("load", runTests, {once: true}); +</script> diff --git a/testing/web-platform/tests/editing/other/exec-command-without-editable-element.tentative.html b/testing/web-platform/tests/editing/other/exec-command-without-editable-element.tentative.html new file mode 100644 index 0000000000..0547140306 --- /dev/null +++ b/testing/web-platform/tests/editing/other/exec-command-without-editable-element.tentative.html @@ -0,0 +1,526 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test that execCommand without editable element</title> +<script src=../include/implementation.js></script> +<script>var testsJsLibraryOnly = true</script> +<script src=../include/tests.js></script> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +"use strict"; + +setup({explicit_done: true}); + +// This test calls execCommand() without editable element in the document, +// but its parent or child document has editable element and it has focus. +// In most cases, execCommand() should do nothing and return false. However, +// "cut", "copy", "paste" and "selectall" commands should work without DOM tree +// modification for making web apps can implement their own editor without +// editable element. +async function runTests() { + let parentWindow = window; + let parentDocument = document; + let parentSelection = parentDocument.getSelection(); + let parentEditor = parentDocument.getElementById("editor"); + parentEditor.focus(); + let iframe = document.getElementsByTagName("iframe")[0]; + let childWindow = iframe.contentWindow; + let childDocument = iframe.contentDocument; + let childSelection = childDocument.getSelection(); + let childEditor = childDocument.getElementById("editor"); + childEditor.focus(); + + // execCommand() in child document shouldn't affect to focused parent + // document. + await doTest(parentWindow, parentDocument, parentSelection, parentEditor, + childWindow, childDocument, childSelection, childEditor, false); + // execCommand() in parent document shouldn't affect to focused child + // document but "cut" and "copy" may affect the focused child document. + await doTest(childWindow, childDocument, childSelection, childEditor, + parentWindow, parentDocument, parentSelection, parentEditor, true); + + done(); +} + +async function doTest(aFocusWindow, aFocusDocument, aFocusSelection, aFocusEditor, + aExecWindow, aExecDocument, aExecSelection, aExecEditor, + aExecInParent) { + const kTests = [ + /** + * command: The command which you test. + * focusContent: Will be set to innerHTML of div#editor element in focused + * document. + * execContent: Will be set to innerHTML of div#editor element in the + * document whose execCommand() will be called. + * initFunc: [optional] If you need to do something before running the + * test, you can do it with a function. + * expectedFocusContent: Expected content and selection in div#editor in + * focused document after calling execCommand(). + * expectedExecContent: Expected content and selection in div#editor in + * the document whose execCommand() is called. + * event: The event which you need to check whether it's fired or not. + * expectedFiredInFocus: true if the event should be fired on the focused + * document node. + * expectedFiredInExec: true if the event should be fired on the document + * node whose execCommand() is called. + * expectedResult: Expected result of execCommand(). + */ + {command: "bold", value: "bold", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "italic", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "underline", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "strikethrough", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "subscript", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "superscript", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + // "cut", "copy" and "paste" command should cause firing corresponding + // events to make web apps be able to implement their own editor even + // if there is no editor and selection is collapsed. + {command: "cut", value: null, + focusContent: "a[b]c", execContent: "ab[]c", + expectedFocusContent: "a[b]c", expectedExecContent: "ab[]c", + event: "cut", expectedFiredInFocus: false, expectedFiredInExec: true, + expectedResult: false, + }, + {command: "cut", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "cut", expectedFiredInFocus: false, expectedFiredInExec: true, + expectedResult: false, + }, + {command: "copy", value: null, + focusContent: "a[b]c", execContent: "ab[]c", + expectedFocusContent: "a[b]c", expectedExecContent: "ab[]c", + event: "copy", expectedFiredInFocus: false, expectedFiredInExec: true, + expectedResult: false, + }, + {command: "copy", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "copy", expectedFiredInFocus: false, expectedFiredInExec: true, + expectedResult: false, + }, + {command: "paste", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + initFunc: () => { aFocusDocument.execCommand("copy", false, "b"); }, + event: "paste", expectedFiredInFocus: false, expectedFiredInExec: true, + expectedResult: false, + }, + {command: "delete", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "forwarddelete", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + // "selectall" command should be available without editable content. + {command: "selectall", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: undefined, + event: "selectionchange", expectedFiredInFocus: false, expectedFiredInExec: true, + expectedResult: true, + }, + {command: "undo", value: null, + focusContent: "a[]c", execContent: "a[b]c", + initFunc: () => { aFocusDocument.execCommand("insertText", false, "b"); }, + expectedFocusContent: "ab[]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "redo", value: null, + focusContent: "a[]c", execContent: "a[b]c", + initFunc: () => { + aFocusDocument.execCommand("insertText", false, "b"); + aFocusDocument.execCommand("undo", false, null); + }, + expectedFocusContent: "a[]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "indent", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "outdent", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "backcolor", value: "#000000", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "forecolor", value: "#F0F0F0", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "hilitecolor", value: "#FFFF00", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "fontname", value: "DummyFont", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "fontsize", value: "5", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "increasefontsize", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "decreasefontsize", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "inserthorizontalrule", value: null, + focusContent: "a[]bc", execContent: "a[]bc", + expectedFocusContent: "a[]bc", expectedExecContent: "a[]bc", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "createlink", value: "foo.html", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "insertimage", value: "no-image.png", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "inserthtml", value: "<b>inserted</b>", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "inserttext", value: "**inserted**", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "justifyleft", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "justifyright", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "justifycenter", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "justifyfull", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "removeformat", value: null, + focusContent: "<b>a[b]c</b>", execContent: "<b>a[b]c</b>", + expectedFocusContent: "<b>a[b]c</b>", expectedExecContent: "<b>a[b]c</b>", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "unlink", value: null, + focusContent: "<a href=\"foo.html\">a[b]c</a>", execContent: "<a href=\"foo.html\">a[b]c</a>", + expectedFocusContent: "<a href=\"foo.html\">a[b]c</a>", expectedExecContent: "<a href=\"foo.html\">a[b]c</a>", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "insertorderedlist", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "insertunorderedlist", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "insertparagraph", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "insertlinebreak", value: null, + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "formatblock", value: "div", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + {command: "heading", value: "h1", + focusContent: "a[b]c", execContent: "a[b]c", + expectedFocusContent: "a[b]c", expectedExecContent: "a[b]c", + event: "input", expectedFiredInFocus: false, expectedFiredInExec: false, + expectedResult: false, + }, + /** + * command: The command which you test. + * state: The state which is used with execCommand(). + * initState: The state which should be set with execCommand() first. + * focusContent: Will be set to innerHTML of div#editor element in focused + * document. + * execContent: Will be set to innerHTML of div#editor element in the + * document whose execCommand() will be called. + * initFunc: [optional] If you need to do something before running the + * test, you can do it with a function. + * expectedSetStateInFocus: Expected queryCommandState() result in focused + * document. + * expectedSetStateInExec: Expected queryCommandState() result in document + * whose execCommand() is called. + * expectedResult: Expected result of execCommand(). + */ + {command: "styleWithCSS", state: "true", initState: "false", + focusContent: "a[b]c", execContent: "a[b]c", + expectedSetStateInFocus: false, expectedSetStateInExec: false, + expectedResult: false, + }, + {command: "contentReadOnly", state: "true", initState: "false", + focusContent: "a[b]c", execContent: "a[b]c", + expectedSetStateInFocus: false, expectedSetStateInExec: false, + expectedResult: false, + }, + {command: "insertBrOnReturn", state: "true", initState: "false", + focusContent: "a[b]c", execContent: "a[b]c", + expectedSetStateInFocus: false, expectedSetStateInExec: false, + expectedResult: false, + }, + {command: "defaultParagraphSeparator", state: "div", initState: "p", + focusContent: "a[b]c", execContent: "a[b]c", + expectedSetStateInFocus: false, expectedSetStateInExec: false, + expectedResult: false, + }, + {command: "defaultParagraphSeparator", state: "p", initState: "div", + focusContent: "a[b]c", execContent: "a[b]c", + expectedSetStateInFocus: false, expectedSetStateInExec: false, + expectedResult: false, + }, + {command: "enableObjectResizing", state: "true", initState: "false", + focusContent: "a[b]c", execContent: "a[b]c", + expectedSetStateInFocus: false, expectedSetStateInExec: false, + expectedResult: false, + }, + {command: "enableInlineTableEditing", state: "true", initState: "false", + focusContent: "a[b]c", execContent: "a[b]c", + expectedSetStateInFocus: false, expectedSetStateInExec: false, + expectedResult: false, + }, + {command: "enableAbsolutePositionEditing", state: "true", initState: "false", + focusContent: "a[b]c", execContent: "a[b]c", + expectedSetStateInFocus: false, expectedSetStateInExec: false, + expectedResult: false, + }, + ]; + + async function waitForCondition(aCheckFunc) { + let retry = 60; + while (retry--) { + if (aCheckFunc()) { + return; + } + await new Promise(resolve => requestAnimationFrame(resolve)); + } + } + + for (const kTest of kTests) { + // Skip unsupported command since it's not purpose of this tests whether + // each command is supported on the browser. + if (!aExecDocument.queryCommandSupported(kTest.command)) { + continue; + } + aExecEditor.removeAttribute("contenteditable"); // Disable commands in the exec document. + let points = setupDiv(aFocusEditor, kTest.focusContent); + aFocusSelection.setBaseAndExtent(points[0], points[1], points[2], points[3]); + points = setupDiv(aExecEditor, kTest.execContent); + aExecSelection.setBaseAndExtent(points[0], points[1], points[2], points[3]); + aFocusWindow.focus(); + aFocusEditor.focus(); + if (kTest.initFunc) { + kTest.initFunc(); + } + if (kTest.state === undefined) { + let eventFiredOnFocusDocument = false; + function handlerOnFocusDocument() { + eventFiredOnFocusDocument = true; + } + aFocusDocument.addEventListener(kTest.event, handlerOnFocusDocument, {capture: true}); + let eventFiredOnExecDocument = false; + function handlerOnExecDocument() { + eventFiredOnExecDocument = true; + } + aExecDocument.addEventListener(kTest.event, handlerOnExecDocument, {capture: true}); + const kDescription = `${aExecInParent ? "Parent" : "Child"}Document.execCommand(${kTest.command}, false, ${kTest.value}) with ${kTest.execContent}`; + test(function () { + let ret = aExecDocument.execCommand(kTest.command, false, kTest.value); + assert_equals(ret, kTest.expectedResult, `execCommand should return ${kTest.expectedResult}`); + }, `${kDescription}: calling execCommand`); + if (kTest.event === "selectionchange") { + test(function () { + assert_false(eventFiredOnFocusDocument, + `"${kTest.event}" event should not be fired synchronously on focused document`); + assert_false(eventFiredOnExecDocument, + `"${kTest.event}" event should not be fired synchronously on executed document`); + }, `${kDescription}: checking unexpected synchronous event`); + await waitForCondition(() => eventFiredOnFocusDocument && eventFiredOnExecDocument); + // TODO: Whether select all changes selection in the focused document depends on the + // implementation of "Select All". + } else { + test(function () { + assert_equals(eventFiredOnFocusDocument, kTest.expectedFiredInFocus, + `"${kTest.event}" event should${kTest.expectedFiredInFocus ? "" : " not"} be fired`); + }, `${kDescription}: checking event on focused document`); + } + test(function () { + assert_equals(eventFiredOnExecDocument, kTest.expectedFiredInExec, + `"${kTest.event}" event should${kTest.expectedFiredInExec ? "" : " not"} be fired`); + }, `${kDescription}: checking event on executed document`); + test(function () { + if (aFocusSelection.rangeCount) { + addBrackets(aFocusSelection.getRangeAt(0)); + } + assert_equals(aFocusEditor.innerHTML, kTest.expectedFocusContent); + }, `${kDescription}: checking result content in focused document`); + test(function () { + if (kTest.command === "selectall") { + assert_true(aExecSelection.rangeCount > 0); + assert_equals( + aExecSelection.toString().replace(/[\r\n]/g, ""), + aExecDocument.body.textContent.replace(/[\r\n]/g, "") + ); + } else { + if (aExecSelection.rangeCount) { + addBrackets(aExecSelection.getRangeAt(0)); + } + assert_equals(aExecEditor.innerHTML, kTest.expectedExecContent); + } + }, `${kDescription}: checking result content in executed document`); + aFocusDocument.removeEventListener(kTest.event, handlerOnFocusDocument, {capture: true}); + aExecDocument.removeEventListener(kTest.event, handlerOnExecDocument, {capture: true}); + aExecEditor.setAttribute("contenteditable", ""); + } else { + const kDescription = `${aExecInParent ? "Parent" : "Child"}Document.execCommand(${kTest.command}, false, ${kTest.state})`; + test(function () { + let ret = aExecDocument.execCommand(kTest.command, false, kTest.initState); + assert_equals(ret, kTest.expectedResult, `execCommand should return ${kTest.expectedResult}`); + }, `${kDescription}: calling execCommand to initialize`); + let hasSetState = false; + test(function () { + hasSetState = aExecDocument.queryCommandState(kTest.command); + assert_equals(hasSetState, kTest.expectedSetStateInExec, `queryCommandState on executed document should return ${kTest.expectedSetState}`); + }, `${kDescription}: calling queryCommandState on executed document after initializing`); + test(function () { + let ret = aFocusDocument.queryCommandState(kTest.command); + assert_equals(ret, kTest.expectedSetStateInFocus, `queryCommandState on focus document should return ${kTest.expectedSetState}`); + }, `${kDescription}: calling queryCommandState on focus document after initializing`); + if (hasSetState) { + test(function () { + let ret = aExecDocument.queryCommandValue(kTest.command); + assert_equals(ret, kTest.initState, `queryCommandValue on executed document should return ${kTest.initState}`); + }, `${kDescription}: calling queryCommandValue on executed document after initializing`); + } + test(function () { + let ret = aExecDocument.execCommand(kTest.command, false, kTest.state); + assert_equals(ret, kTest.expectedResult, `execCommand should return ${kTest.expectedResult}`); + }, `${kDescription}: calling execCommand to set state`); + test(function () { + hasSetState = aExecDocument.queryCommandState(kTest.command); + assert_equals(hasSetState, kTest.expectedSetStateInExec, `queryCommandState should return ${kTest.expectedSetState}`); + }, `${kDescription}: calling queryCommandState on executed document`); + test(function () { + let ret = aFocusDocument.queryCommandState(kTest.command); + assert_equals(ret, kTest.expectedSetStateInFocus, `queryCommandState should return ${kTest.expectedSetState}`); + }, `${kDescription}: calling queryCommandState on focused document`); + if (hasSetState) { + test(function () { + let ret = aExecDocument.queryCommandValue(kTest.command); + assert_equals(ret, kTest.state, `queryCommandValue should return ${kTest.initState}`); + }, `${kDescription}: calling queryCommandValue on executed document`); + } + aExecEditor.setAttribute("contenteditable", ""); + test(function () { + let ret = aExecDocument.queryCommandState(kTest.command); + assert_equals(ret, kTest.expectedSetStateInExec, `queryCommandState should return ${kTest.expectedSetState}`); + }, `${kDescription}: calling queryCommandState on executed document after making executed document editable`); + } + } +} + +window.addEventListener("load", runTests, {once: true}); +</script> +<body> +<div contenteditable id="editor">abc</div> +<iframe srcdoc="<div contenteditable id='editor'>def</div><span>ghi</span>"></iframe> +</body> diff --git a/testing/web-platform/tests/editing/other/extra-text-nodes.html b/testing/web-platform/tests/editing/other/extra-text-nodes.html new file mode 100644 index 0000000000..2cd1232d00 --- /dev/null +++ b/testing/web-platform/tests/editing/other/extra-text-nodes.html @@ -0,0 +1,43 @@ +<!doctype html> +<meta charset=utf-8> +<title>Editor should not create unnecessary text nodes</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div contenteditable></div> +<script> +var div = document.querySelector("div"); +var walker = document.createTreeWalker(div, NodeFilter.SHOW_TEXT); +function testInput(html, callback, desc) { + test(() => { + div.innerHTML = html; + div.focus(); + callback(); + + walker.currentNode = walker.root; + var node; + while (node = walker.nextNode()) { + if (node.nextSibling) { + assert_not_equals(node.nextSibling.nodeType, Node.TEXT_NODE, + 'text node "' + node.nodeValue + '" is next to "' + + node.nextSibling.nodeValue + '"'); + } + } + }, desc); +} + +[ + ['<img src="#">foo<img src="#">', + () => { + getSelection().collapse(div, 1); + document.execCommand("inserttext", false, "x"); + }, + "Simple insertText"], + ['<p>editor</p>', + () => { + getSelection().collapse(div.firstChild.firstChild, 3); + document.execCommand("insertlinebreak", false, ""); + document.execCommand("inserttext", false, "x"); + }, + "insertText after insertLineBreak"], +].forEach(([a, b, c]) => testInput(a, b, c)); +</script> diff --git a/testing/web-platform/tests/editing/other/formatblock-preserving-selection.tentative.html b/testing/web-platform/tests/editing/other/formatblock-preserving-selection.tentative.html new file mode 100644 index 0000000000..d10e80b4ea --- /dev/null +++ b/testing/web-platform/tests/editing/other/formatblock-preserving-selection.tentative.html @@ -0,0 +1,136 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?styleWithCSS=false&block=address"> +<meta name="variant" content="?styleWithCSS=false&block=article"> +<meta name="variant" content="?styleWithCSS=false&block=blockquote"> +<meta name="variant" content="?styleWithCSS=false&block=dd"> +<meta name="variant" content="?styleWithCSS=false&block=div"> +<meta name="variant" content="?styleWithCSS=false&block=dt"> +<meta name="variant" content="?styleWithCSS=false&block=h1"> +<meta name="variant" content="?styleWithCSS=false&block=li"> +<meta name="variant" content="?styleWithCSS=false&block=pre"> +<meta name="variant" content="?styleWithCSS=true&block=address"> +<meta name="variant" content="?styleWithCSS=true&block=article"> +<meta name="variant" content="?styleWithCSS=true&block=blockquote"> +<meta name="variant" content="?styleWithCSS=true&block=dd"> +<meta name="variant" content="?styleWithCSS=true&block=div"> +<meta name="variant" content="?styleWithCSS=true&block=dt"> +<meta name="variant" content="?styleWithCSS=true&block=h1"> +<meta name="variant" content="?styleWithCSS=true&block=li"> +<meta name="variant" content="?styleWithCSS=true&block=pre"> +<title>Test preserving selection after formatBlock</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const editor = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editor); +const searchParams = new URLSearchParams(document.location.search); +const styleWithCSS = searchParams.get("styleWithCSS"); +const block = searchParams.get("block"); +document.execCommand("styleWithCSS", false, styleWithCSS); + +// Note that it's not scope of this test how browsers to convert the selected +// content to a block. + +// html: Initial HTML which will be set editor.innerHTML, it should contain +// selection range with a pair of "[" or "{" and "]" or "}". +// expectedSelectedString: After executing "outdent", compared with +// getSelection().toString().replace(/[ \n\r\t]+/g, "") +const tests = [ + { + html: "a[b]c", + expectedSelectedString: "b", + }, + { + html: "a[bc<br>de]f", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[b]c</div>", + expectedSelectedString: "b", + }, + { + html: "<div>a[bc</div><div>de]f</div>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc<br>de]f</div>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[b]c</li></ul>", + expectedSelectedString: "b", + }, + { + html: "<ul><li>a[bc</li><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[bc</li><li>de]f</li><li>ghi</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>abc</li><li>d[ef</li><li>gh]i</li></ul>", + expectedSelectedString: "efgh", + }, + { + html: "<ul><li>abc</li><li>d[ef</li></ul>" + + "<div>gh]i</div>", + expectedSelectedString: "efgh", + }, + { + html: "<div>a[bc</div>" + + "<ul><li>de]f</li><li>ghi</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<table><tr><td>a[b]c</td></tr></table>", + expectedSelectedString: "b", + }, + { + html: "<table><tr><td>a[bc</td><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<table><tr><td>a[bc</td></tr><tr><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc</div>" + + "<table><tr><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<table><tr><td>a[bc</td></tr></table>" + + "<div>de]f</div>", + expectedSelectedString: "bcde", + }, +]; + +for (const t of tests) { + test(() => { + utils.setupEditingHost(t.html); + document.execCommand("formatblock", false, block); + assert_equals( + getSelection().toString().replace(/[ \n\r\t]+/g, ""), + t.expectedSelectedString, + `Result: ${editor.innerHTML}` + ); + }, `Preserve selection after formatBlock with ${block} at ${t.html}`); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/indent-preserving-selection.tentative.html b/testing/web-platform/tests/editing/other/indent-preserving-selection.tentative.html new file mode 100644 index 0000000000..b3fae41faf --- /dev/null +++ b/testing/web-platform/tests/editing/other/indent-preserving-selection.tentative.html @@ -0,0 +1,103 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?styleWithCSS=false"> +<meta name="variant" content="?styleWithCSS=true"> +<title>Test preserving selection after indent</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const editor = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editor); +const styleWithCSS = + new URLSearchParams(document.location.search).get("styleWithCSS"); +document.execCommand("styleWithCSS", false, styleWithCSS); + +// Note that it's not scope of this test how browsers to indent the selected +// content. + +// html: Initial HTML which will be set editor.innerHTML, it should contain +// selection range with a pair of "[" or "{" and "]" or "}". +// expectedSelectedString: After executing "indent", compared with +// getSelection().toString().replace(/[ \n\r]+/g, "") +const tests = [ + { + html: "<div>a[b]c</div>", + expectedSelectedString: "b", + }, + { + html: "<div>a[bc</div><div>de]f</div>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[b]c</li></ul>", + expectedSelectedString: "b", + }, + { + html: "<ul><li>a[bc</li><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc</div>" + + "<ul><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[bc</li></ul>" + + "<ul><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>abc</li><li>d[ef</li></ul>" + + "<ul><li>gh]i</li></ul>", + expectedSelectedString: "efgh", + }, + { + html: "<ul><li>abc</li><li>d[ef</li></ul>" + + "<ul><li>gh]i</li><li>jkl</li></ul>", + expectedSelectedString: "efgh", + }, + { + html: "<ul><ul><li>a[bc</li></ul><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><ul><li>a[bc</li></ul><li>de]f</li></ol>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[bc</li><ul><li>de]f</li></ul></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><li>a[bc</li><ul><li>de]f</li></ul></ol>", + expectedSelectedString: "bcde", + }, +]; + +for (const t of tests) { + test(() => { + utils.setupEditingHost(t.html); + document.execCommand("indent"); + assert_equals( + getSelection().toString().replace(/[ \n\r]+/g, ""), + t.expectedSelectedString, + `Result: ${editor.innerHTML}` + ); + }, `Preserve selection after indent at ${t.html}`); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/input-in-text-control-which-is-also-editing-host.tentative.html b/testing/web-platform/tests/editing/other/input-in-text-control-which-is-also-editing-host.tentative.html new file mode 100644 index 0000000000..1ca22b6730 --- /dev/null +++ b/testing/web-platform/tests/editing/other/input-in-text-control-which-is-also-editing-host.tentative.html @@ -0,0 +1,184 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?textcontrol=text"> +<meta name="variant" content="?textcontrol=password"> +<meta name="variant" content="?textcontrol=textarea"> +<title>Check whether a text control element handles user input when it's an editing host</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +</head> +<body> +<div></div> +<script> +const searchParams = new URLSearchParams(document.location.search); +const textControlType = searchParams.get("textcontrol"); +const textControlDescription = + textControlType == "textarea" + ? "<textarea contenteditable>" + : `<input type="${textControlType}" contenteditable>`; +const div = document.querySelector("div"); + +function createTextControl() { + const textControl = document.createElement( + textControlType == "textarea" ? "textarea" : "input" + ); + if (textControlType != "textarea") { + textControl.type = textControlType; + } + return textControl; +} + +promise_test(async t => { + const textControl = createTextControl(); + div.appendChild(textControl); + textControl.setAttribute("contenteditable", ""); + textControl.focus(); + await (new test_driver.Actions() + .keyDown("a") + .keyUp("a") + .keyDown("b") + .keyUp("b") + .keyDown("c") + .keyUp("c") + .send()); + assert_equals( + textControl.value, + "abc", + `${t.name}: The text control value should be updated` + ); + assert_equals( + document.querySelector("div").textContent.trim(), + "", + `${t.name}: No text should be inserted as a child of the text control` + ); + textControl.remove(); +}, `User typing in ${textControlDescription} should update the value`); + +promise_test(async t => { + const textControl = createTextControl(); + div.appendChild(textControl); + textControl.setAttribute("contenteditable", ""); + textControl.focus(); + document.execCommand("insertText", false, "abc"); + assert_equals( + textControl.value, + "abc", + `${t.name}: The text control value should be updated` + ); + assert_equals( + div.textContent.trim(), + "", + `${t.name}: No text should be inserted as a child of the text control` + ); + textControl.remove(); +}, `execCommand("insertText") in ${textControlDescription} should update the value`); + +promise_test(async t => { + const textControl = createTextControl(); + div.appendChild(textControl); + textControl.focus(); + textControl.setAttribute("contenteditable", ""); + await (new test_driver.Actions() + .keyDown("a") + .keyUp("a") + .keyDown("b") + .keyUp("b") + .keyDown("c") + .keyUp("c") + .send()); + assert_equals( + textControl.value, + "abc", + `${t.name}: The text control value should be updated` + ); + assert_equals( + div.textContent.trim(), + "", + `${t.name}: No text should be inserted as a child of the text control` + ); + textControl.remove(); +}, `User typing in ${textControlDescription} should update the value (became an editing host during focused)`); + +promise_test(async t => { + const textControl = createTextControl(); + div.appendChild(textControl); + textControl.focus(); + textControl.setAttribute("contenteditable", ""); + document.execCommand("insertText", false, "abc"); + assert_equals( + textControl.value, + "abc", + `${t.name}: The text control value should be updated` + ); + assert_equals( + div.textContent.trim(), + "", + `${t.name}: No text should be inserted as a child of the text control` + ); + textControl.remove(); +}, `execCommand("insertText") in ${textControlDescription} should update the value (became an editing host during focused)`); + +if (textControlType != "textarea") { + promise_test(async t => { + const textControl = createTextControl(); + textControl.type = "button"; + div.appendChild(textControl); + textControl.setAttribute("contenteditable", ""); + textControl.focus(); + textControl.type = textControlType; + await (new test_driver.Actions() + .keyDown("a") + .keyUp("a") + .keyDown("b") + .keyUp("b") + .keyDown("c") + .keyUp("c") + .send()); + assert_equals( + textControl.value, + "abc", + `${t.name}: The text control value should be updated` + ); + assert_equals( + document.querySelector("div").textContent.trim(), + "", + `${t.name}: No text should be inserted as a child of the text control` + ); + textControl.remove(); + }, `User typing in ${ + textControlDescription + } should update the value (became an editing host during focused and different type)`); + + promise_test(async t => { + const textControl = createTextControl(); + textControl.type = "button"; + div.appendChild(textControl); + textControl.setAttribute("contenteditable", ""); + textControl.focus(); + textControl.type = textControlType; + document.execCommand("insertText", false, "abc"); + assert_equals( + textControl.value, + "abc", + `${t.name}: The text control value should be updated` + ); + assert_equals( + div.textContent.trim(), + "", + `${t.name}: No text should be inserted as a child of the text control` + ); + textControl.remove(); + }, `execCommand("insertText") in ${ + textControlDescription + } should update the value (became an editing host during focused and different type)`); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/insert-list-preserving-selection.tentative.html b/testing/web-platform/tests/editing/other/insert-list-preserving-selection.tentative.html new file mode 100644 index 0000000000..b7faf4f27a --- /dev/null +++ b/testing/web-platform/tests/editing/other/insert-list-preserving-selection.tentative.html @@ -0,0 +1,155 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?styleWithCSS=false&command=insertOrderedList"> +<meta name="variant" content="?styleWithCSS=false&command=insertUnorderedList"> +<meta name="variant" content="?styleWithCSS=true&command=insertOrderedList"> +<meta name="variant" content="?styleWithCSS=true&command=insertUnorderedList"> +<title>Test preserving selection after insert*List</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const editor = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editor); +const searchParams = new URLSearchParams(document.location.search); +const styleWithCSS = searchParams.get("styleWithCSS"); +const command = searchParams.get("command"); +document.execCommand("styleWithCSS", false, styleWithCSS); + +// Note that it's not scope of this test how browsers to convert the selected +// content to a list. + +// html: Initial HTML which will be set editor.innerHTML, it should contain +// selection range with a pair of "[" or "{" and "]" or "}". +// expectedSelectedString: After executing "outdent", compared with +// getSelection().toString().replace(/[ \n\r\t]+/g, "") +const tests = [ + { + html: "<div>a[b]c</div>", + expectedSelectedString: "b", + }, + { + html: "<div>a[bc</div><div>de]f</div>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc<br>de]f</div>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[b]c</li></ul>", + expectedSelectedString: "b", + }, + { + html: "<ul><li>a[bc</li><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[bc</li><li>de]f</li><li>ghi</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>abc</li><li>d[ef</li><li>gh]i</li></ul>", + expectedSelectedString: "efgh", + }, + { + html: "<ol><li>a[b]c</li></ol>", + expectedSelectedString: "b", + }, + { + html: "<ol><li>a[bc</li><li>de]f</li></ol>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><li>a[bc</li><li>de]f</li><li>ghi</li></ol>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><li>abc</li><li>d[ef</li><li>gh]i</li></ol>", + expectedSelectedString: "efgh", + }, + { + html: "<ul><li>a[bc</li></ul>" + + "<ul><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><li>a[bc</li></ol>" + + "<ul><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[bc</li></ul>" + + "<ol><li>de]f</li></ol>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc</div>" + + "<ul><li>de]f</li><li>ghi</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>abc</li><li>d[ef</li></ul>" + + "<div>gh]i</div>", + expectedSelectedString: "efgh", + }, + { + html: "<div>a[bc</div>" + + "<ol><li>de]f</li><li>ghi</li></ol>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><li>abc</li><li>d[ef</li></ol>" + + "<div>gh]i</div>", + expectedSelectedString: "efgh", + }, + { + html: "<table><tr><td>a[b]c</td></tr></table>", + expectedSelectedString: "b", + }, + { + html: "<table><tr><td>a[bc</td><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<table><tr><td>a[bc</td></tr><tr><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc</div>" + + "<table><tr><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<table><tr><td>a[bc</td></tr></table>" + + "<div>de]f</div>", + expectedSelectedString: "bcde", + }, +]; + +for (const t of tests) { + test(() => { + utils.setupEditingHost(t.html); + document.execCommand(command); + assert_equals( + getSelection().toString().replace(/[ \n\r\t]+/g, ""), + t.expectedSelectedString, + `Result: ${editor.innerHTML}` + ); + }, `Preserve selection after ${command} at ${t.html}`); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/insert-paragraph-in-void-element.tentative.html b/testing/web-platform/tests/editing/other/insert-paragraph-in-void-element.tentative.html new file mode 100644 index 0000000000..c4f788a550 --- /dev/null +++ b/testing/web-platform/tests/editing/other/insert-paragraph-in-void-element.tentative.html @@ -0,0 +1,206 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test insertParagraph when selection collapsed in void element</title> +<meta name="timeout" content="long"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div contenteditable></div> +<script> +"use strict"; + +const voidElements = [ + "br", + "embed", + "hr", + "img", + "input", + "wbr", +]; + +// This test tests whether the inserted text is inserted into, when selection +// is collapsed in the void element. The expected results are based on Blink, +// but the results of <hr>, <embed> and <wbr> elements are not consistent with +// the other elements'. Therefore, Blink also does not pass some of the +// following tests. +// FYI: This cannot be tested by editing/run because there is no way to collapse +// selection into a void element with the framework. + +const editor = document.querySelector("div[contenteditable]"); +for (const container of ["div", "h1", "li"]) { + const openTagOfContainer = (() => { + if (container == "li") { + return "<ol><li>"; + } + return `<${container}>`; + })(); + const closeTagOfContainer = (() => { + if (container == "li") { + return "</li></ol>"; + } + return `</${container}>`; + })(); + const closeAndOpenTagsOfSplitPoint = (() => { + if (container == "li") { + return "</li><li>"; + } + return `</${container}><${container}>`; + })(); + for (const tag of voidElements) { + const visibleTag = tag == "hr" || tag == "img" || tag == "input"; + test(() => { + editor.innerHTML = `${openTagOfContainer}<${tag}>${closeTagOfContainer}`; + const element = editor.querySelector(tag); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertParagraph"); + if (tag == "br") { + if (!visibleTag && container == "h1") { + assert_in_array( + editor.innerHTML, + `${openTagOfContainer}<br>${closeTagOfContainer}<div><br></div>`, + `The paragraph should be inserted before the <${tag}> element` + ); + } else { + assert_in_array( + editor.innerHTML, + `${openTagOfContainer}<br>${closeAndOpenTagsOfSplitPoint}<br>${closeTagOfContainer}`, + `The paragraph should be inserted before the <${tag}> element` + ); + } + } else if (!visibleTag && container == "h1") { + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}<br>${closeTagOfContainer}<div><${tag}></div>`, + `${openTagOfContainer}<br>${closeTagOfContainer}<div><${tag}><br></div>`, + ], + `The paragraph should be inserted before the <${tag}> element` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}<br>${closeAndOpenTagsOfSplitPoint}<${tag}>${closeTagOfContainer}`, + `${openTagOfContainer}<br>${closeAndOpenTagsOfSplitPoint}<${tag}><br>${closeTagOfContainer}`, + ], + `The paragraph should be inserted before the <${tag}> element` + ); + } + }, `Inserting paragraph when selection is collapsed in <${tag}> in <${container}> which is only child`); + + test(() => { + editor.innerHTML = `${openTagOfContainer}<${tag}>${closeTagOfContainer}`; + const element = editor.querySelector(tag); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + element.getBoundingClientRect(); + document.execCommand("insertParagraph"); + if (tag == "br") { + if (!visibleTag && container == "h1") { + assert_in_array( + editor.innerHTML, + `${openTagOfContainer}<br>${closeTagOfContainer}<div><br></div>`, + `The paragraph should be inserted before the <${tag}> element` + ); + } else { + assert_in_array( + editor.innerHTML, + `${openTagOfContainer}<br>${closeAndOpenTagsOfSplitPoint}<br>${closeTagOfContainer}`, + `The paragraph should be inserted before the <${tag}> element` + ); + } + } else if (!visibleTag && container == "h1") { + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}<br>${closeTagOfContainer}<div><${tag}></div>`, + `${openTagOfContainer}<br>${closeTagOfContainer}<div><${tag}><br></div>`, + ], + `The paragraph should be inserted before the <${tag}> element` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}<br>${closeAndOpenTagsOfSplitPoint}<${tag}>${closeTagOfContainer}`, + `${openTagOfContainer}<br>${closeAndOpenTagsOfSplitPoint}<${tag}><br>${closeTagOfContainer}`, + ], + `The paragraph should be inserted before the <${tag}> element` + ); + } + }, `Inserting paragraph when selection is collapsed in <${tag}> in <${container}> which is only child (explicitly flushes maybe pending layout)`); + + test(() => { + editor.innerHTML = `${openTagOfContainer}abc<${tag}>${closeTagOfContainer}`; + const element = editor.querySelector(tag); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertParagraph"); + if (tag == "br") { + if (!visibleTag && container == "h1") { + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}abc${closeTagOfContainer}<div><br></div>`, + `${openTagOfContainer}abc<br>${closeTagOfContainer}<div><br></div>`, + ], + `The paragraph should be split before the <${tag}> element` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}abc${closeAndOpenTagsOfSplitPoint}<br>${closeTagOfContainer}`, + `${openTagOfContainer}abc<br>${closeAndOpenTagsOfSplitPoint}<br>${closeTagOfContainer}`, + ], + `The paragraph should be split before the <${tag}> element` + ); + } + } else if (!visibleTag && container == "h1") { + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}abc${closeTagOfContainer}<div><${tag}></div>`, + `${openTagOfContainer}abc<br>${closeTagOfContainer}<div><${tag}></div>`, + `${openTagOfContainer}abc${closeTagOfContainer}<div><${tag}><br></div>`, + `${openTagOfContainer}abc<br>${closeTagOfContainer}<div><${tag}><br></div>`, + ], + `The paragraph should be split before the <${tag}> element` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}abc${closeAndOpenTagsOfSplitPoint}<${tag}>${closeTagOfContainer}`, + `${openTagOfContainer}abc<br>${closeAndOpenTagsOfSplitPoint}<${tag}>${closeTagOfContainer}`, + `${openTagOfContainer}abc${closeAndOpenTagsOfSplitPoint}<${tag}><br>${closeTagOfContainer}`, + `${openTagOfContainer}abc<br>${closeAndOpenTagsOfSplitPoint}<${tag}><br>${closeTagOfContainer}`, + ], + `The paragraph should be split before the <${tag}> element` + ); + } + }, `Inserting paragraph when selection is collapsed in <${tag}> which follows a text node in <${container}>`); + + test(() => { + editor.innerHTML = `${openTagOfContainer}<${tag}>abc${closeTagOfContainer}`; + const element = editor.querySelector(tag); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertParagraph"); + assert_in_array( + editor.innerHTML, + [ + `${openTagOfContainer}<br>${closeAndOpenTagsOfSplitPoint}<${tag}>abc${closeTagOfContainer}`, + `${openTagOfContainer}<br>${closeAndOpenTagsOfSplitPoint}<${tag}>abc<br>${closeTagOfContainer}`, + ], + `The paragraph should be inserted before the <${tag}> element` + ); + }, `Inserting paragraph when selection is collapsed in <${tag}> which is followed by a text node in <${container}>`); + } +} + +</script> diff --git a/testing/web-platform/tests/editing/other/insert-text-in-void-element.tentative.html b/testing/web-platform/tests/editing/other/insert-text-in-void-element.tentative.html new file mode 100644 index 0000000000..f84d3fce03 --- /dev/null +++ b/testing/web-platform/tests/editing/other/insert-text-in-void-element.tentative.html @@ -0,0 +1,322 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test insertText when selection collapsed in void element</title> +<meta name="timeout" content="long"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div contenteditable></div> +<script> +"use strict"; + +const voidElements = [ + "br", + "embed", + "hr", + "img", + "input", + "wbr", +]; + +// This test tests whether the inserted text is inserted into, when selection +// is collapsed in the void element. The expected results are based on Blink, +// but the results of <embed> and <wbr> elements are not consistent with the +// other elements'. Therefore, Blink also does not pass some of the following +// tests. +// FYI: This cannot be tested by editing/run because there is no way to collapse +// selection into a void element with the framework. + +const editor = document.querySelector("div[contenteditable]"); +for (const tag of voidElements) { + test(() => { + editor.innerHTML = `<div></div>`; + const element = document.createElement(tag); + editor.firstChild.appendChild(element); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertText", false, "abc"); + if (tag == "br") { + assert_in_array( + editor.innerHTML, + [ + "<div>abc</div>", + "<div>abc<br></div>", + ], + `The text should be inserted before the <br> element` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `<div>abc<${tag}></div>`, + `<div>abc<${tag}><br></div>`, + ], + `The text should be inserted before the <${tag}> element` + ); + } + }, `Inserting text when selection is collapsed in <${tag}> which is only child`); + + test(() => { + editor.innerHTML = `<div></div>`; + const element = document.createElement(tag); + editor.firstChild.appendChild(element); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + element.getBoundingClientRect(); + document.execCommand("insertText", false, "abc"); + if (tag == "br") { + assert_in_array( + editor.innerHTML, + [ + "<div>abc</div>", + "<div>abc<br></div>", + ], + `The text should be inserted before the <br> element` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `<div>abc<${tag}></div>`, + `<div>abc<${tag}><br></div>`, + ], + `The text should be inserted before the <${tag}> element` + ); + } + }, `Inserting text when selection is collapsed in <${tag}> which is only child (explicitly flushes maybe pending layout)`); + + test(() => { + editor.innerHTML = `<div>abc</div>`; + const element = document.createElement(tag); + editor.firstChild.appendChild(element); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertText", false, "def"); + if (tag == "br") { + assert_in_array( + editor.innerHTML, + [ + "<div>abcdef</div>", + "<div>abcdef<br></div>", + ], + `The text should be inserted before the <br> element` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `<div>abcdef<${tag}></div>`, + `<div>abcdef<${tag}><br></div>`, + ], + `The text should be inserted before the <${tag}> element` + ); + } + }, `Inserting text when selection is collapsed in <${tag}> which follows a text node`); + + test(() => { + editor.innerHTML = `<div>def</div>`; + const element = document.createElement(tag); + editor.firstChild.insertBefore(element, editor.firstChild.firstChild); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertText", false, "abc"); + if (tag == "br") { + assert_in_array( + editor.innerHTML, + [ + "<div>abc<br>def</div>", + "<div>abc<br>def<br></div>", + ], + `The text should be inserted before the <br> element` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `<div>abc<${tag}>def</div>`, + `<div>abc<${tag}>def<br></div>`, + ], + `The text should be inserted before the <${tag}> element` + ); + } + }, `Inserting text when selection is collapsed in <${tag}> which is followed by a text node`); + + test(() => { + editor.innerHTML = `<div><span></span></div>`; + const element = document.createElement(tag); + editor.firstChild.appendChild(element); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertText", false, "abc"); + if (tag == "br") { + assert_in_array( + editor.innerHTML, + [ + "<div><span></span>abc</div>", + "<div><span></span>abc<br></div>", + ], + `The text should be inserted after the previous empty inline element of <br>` + ); + } else if (tag == "input") { // visible inline? + assert_in_array( + editor.innerHTML, + [ + `<div><span></span>abc<${tag}></div>`, + `<div><span></span>abc<${tag}><br></div>`, + ], + `The text should be inserted after the previous empty inline element of <${tag}>` + ); + } else if (tag == "hr") { // block + assert_in_array( + editor.innerHTML, + [ + `<div><span></span>abc<${tag}></div>`, + `<div><span></span>abc<br><${tag}></div>`, + ], + `The text should be inserted after the previous empty inline element of <${tag}>` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `<div>abc<span></span><${tag}></div>`, + `<div>abc<span></span><${tag}><br></div>`, + ], + `The text should be inserted before the previous empty inline element of <${tag}>` + ); + } + }, `Inserting text when selection is collapsed in <${tag}> which follows an empty <span> element`); + + test(() => { + editor.innerHTML = `<div>abc<span></span></div>`; + const element = document.createElement(tag); + editor.firstChild.appendChild(element); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertText", false, "def"); + if (tag == "br") { + assert_in_array( + editor.innerHTML, + [ + "<div>abcdef<span></span></div>", + "<div>abcdef<span></span><br></div>", + ], + `The text should be inserted at end of the first text node before empty <span> and <br>` + ); + } else if (tag == "hr") { // block + assert_in_array( + editor.innerHTML, + [ + `<div>abc<span></span>def<${tag}></div>`, + `<div>abc<span></span>def<br><${tag}></div>`, + ], + `The text should be inserted after the previous empty inline element of <${tag}> even if the empty element follows a text node` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `<div>abcdef<span></span><${tag}></div>`, + `<div>abcdef<span></span><${tag}><br></div>`, + ], + `The text should be inserted before the previous empty inline element of <${tag}>` + ); + } + }, `Inserting text when selection is collapsed in <${tag}> which follows a text node and an empty <span> element`); + + test(() => { + editor.innerHTML = `<div><span>abc</span></div>`; + const element = document.createElement(tag); + editor.firstChild.appendChild(element); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertText", false, "def"); + if (tag == "br") { + assert_in_array( + editor.innerHTML, + [ + "<div><span>abcdef</span></div>", + "<div><span>abcdef</span><br></div>", + ], + `The text should be inserted at end of the text node in <span>` + ); + } else if (tag == "hr") { // block + assert_in_array( + editor.innerHTML, + [ + `<div><span>abc</span>def<${tag}></div>`, + `<div><span>abc</span>def<br><${tag}></div>`, + ], + `The text should be inserted at after the span and before <${tag}>` + ); + } else { + assert_in_array( + editor.innerHTML, + [ + `<div><span>abcdef</span><${tag}></div>`, + `<div><span>abcdef</span><${tag}><br></div>`, + ], + `The text should be inserted at end of the text node in <span> before <${tag}>` + ); + } + }, `Inserting text when selection is collapsed in <${tag}> which follows a non-empty <span> element`); + + test(() => { + editor.innerHTML = `<div>abc<span></span>\n</div>`; + const element = document.createElement(tag); + editor.firstChild.appendChild(element); + editor.focus(); + const selection = getSelection(); + selection.collapse(element, 0); + document.execCommand("insertText", false, "def"); + if (tag == "br") { + assert_in_array( + editor.innerHTML.replace(/\n/g, " "), + [ + "<div>abcdef<span></span></div>", + "<div>abcdef<span></span><br></div>", + "<div>abcdef<span></span> </div>", + "<div>abcdef<span></span> <br></div>", + ], + `The text should be inserted at end of the first text node with ignoring the empty <span> and invisible text node before <br>` + ); + } else if (tag == "img" || tag == "input") { // visible inline + assert_in_array( + editor.innerHTML.replace(/\n/g, " "), + [ + `<div>abc<span></span> def<${tag}></div>`, + `<div>abc<span></span> def<${tag}><br></div>`, + `<div>abc<span></span> def<${tag}></div>`, + `<div>abc<span></span> def<${tag}><br></div>`, + ], + `The text should be inserted at end of the last visible text node` + ); + } else if (tag == "hr") { // block + assert_in_array( + editor.innerHTML, + [ + `<div>abc<span></span>def<${tag}></div>`, + `<div>abc<span></span>def<br><${tag}></div>`, + ], + `The text should be inserted after the previous empty inline element` + ); + } else { + assert_in_array( + editor.innerHTML.replace(/\n/g, " "), + [ + `<div>abcdef<span></span> <${tag}></div>`, + `<div>abcdef<span></span> <${tag}><br></div>`, + ], + `The text should be inserted before the previous empty inline element` + ); + } + }, `Inserting text when selection is collapsed in <${tag}> which follows a text node, an empty <span> element and white-space only text node`); +} + +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/editing/other/inserthtml-do-not-preserve-inline-styles.html b/testing/web-platform/tests/editing/other/inserthtml-do-not-preserve-inline-styles.html new file mode 100644 index 0000000000..3483f8f995 --- /dev/null +++ b/testing/web-platform/tests/editing/other/inserthtml-do-not-preserve-inline-styles.html @@ -0,0 +1,176 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=italic"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=strikethrough"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=subscript"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=superscript"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=underline"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=fontname"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=fontsize"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=forecolor"> +<meta name="variant" content="?stylesAtInsertionPoint=bold&stylesInserting=hilitecolor"> +<meta name="variant" content="?stylesAtInsertionPoint=italic&stylesInserting=bold"> +<meta name="variant" content="?stylesAtInsertionPoint=strikethrough&stylesInserting=bold"> +<meta name="variant" content="?stylesAtInsertionPoint=subscript&stylesInserting=bold"> +<meta name="variant" content="?stylesAtInsertionPoint=superscript&stylesInserting=bold"> +<meta name="variant" content="?stylesAtInsertionPoint=underline&stylesInserting=bold"> +<meta name="variant" content="?stylesAtInsertionPoint=fontname&stylesInserting=bold"> +<meta name="variant" content="?stylesAtInsertionPoint=forecolor&stylesInserting=bold"> +<meta name="variant" content="?stylesAtInsertionPoint=hilitecolor&stylesInserting=bold"> +<meta name="variant" content="?stylesAtInsertionPoint=bold,italic&stylesInserting=strikethrough,underline"> +<title>insertHTML should not preserve inline styles at insertion point</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body><div contenteditable></div> +<script> +"use strict"; + +const params = new URLSearchParams(location.search.substring(1)); +const stylesAtInsertionPoint = params.get("stylesAtInsertionPoint").split(","); +const stylesInserting = params.get("stylesInserting").split(","); + +function getOpenTagForStyle(style) { + switch (style.toLowerCase()) { + case "bold": + return "<b>"; + case "italic": + return "<i>"; + case "strikethrough": + return "<s>"; + case "subscript": + return "<sub>"; + case "superscript": + return "<sup>"; + case "underline": + return "<u>"; + case "fontname": + return "<font face=\"monospace\">"; + case "fontsize": + return "<font size=\"5\">"; + case "forecolor": + return "<font color=\"#0000FF\">"; + case "hilitecolor": + return "<span style=\"background-color:rgb(0, 255, 255)\">"; + } +} + +function getClosedTagForStyle(style) { + switch (style.toLowerCase()) { + case "bold": + return "</b>"; + case "italic": + return "</i>"; + case "strikethrough": + return "</s>"; + case "subscript": + return "</sub>"; + case "superscript": + return "</sup>"; + case "underline": + return "</u>"; + case "fontname": + case "fontsize": + case "forecolor": + return "</font>"; + case "hilitecolor": + return "</span>"; + } +} + +function openTags(styles) { + let openTags = ""; + for (const style of styles) { + openTags = getOpenTagForStyle(style) + openTags; + } + return openTags; +} + +function closedTags(styles) { + let closedTags = ""; + for (const style of styles) { + closedTags += getClosedTagForStyle(style); + } + return closedTags; +} + +const editingHost = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editingHost); + +function addTest(aTest) { + test(() => { + utils.setupEditingHost(aTest.innerHTML); + document.execCommand("insertHTML", false, aTest.newContent); + utils.normalizeStyleAttributeValues(); + assert_equals(editingHost.innerHTML, aTest.expectedResult, aTest.description); + for (const style of stylesInserting) { + switch (style.toLocaleLowerCase()) { + case "fontsize": + assert_equals( + document.queryCommandValue(style), + "5", + `document.queryCommandValue("${style}")` + ); + break; + case "fontname": + assert_equals( + document.queryCommandValue(style), + "monospace", + `document.queryCommandValue("${style}")` + ); + break; + case "forecolor": + assert_equals( + document.queryCommandValue(style), + "rgb(0, 0, 255)", + `document.queryCommandValue("${style}")` + ); + break; + case "hilitecolor": + assert_equals( + document.queryCommandValue(style), + "rgb(0, 255, 255))", + `document.queryCommandValue("${style}")` + ); + break; + default: + assert_true( + document.queryCommandState(style), + `document.queryCommandState("${style}")` + ); + break; + } + } + }, `insertHTML with "${aTest.newContent}" into ${aTest.innerHTML}`); +} + +addTest({ + innerHTML: `${openTags(stylesAtInsertionPoint)}[]def${closedTags(stylesAtInsertionPoint)}`, + newContent: `${openTags(stylesInserting)}abc${closedTags(stylesInserting)}`, + expectedResult: `${openTags(stylesInserting)}abc${closedTags(stylesInserting)}` + + `${openTags(stylesAtInsertionPoint)}def${closedTags(stylesAtInsertionPoint)}`, + description: "New content should be inserted before the inline containers", +}); +addTest({ + innerHTML: `${openTags(stylesAtInsertionPoint)}abc[]${closedTags(stylesAtInsertionPoint)}`, + newContent: `${openTags(stylesInserting)}def${closedTags(stylesInserting)}`, + expectedResult: `${openTags(stylesAtInsertionPoint)}abc${closedTags(stylesAtInsertionPoint)}` + + `${openTags(stylesInserting)}def${closedTags(stylesInserting)}`, + description: "New content should be appended after the inline containers", +}); +addTest({ + innerHTML: `${openTags(stylesAtInsertionPoint)}a[]c${closedTags(stylesAtInsertionPoint)}`, + newContent: `${openTags(stylesInserting)}b${closedTags(stylesInserting)}`, + expectedResult: `${openTags(stylesAtInsertionPoint)}a${closedTags(stylesAtInsertionPoint)}` + + `${openTags(stylesInserting)}b${closedTags(stylesInserting)}` + + `${openTags(stylesAtInsertionPoint)}c${closedTags(stylesAtInsertionPoint)}`, + description: "The inline containers should be split and new content inserted between them", +}); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/insertlinebreak-with-white-space-style.tentative.html b/testing/web-platform/tests/editing/other/insertlinebreak-with-white-space-style.tentative.html new file mode 100644 index 0000000000..7e362c3571 --- /dev/null +++ b/testing/web-platform/tests/editing/other/insertlinebreak-with-white-space-style.tentative.html @@ -0,0 +1,426 @@ +<!doctype html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<meta name="variant" content="?pre"> +<meta name="variant" content="?pre-wrap"> +<meta name="variant" content="?pre-line"> +<meta name="variant" content="?nowrap"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../include/editor-test-utils.js"></script> +<link rel=stylesheet href=../include/reset.css> +<title>insertlinebreak in white-space specified element</title> +<body><div contenteditable></div></body> +<script> +/** + * The expected behavior is based on Chrome 91. + */ +const style = location.search.substr(1); +const isNewLineSignificant = style == "pre" || style == "pre-wrap" || style == "pre-line"; +const editingHost = document.querySelector("div[contenteditable]"); +for (const defaultParagraphSeparator of ["div", "p"]) { + document.execCommand("defaultparagraphseparator", false, defaultParagraphSeparator); + for (const display of ["block", "inline", "inline-block"]) { + // insertlinebreak at direct child of editing host. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`abc[]`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `abc\n\n`, + `abc\n<br>`, + ], + "A linefeed should be inserted at end" + ); + } else { + assert_equals( + editingHost.innerHTML, + `abc<br><br>`, + "A <br> should be inserted at end" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}">abc[]</div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`[]abc`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `\nabc`, + `\nabc<br>`, + ], + "A linefeed should be inserted at start" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<br>abc`, + `<br>abc<br>`, + ], + "A <br> should be inserted at start" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}">[]abc</div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`a[]bc`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `a\nbc`, + `a\nbc<br>`, + ], + "A linefeed should be inserted" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `a<br>bc`, + `a<br>bc<br>`, + ], + "A <br> should be inserted" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}">a[]bc</div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + // inline styles should be preserved. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`abc[]`); + editingHost.getBoundingClientRect(); + document.execCommand("italic"); + document.execCommand("insertlinebreak"); + document.execCommand("inserttext", false, "def"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `abc\n<i>def</i>`, + `abc\n<i>def\n</i>`, + `abc\n<i>def<br></i>`, + `abc\n<i>def</i>\n`, + `abc\n<i>def</i><br>`, + ], + "The temporary inline style should be applied to next line" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `abc<br><i>def</i>`, + `abc<br><i>def<br></i>`, + `abc<br><i>def</i><br>`, + ], + "The temporary inline style should be applied to next line" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}">abc[]</div> (defaultparagraphseparator: ${defaultParagraphSeparator}) (preserving temporary inline style test)`); + + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<b>abc[]</b>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + document.execCommand("inserttext", false, "def"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<b>abc\ndef</b>`, + `<b>abc\ndef\n</b>`, + `<b>abc\ndef<br></b>`, + `<b>abc\ndef</b>\n`, + `<b>abc\ndef</b><br>`, + ], + "The inline style should be applied to next line" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<b>abc<br>def</b>`, + `<b>abc<br>def<br></b>`, + `<b>abc<br>def</b><br>`, + ], + "The inline style should be applied to next line" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}"><b>abc[]</b></div> (defaultparagraphseparator: ${defaultParagraphSeparator}) (preserving inline style test)`); + + for (const paragraph of ["div", "p"]) { + // insertlinebreak in existing paragraph whose `white-space` is specified. + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph} style="white-space:${style}">abc[]</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} style="white-space:${style}">abc\n\n</${paragraph}>`, + `<${paragraph} style="white-space:${style}">abc\n<br></${paragraph}>`, + ], + "A linefeed should be inserted at end of the paragraph" + ); + } else { + assert_equals( + editingHost.innerHTML, + `<${paragraph} style="white-space:${style}">abc<br><br></${paragraph}>`, + "A <br> should be inserted at end of the paragraph" + ); + } + }, `<div contenteditable style="display:${display}"><${paragraph} style="white-space:${style}">abc[]</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph} style="white-space:${style}">[]abc</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} style="white-space:${style}">\nabc</${paragraph}>`, + `<${paragraph} style="white-space:${style}">\nabc<br></${paragraph}>`, + ], + "A linefeed should be inserted at start" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} style="white-space:${style}"><br>abc</${paragraph}>`, + `<${paragraph} style="white-space:${style}"><br>abc<br></${paragraph}>`, + ], + "A <br> should be inserted at start" + ); + } + }, `<div contenteditable style="display:${display}"><${paragraph} style="white-space:${style}">[]abc</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph} style="white-space:${style}">a[]bc</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} style="white-space:${style}">a\nbc</${paragraph}>`, + `<${paragraph} style="white-space:${style}">a\nbc<br></${paragraph}>`, + ], + "A linefeed should be inserted" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} style="white-space:${style}">a<br>bc</${paragraph}>`, + `<${paragraph} style="white-space:${style}">a<br>bc<br></${paragraph}>`, + ], + "A <br> should be inserted" + ); + } + }, `<div contenteditable style="display:${display}"><${paragraph} style="white-space:${style}">a[]bc</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + // insertlinebreak in existing paragraph. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph}>abc[]</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph}>abc\n\n</${paragraph}>`, + `<${paragraph}>abc\n<br></${paragraph}>`, + ], + "A linefeed should be inserted at end of the paragraph" + ); + } else { + assert_equals( + editingHost.innerHTML, + `<${paragraph}>abc<br><br></${paragraph}>`, + "A <br> should be inserted at end of the paragraph" + ); + } + }, `<div contenteditable style="display:${display}; white-space:${style}"><${paragraph}>abc[]</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph}>[]abc</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph}>\nabc</${paragraph}>`, + `<${paragraph}>\nabc<br></${paragraph}>`, + ], + "A linefeed should be inserted at start" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph}><br>abc</${paragraph}>`, + `<${paragraph}><br>abc<br></${paragraph}>`, + ], + "A <br> should be inserted at start" + ); + } + }, `<div contenteditable style="display:${display}; white-space:${style}"><${paragraph}>[]abc</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph}>a[]bc</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph}>a\nbc</${paragraph}>`, + `<${paragraph}>a\nbc<br></${paragraph}>`, + ], + "A linefeed should be inserted" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph}>a<br>bc</${paragraph}>`, + `<${paragraph}>a<br>bc<br></${paragraph}>`, + ], + "A <br> should be inserted" + ); + } + }, `<div contenteditable style="display:${display}; white-space:${style}"><${paragraph}>a[]bc</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + // Styling the existing paragraph instead of editing host. + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = "block"; + const utils = new EditorTestUtils(editingHost); + const styleAttr = `style="display:${display}; white-space:${style}"`; + utils.setupEditingHost(`<${paragraph} ${styleAttr}>abc[]</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}>abc\n\n</${paragraph}>`, + `<${paragraph} ${styleAttr}>abc\n<br></${paragraph}>`, + ], + "A linefeed should be inserted at end of the paragraph" + ); + } else { + assert_equals( + editingHost.innerHTML, + `<${paragraph} ${styleAttr}>abc<br><br></${paragraph}>`, + "A <br> should be inserted at end of the paragraph" + ); + } + }, `<div contenteditable><${paragraph} style="display:${display}; white-space:${style}">abc[]</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = "block"; + const utils = new EditorTestUtils(editingHost); + const styleAttr = `style="display:${display}; white-space:${style}"`; + utils.setupEditingHost(`<${paragraph} ${styleAttr}>[]abc</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}>\nabc</${paragraph}>`, + `<${paragraph} ${styleAttr}>\nabc<br></${paragraph}>`, + ], + "A linefeed should be inserted at start" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}><br>abc</${paragraph}>`, + `<${paragraph} ${styleAttr}><br>abc<br></${paragraph}>`, + ], + "A <br> should be inserted at start" + ); + } + }, `<div contenteditable><${paragraph} style="display:${display}; white-space:${style}">[]abc</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = "block"; + const utils = new EditorTestUtils(editingHost); + const styleAttr = `style="display:${display}; white-space:${style}"`; + utils.setupEditingHost(`<${paragraph} ${styleAttr}>a[]bc</${paragraph}>`); + editingHost.getBoundingClientRect(); + document.execCommand("insertlinebreak"); + if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}>a\nbc</${paragraph}>`, + `<${paragraph} ${styleAttr}>a\nbc<br></${paragraph}>`, + ], + "A linefeed should be inserted" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}>a<br>bc</${paragraph}>`, + `<${paragraph} ${styleAttr}>a<br>bc<br></${paragraph}>`, + ], + "A <br> should be inserted" + ); + } + }, `<div contenteditable><${paragraph} style="display:${display}; white-space:${style}">a[]bc</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`); + } + } +} +</script> diff --git a/testing/web-platform/tests/editing/other/insertparagraph-in-child-of-head.tentative.html b/testing/web-platform/tests/editing/other/insertparagraph-in-child-of-head.tentative.html new file mode 100644 index 0000000000..2ee8a4b33b --- /dev/null +++ b/testing/web-platform/tests/editing/other/insertparagraph-in-child-of-head.tentative.html @@ -0,0 +1,367 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?designMode=off&white-space=normal"> +<meta name="variant" content="?designMode=off&white-space=pre"> +<meta name="variant" content="?designMode=off&white-space=pre-line"> +<meta name="variant" content="?designMode=off&white-space=pre-wrap"> +<meta name="variant" content="?designMode=on&white-space=normal"> +<meta name="variant" content="?designMode=on&white-space=pre"> +<meta name="variant" content="?designMode=on&white-space=pre-line"> +<meta name="variant" content="?designMode=on&white-space=pre-wrap"> +<title>Insert paragraph in a block element in the head element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<iframe srcdoc=""></iframe> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const whiteSpace = searchParams.get("white-space"); +const testingDesignMode = searchParams.get("designMode") == "on"; + +const isPreformatted = + whiteSpace == "pre" || whiteSpace == "pre-line" || whiteSpace == "pre-wrap"; + +const iframe = document.querySelector("iframe"); +const minimumSrcDoc = + "<html>" + + "<head style='display:block'>" + + "<title>iframe</title>" + + "<script src='/resources/testdriver.js'></" + "script>" + + "<script src='/resources/testdriver-vendor.js'></" + "script>" + + "<script src='/resources/testdriver-actions.js'></" + "script>" + + "</head>" + + "<body><br></body>" + + "</html>"; + +async function initializeAndWaitForLoad(iframeElement, srcDocValue) { + const waitForLoad = + new Promise( + resolve => iframeElement.addEventListener("load", resolve, {once: true}) + ); + iframeElement.srcdoc = srcDocValue; + await waitForLoad; + if (testingDesignMode) { + iframeElement.contentDocument.designMode = "on"; + } else { + iframeElement.contentDocument.documentElement.setAttribute("contenteditable", ""); + } + iframeElement.contentWindow.focus(); + iframeElement.contentDocument.execCommand("defaultParagraphSeparator", false, "div"); +} + +function removeResourceScriptElements(node) { + node.querySelectorAll("script").forEach( + element => { + if (element.getAttribute("src")?.startsWith("/resources")) { + element.remove() + } + } + ); +} + +// DO NOT USE multi-line comment in this file, then, you can comment out +// unnecessary tests when you need to attach the browser with a debugger. + +// <title>, <style> and <script> cannot have <br> element. Therefore, it's +// hard to think what is the best if linefeeds are not preformatted. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const title = childDoc.querySelector("title"); + title.setAttribute("style", `display:block;white-space:${whiteSpace}`); + const utils = new EditorTestUtils(title); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + title.removeAttribute("style"); + childDoc.head.removeAttribute("style"); + + if (!isPreformatted) { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + "<head><title></title></head><body><br></body>", // noop + "<head><title>\n</title></head><body><br></body>", // (collapse white-space) + "<head><title>\n\n</title></head><body><br></body>", // (collapse white-spaces) + ], + "0-2 collapsible linefeed(s) should be inserted" + ); + } else { + // The second linefeed is required to make the last line visible + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>\n\n</title></head><body><br></body>", + "2 preformatted linefeeds should be inserted" + ); + } +}, `insertParagraph in empty <title style="display:block;white-space:${whiteSpace}"> should not split it`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const title = childDoc.querySelector("title"); + title.setAttribute("style", `display:block;white-space:${whiteSpace}`); + const utils = new EditorTestUtils(title); + utils.setupEditingHost("ab[]cd"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + title.removeAttribute("style"); + childDoc.head.removeAttribute("style"); + + if (!isPreformatted) { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + "<head><title>abcd</title></head><body><br></body>", // noop + "<head><title>ab\ncd</title></head><body><br></body>", // (collapsible white-space) + ], + "0-1 collapsible linefeed should be inserted" + ); + } else { + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>ab\ncd</title></head><body><br></body>", + "1 preformatted linefeed should be inserted" + ); + } +}, `insertParagraph in <title style="display:block;white-space:${whiteSpace}"> containing text should not split it`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const style = childDoc.createElement("style"); + style.setAttribute("style", `display:block;white-space:${whiteSpace}`); + childDoc.head.appendChild(style); + const utils = new EditorTestUtils(style); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + style.removeAttribute("style"); + childDoc.head.removeAttribute("style"); + + if (!isPreformatted) { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + "<head><title>iframe</title><style></style></head><body><br></body>", // noop + "<head><title>iframe</title><style>\n</style></head><body><br></body>", // (collapsible white-space) + "<head><title>iframe</title><style>\n\n</style></head><body><br></body>", // (collapsible white-spaces) + ], + "0-2 collapsible linefeeds should be inserted" + ); + } else { + // The second linefeed is required to make the last line visible + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><style>\n\n</style></head><body><br></body>", + "2 preformatted linefeeds should be inserted" + ); + } +}, `insertParagraph in empty <style style="display:block;white-space:${whiteSpace}"> should not split it`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const style = childDoc.createElement("style"); + style.setAttribute("style", `display:block;white-space:${whiteSpace}`); + childDoc.head.appendChild(style); + const utils = new EditorTestUtils(style); + utils.setupEditingHost("ab[]cd"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + style.removeAttribute("style"); + childDoc.head.removeAttribute("style"); + + if (!isPreformatted) { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + "<head><title>iframe</title><style>abcd</style></head><body><br></body>", // noop + "<head><title>iframe</title><style>ab\ncd</style></head><body><br></body>", // (collapsible white-space) + ], + "0-1 collapsible linefeed should be inserted" + ); + } else { + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><style>ab\ncd</style></head><body><br></body>", + "1 preformatted linefeed should be inserted" + ); + } +}, `insertParagraph in <style style="display:block;white-space:${whiteSpace}"> containing text should not split it`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const script = childDoc.createElement("script"); + script.setAttribute("style", `display:block;white-space:${whiteSpace}`); + childDoc.head.appendChild(script); + // Setting <script>.innerHTML causes throwing exception because it runs + // setting script, so we cannot use EditorTestUtils.setupEditingHost. + childDoc.getSelection().collapse(script, 0); + const utils = new EditorTestUtils(childDoc.documentElement); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + script.removeAttribute("style"); + childDoc.head.removeAttribute("style"); + + if (!isPreformatted) { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + "<head><title>iframe</title><script></" + "script></head><body><br></body>", // noop + "<head><title>iframe</title><script>\n</" + "script></head><body><br></body>", // (collapsible white-space) + "<head><title>iframe</title><script>\n\n</" + "script></head><body><br></body>", // (collapsible white-spaces) + ], + "0-2 collapsible linefeeds should be inserted" + ); + } else { + // The second linefeed is required to make the last line visible + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><script>\n\n</" + "script></head><body><br></body>", + "2 preformatted linefeeds should be inserted" + ); + } +}, `insertParagraph in empty <script style="display:block;white-space:${whiteSpace}"> should not split it`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const script = childDoc.createElement("script"); + script.setAttribute("style", `display:block;white-space:${whiteSpace}`); + childDoc.head.appendChild(script); + // Setting <script>.innerHTML causes throwing exception because it runs + // setting script, so we cannot use EditorTestUtils.setupEditingHost. + script.innerText = "// ab// cd"; + childDoc.getSelection().collapse(script.firstChild, "// ab".length); + const utils = new EditorTestUtils(childDoc.documentElement); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + script.removeAttribute("style"); + childDoc.head.removeAttribute("style"); + + if (!isPreformatted) { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + "<head><title>iframe</title><script>// ab// cd</" + "script></head><body><br></body>", // noop + "<head><title>iframe</title><script>// ab\n// cd</" + "script></head><body><br></body>", // (collapsible white-space) + ], + "0-1 linefeed should be inserted" + ); + } else { + assert_equals( + childDoc.documentElement.innerHTML, + "<head><title>iframe</title><script>// ab\n// cd</" + "script></head><body><br></body>", + "1 preformatted linefeed should be inserted" + ); + } +}, `insertParagraph in <script style="display:block;white-space:${whiteSpace}"> containing text should not split it`); + +// <div> element in the <head> should be same behavior as the following result. +// See insertparagraph-in-child-of-html.tentative.html for the detail. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.head.appendChild(div); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + + if (!isPreformatted) { + assert_equals( + childDoc.documentElement.innerHTML, + `<head><title>iframe</title><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div></head><body><br></body>`, + "The <div> should be split" + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}">\n</div></head><body><br></body>`, + `<head><title>iframe</title><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div></head><body><br></body>`, + ], + "The <div> should be split" + ); + } +}, `insertParagraph in empty <div style="white-space:${ + whiteSpace +}"> in the <head> should split the <div>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.head.appendChild(div); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("{}<br>"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + + if (!isPreformatted) { + assert_equals( + childDoc.documentElement.innerHTML, + `<head><title>iframe</title><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div></head><body><br></body>`, + "The <div> should be split" + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}">\n</div></head><body><br></body>`, + `<head><title>iframe</title><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}">\n</div></head><body><br></body>`, + `<head><title>iframe</title><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}"><br></div></head><body><br></body>`, + `<head><title>iframe</title><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div></head><body><br></body>`, + ], + "The <div> should be split" + ); + } +}, `insertParagraph in <div style="white-space:${ + whiteSpace +}"> (containing only a <br>) in the <head> should split the <div> element`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.head.appendChild(div); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("ab[]cd"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + childDoc.head.removeAttribute("style"); + + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title><div style="white-space:${whiteSpace}">ab</div><div style="white-space:${whiteSpace}">cd</div></head><body><br></body>`, + `<head><title>iframe</title><div style="white-space:${whiteSpace}">ab</div><div style="white-space:${whiteSpace}">cd<br></div></head><body><br></body>`, + `<head><title>iframe</title><div style="white-space:${whiteSpace}">ab<br></div><div style="white-space:${whiteSpace}">cd</div></head><body><br></body>`, + `<head><title>iframe</title><div style="white-space:${whiteSpace}">ab<br></div><div style="white-space:${whiteSpace}">cd<br></div></head><body><br></body>`, + ], + "The <div> should be split" + ); +}, `insertParagraph in <div style="white-space:${ + whiteSpace +}"> (containing text) in the <head> should split the <div> element`); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/insertparagraph-in-child-of-html.tentative.html b/testing/web-platform/tests/editing/other/insertparagraph-in-child-of-html.tentative.html new file mode 100644 index 0000000000..a82da32df1 --- /dev/null +++ b/testing/web-platform/tests/editing/other/insertparagraph-in-child-of-html.tentative.html @@ -0,0 +1,344 @@ +<!doctype html> +<html><head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?designMode=off&white-space=normal"> +<meta name="variant" content="?designMode=off&white-space=pre"> +<meta name="variant" content="?designMode=off&white-space=pre-line"> +<meta name="variant" content="?designMode=off&white-space=pre-wrap"> +<meta name="variant" content="?designMode=on&white-space=normal"> +<meta name="variant" content="?designMode=on&white-space=pre"> +<meta name="variant" content="?designMode=on&white-space=pre-line"> +<meta name="variant" content="?designMode=on&white-space=pre-wrap"> +<title>Insert paragraph in a child of the html element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head><body> +<iframe srcdoc=""></iframe> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const whiteSpace = searchParams.get("white-space"); +const testingDesignMode = searchParams.get("designMode") == "on"; + +const isPreformatted = + whiteSpace == "pre" || whiteSpace == "pre-line" || whiteSpace == "pre-wrap"; + +const iframe = document.querySelector("iframe"); +const minimumSrcDoc = + "<html>" + + "<head>" + + "<title>iframe</title>" + + "<script src='/resources/testdriver.js'></" + "script>" + + "<script src='/resources/testdriver-vendor.js'></" + "script>" + + "<script src='/resources/testdriver-actions.js'></" + "script>" + + "</head>" + + "<body><br></body>" + + "</html>"; + +async function initializeAndWaitForLoad(iframeElement, srcDocValue) { + const waitForLoad = + new Promise( + resolve => iframeElement.addEventListener("load", resolve, {once: true}) + ); + iframeElement.srcdoc = srcDocValue; + await waitForLoad; + if (testingDesignMode) { + iframeElement.contentDocument.designMode = "on"; + } else { + iframeElement.contentDocument.documentElement.setAttribute("contenteditable", ""); + } + iframeElement.contentWindow.focus(); + iframeElement.contentDocument.execCommand("defaultParagraphSeparator", false, "div"); +} + +function removeResourceScriptElements(node) { + node.querySelectorAll("script").forEach( + element => { + if (element.getAttribute("src")?.startsWith("/resources")) { + element.remove() + } + } + ); +} + +// DO NOT USE multi-line comment in this file, then, you can comment out +// unnecessary tests when you need to attach the browser with a debugger. + +// For backward compatibility, <div> elements outside <body> should be split +// by insertParagraph. However, should not unwrap existing <div> in any case +// to avoid its child to become children of the <html>. +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.appendChild(div); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + if (!isPreformatted) { + assert_equals( + childDoc.documentElement.innerHTML, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div>`, + "The <div> should be split" + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}">\n</div>`, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div>`, + ], + "The <div> should be split" + ); + } +}, `insertParagraph in empty <div style="white-space:${ + whiteSpace +}"> after <body> should split the <div>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.appendChild(div); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("{}<br>"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + if (!isPreformatted) { + assert_equals( + childDoc.documentElement.innerHTML, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div>`, + "The <div> should be split" + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}">\n</div>`, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}">\n</div>`, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}"><br></div>`, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div>`, + ], + "The <div> should be split" + ); + } +}, `insertParagraph in <div style="white-space:${ + whiteSpace +}"> (containing only a <br>) after <body> should split the <div>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.appendChild(div); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("ab[]cd"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}">ab</div><div style="white-space:${whiteSpace}">cd</div>`, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}">ab<br></div><div style="white-space:${whiteSpace}">cd</div>`, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}">ab</div><div style="white-space:${whiteSpace}">cd<br></div>`, + `<head><title>iframe</title></head><body><br></body><div style="white-space:${whiteSpace}">ab<br></div><div style="white-space:${whiteSpace}">cd<br></div>`, + ], + "The <div> should be split" + ); +}, `insertParagraph in <div style="white-space:${ + whiteSpace +}"> (containing text) after <body> should not create another <div> element`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.insertBefore(div, childDoc.body); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + if (!isPreformatted) { + assert_equals( + childDoc.documentElement.innerHTML, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div><body><br></body>`, + "The <div> should be split" + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}">\n</div><body><br></body>`, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div><body><br></body>`, + ], + "The <div> should be split" + ); + } +}, `insertParagraph in empty <div style="white-space:${ + whiteSpace +}"> before <body> should split the <div>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.insertBefore(div, childDoc.body); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("{}<br>"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + if (!isPreformatted) { + assert_equals( + childDoc.documentElement.innerHTML, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div><body><br></body>`, + "The <div> should be split" + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}">\n</div><body><br></body>`, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}">\n</div><body><br></body>`, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}"><br></div><body><br></body>`, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div><body><br></body>`, + ], + "The <div> should be split" + ); + } +}, `insertParagraph in <div style="white-space:${ + whiteSpace +}"> (containing only a <br>) before <body> should split the <div>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.insertBefore(div, childDoc.body); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("ab[]cd"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}">ab</div><div style="white-space:${whiteSpace}">cd</div><body><br></body>`, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}">ab<br></div><div style="white-space:${whiteSpace}">cd</div><body><br></body>`, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}">ab</div><div style="white-space:${whiteSpace}">cd<br></div><body><br></body>`, + `<head><title>iframe</title></head><div style="white-space:${whiteSpace}">ab<br></div><div style="white-space:${whiteSpace}">cd<br></div><body><br></body>`, + ], + "The <div> should be split" + ); +}, `insertParagraph in <div style="white-space:${ + whiteSpace +}"> (containing text) before <body> should split the <div>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = document.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.insertBefore(div, childDoc.head); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + if (!isPreformatted) { + assert_equals( + childDoc.documentElement.innerHTML, + `<div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div><head><title>iframe</title></head><body><br></body>`, + "The <div> should be split" + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}">\n</div><head><title>iframe</title></head><body><br></body>`, + `<div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div><head><title>iframe</title></head><body><br></body>`, + ], + "The <div> should be split" + ); + } +}, `insertParagraph in empty <div style="white-space:${ + whiteSpace +}"> before <head> should split the <div>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.insertBefore(div, childDoc.head); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("{}<br>"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + if (!isPreformatted) { + assert_equals( + childDoc.documentElement.innerHTML, + `<div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div><head><title>iframe</title></head><body><br></body>`, + "The <div> should be split" + ); + } else { + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}">\n</div><head><title>iframe</title></head><body><br></body>`, + `<div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}">\n</div><head><title>iframe</title></head><body><br></body>`, + `<div style="white-space:${whiteSpace}">\n</div><div style="white-space:${whiteSpace}"><br></div><head><title>iframe</title></head><body><br></body>`, + `<div style="white-space:${whiteSpace}"><br></div><div style="white-space:${whiteSpace}"><br></div><head><title>iframe</title></head><body><br></body>`, + ], + "The <div> should be split" + ); + } +}, `insertParagraph in <div style="white-space:${ + whiteSpace +}"> (containing only a <br>) before <head> should split the <div>`); + +promise_test(async () => { + await initializeAndWaitForLoad(iframe, minimumSrcDoc); + const childDoc = iframe.contentDocument; + const div = childDoc.createElement("div"); + div.setAttribute("style", `white-space:${whiteSpace}`); + childDoc.documentElement.insertBefore(div, childDoc.head); + const utils = new EditorTestUtils(div); + utils.setupEditingHost("ab[]cd"); + await utils.sendEnterKey(); + removeResourceScriptElements(childDoc); + + assert_in_array( + childDoc.documentElement.innerHTML, + [ + `<div style="white-space:${whiteSpace}">ab</div><div style="white-space:${whiteSpace}">cd</div><head><title>iframe</title></head><body><br></body>`, + `<div style="white-space:${whiteSpace}">ab<br></div><div style="white-space:${whiteSpace}">cd</div><head><title>iframe</title></head><body><br></body>`, + `<div style="white-space:${whiteSpace}">ab</div><div style="white-space:${whiteSpace}">cd<br></div><head><title>iframe</title></head><body><br></body>`, + `<div style="white-space:${whiteSpace}">ab<br></div><div style="white-space:${whiteSpace}">cd<br></div><head><title>iframe</title></head><body><br></body>`, + ], + "The <div> should be split" + ); +}, `insertParagraph in <div style="white-space:${ + whiteSpace +}"> (containing text) before <head> should split the <div>`); + +</script> +</body></html> diff --git a/testing/web-platform/tests/editing/other/insertparagraph-in-editing-host-cannot-have-div.tentative.html b/testing/web-platform/tests/editing/other/insertparagraph-in-editing-host-cannot-have-div.tentative.html new file mode 100644 index 0000000000..fe6ea2c183 --- /dev/null +++ b/testing/web-platform/tests/editing/other/insertparagraph-in-editing-host-cannot-have-div.tentative.html @@ -0,0 +1,182 @@ +<!doctype html> +<meta charset=utf-8> +<meta name="timeout" content="long"> + +<meta name="variant" content="?host=span&white-space=normal&display=block&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre&display=block&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre-wrap&display=block&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre-line&display=block&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=normal&display=block&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre&display=block&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre-wrap&display=block&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre-line&display=block&command=insertText"> + +<meta name="variant" content="?host=span&white-space=normal&display=inline&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre&display=inline&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre-wrap&display=inline&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre-line&display=inline&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=normal&display=inline&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre&display=inline&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre-wrap&display=inline&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre-line&display=inline&command=insertText"> + +<meta name="variant" content="?host=span&white-space=normal&display=inline-block&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre&display=inline-block&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre-wrap&display=inline-block&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=pre-line&display=inline-block&command=insertParagraph"> +<meta name="variant" content="?host=span&white-space=normal&display=inline-block&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre&display=inline-block&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre-wrap&display=inline-block&command=insertText"> +<meta name="variant" content="?host=span&white-space=pre-line&display=inline-block&command=insertText"> + +<meta name="variant" content="?host=p&white-space=normal&display=block&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre&display=block&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre-wrap&display=block&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre-line&display=block&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=normal&display=block&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre&display=block&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre-wrap&display=block&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre-line&display=block&command=insertText"> + +<meta name="variant" content="?host=p&white-space=normal&display=inline&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre&display=inline&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre-wrap&display=inline&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre-line&display=inline&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=normal&display=inline&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre&display=inline&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre-wrap&display=inline&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre-line&display=inline&command=insertText"> + +<meta name="variant" content="?host=p&white-space=normal&display=inline-block&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre&display=inline-block&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre-wrap&display=inline-block&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=pre-line&display=inline-block&command=insertParagraph"> +<meta name="variant" content="?host=p&white-space=normal&display=inline-block&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre&display=inline-block&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre-wrap&display=inline-block&command=insertText"> +<meta name="variant" content="?host=p&white-space=pre-line&display=inline-block&command=insertText"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../include/editor-test-utils.js"></script> +<link rel=stylesheet href=../include/reset.css> +<title>Tests for inserting paragraph in editing host which cannot have <div> element as child</title> +<body></body> +<script> +const params = new URLSearchParams(location.search); +const tag = params.get("host"); +const whiteSpace = params.get("white-space"); +const isNewLineSignificant = whiteSpace == "pre" || whiteSpace == "pre-line" || whiteSpace == "pre-wrap"; +const display = params.get("display"); +const command = params.get("command"); +const editingHost = document.createElement(tag); +editingHost.contentEditable = true; +editingHost.style.whiteSpace = whiteSpace; +editingHost.style.display = display; +document.body.appendChild(editingHost); + +function execInsertTextOrParagraphCommand() { + if (command == "insertParagraph") { + document.execCommand(command); + } else { + // Inserting a linefeed by insertText command should be equivalent of insertParagraph. + document.execCommand(command, false, "\n"); + } +} + +const editingHostOpenTagAttrs = `contenteditable style="display:${display}; white-space:${whiteSpace}"`; +test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`a[]b`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // A linefeed should be inserted if it's significant. Otherwise, <br>. + assert_equals( + editingHost.innerHTML, + isNewLineSignificant ? "a\nb" : "a<br>b" + ); +}, `<${tag} ${editingHostOpenTagAttrs}>a[]b</${tag}>`); + +test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<span style="white-space:normal">a[]b</span>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // A <br> element should be inserted if another <span> makes the linebreaks not significant. + assert_equals(editingHost.innerHTML, `<span style="white-space:normal">a<br>b</span>`); +}, `<${tag} ${editingHostOpenTagAttrs}><span style="white-space:normal">a[]b</span></${tag}>`); + +if (isNewLineSignificant) { + test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<span style="white-space:normal"><span style="white-space:${whiteSpace}">a[]b</span></span>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // A linefeed should be inserted even if `white-space` is changed by ancestor, but it's back to preformatted. + assert_equals(editingHost.innerHTML, `<span style="white-space:normal"><span style="white-space:${whiteSpace}">a\nb</span></span>`); + }, `<${tag} ${editingHostOpenTagAttrs}><span style="white-space:normal"><span style="white-space:${whiteSpace}">a[]b</span></span></${tag}>`); +} + +test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<span style="display:block">a[]b</span>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // Split the internal <span> which is styled as block. + assert_equals( + editingHost.innerHTML, + `<span style="display:block">a</span><span style="display:block">b</span>` + ); +}, `<${tag} ${editingHostOpenTagAttrs}><span style="display:block;white-space:normal">a[]b</span></${tag}>`); + +test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<div>a[]b</div>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // Although neither <span> nor <p> can have <div>, but if the insertion point is in the invalid <div>, + // browsers should just split the <div>. + assert_equals(editingHost.innerHTML, `<div>a</div><div>b</div>`); +}, `<${tag} ${editingHostOpenTagAttrs}><div>a[]b</div></${tag}>`); + +test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<div style="display:inline">a[]b</div>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // If <div> is styled as inline, it should be treated like <span>. + assert_equals(editingHost.innerHTML, `<div style="display:inline">a\nb</div>`); +}, `<${tag} ${editingHostOpenTagAttrs}><div style="display:inline">a[]b</div></${tag}>`); + +test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<div style="display:inline-block">a[]b</div>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // If <div> is styled as inline-block, it should be treated like <span>. + assert_equals( + editingHost.innerHTML, + isNewLineSignificant + ? `<div style="display:inline-block">a\nb</div>` + : `<div style="display:inline-block">a<br>b</div>` + ); +}, `<${tag} ${editingHostOpenTagAttrs}><div style="display:inline-block">a[]b</div></${tag}>`); + +test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<div style="display:inline;white-space:normal">a[]b</div>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // If <div> is styled as inline, it should be treated like <span>. + assert_equals(editingHost.innerHTML, `<div style="display:inline;white-space:normal">a<br>b</div>`); +}, `<${tag} ${editingHostOpenTagAttrs}><div style="display:inline;white-space:normal">a[]b</div></${tag}>`); + +test(() => { + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<div style="display:inline-block;white-space:normal">a[]b</div>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // If <div> is styled as inline-block, it should be treated like <span>. + assert_equals(editingHost.innerHTML, `<div style="display:inline-block;white-space:normal">a<br>b</div>`); +}, `<${tag} ${editingHostOpenTagAttrs}><div style="display:inline-block;white-space:normal">a[]b</div></${tag}>`); +</script> diff --git a/testing/web-platform/tests/editing/other/insertparagraph-in-inline-editing-host.tentative.html b/testing/web-platform/tests/editing/other/insertparagraph-in-inline-editing-host.tentative.html new file mode 100644 index 0000000000..9baed3f7fa --- /dev/null +++ b/testing/web-platform/tests/editing/other/insertparagraph-in-inline-editing-host.tentative.html @@ -0,0 +1,502 @@ +<!doctype html> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> + +<meta name="variant" content="?white-space=normal&display=inline&method=enter"> +<meta name="variant" content="?white-space=normal&display=inline&method=shift-enter"> +<meta name="variant" content="?white-space=normal&display=inline-block&method=enter"> +<meta name="variant" content="?white-space=normal&display=inline-block&method=shift-enter"> +<meta name="variant" content="?white-space=normal&display=block&method=enter"> +<meta name="variant" content="?white-space=normal&display=block&method=shift-enter"> +<meta name="variant" content="?white-space=normal&display=list-item&method=enter"> +<meta name="variant" content="?white-space=normal&display=list-item&method=shift-enter"> +<meta name="variant" content="?white-space=normal&display=table-cell&method=enter"> +<meta name="variant" content="?white-space=normal&display=table-cell&method=shift-enter"> + +<meta name="variant" content="?white-space=pre&display=inline&method=enter"> +<meta name="variant" content="?white-space=pre&display=inline&method=shift-enter"> +<meta name="variant" content="?white-space=pre&display=inline-block&method=enter"> +<meta name="variant" content="?white-space=pre&display=inline-block&method=shift-enter"> +<meta name="variant" content="?white-space=pre&display=block&method=enter"> +<meta name="variant" content="?white-space=pre&display=block&method=shift-enter"> +<meta name="variant" content="?white-space=pre&display=list-item&method=enter"> +<meta name="variant" content="?white-space=pre&display=list-item&method=shift-enter"> +<meta name="variant" content="?white-space=pre&display=table-cell&method=enter"> +<meta name="variant" content="?white-space=pre&display=table-cell&method=shift-enter"> + +<meta name="variant" content="?white-space=pre-line&display=inline&method=enter"> +<meta name="variant" content="?white-space=pre-line&display=inline&method=shift-enter"> +<meta name="variant" content="?white-space=pre-line&display=inline-block&method=enter"> +<meta name="variant" content="?white-space=pre-line&display=inline-block&method=shift-enter"> +<meta name="variant" content="?white-space=pre-line&display=block&method=enter"> +<meta name="variant" content="?white-space=pre-line&display=block&method=shift-enter"> +<meta name="variant" content="?white-space=pre-line&display=list-item&method=enter"> +<meta name="variant" content="?white-space=pre-line&display=list-item&method=shift-enter"> +<meta name="variant" content="?white-space=pre-line&display=table-cell&method=enter"> +<meta name="variant" content="?white-space=pre-line&display=table-cell&method=shift-enter"> + +<meta name="variant" content="?white-space=pre-wrap&display=inline&method=enter"> +<meta name="variant" content="?white-space=pre-wrap&display=inline&method=shift-enter"> +<meta name="variant" content="?white-space=pre-wrap&display=inline-block&method=enter"> +<meta name="variant" content="?white-space=pre-wrap&display=inline-block&method=shift-enter"> +<meta name="variant" content="?white-space=pre-wrap&display=block&method=enter"> +<meta name="variant" content="?white-space=pre-wrap&display=block&method=shift-enter"> +<meta name="variant" content="?white-space=pre-wrap&display=list-item&method=enter"> +<meta name="variant" content="?white-space=pre-wrap&display=list-item&method=shift-enter"> +<meta name="variant" content="?white-space=pre-wrap&display=table-cell&method=enter"> +<meta name="variant" content="?white-space=pre-wrap&display=table-cell&method=shift-enter"> + +<title>Line breaking in inline editing host</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<div><span contenteditable></span></div> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const testingInsertParagraph = searchParams.get("method") == "enter"; +const whiteSpace = searchParams.get("white-space"); +const display = searchParams.get("display"); + +const isPreformatted = + whiteSpace == "pre" || whiteSpace == "pre-line" || whiteSpace == "pre-wrap"; + +const editingHost = document.querySelector("span[contenteditable]"); +const container = editingHost.parentElement; +const utils = new EditorTestUtils(editingHost); +const modifiers = (() => { + if (testingInsertParagraph) { + return null; + } + // Only Safari treats `Ctrl+Enter` as insertLineBreak + if (navigator.platform.includes("Mac") && + navigator.userAgent.includes("Safari") && + !navigator.userAgent.includes("Chrom")) { + return utils.kControl; + } + // The others including WebKitGTK treat `Shift+Enter` as it. + return utils.kShift; +})(); + +for (const defaultParagraphSeparator of ["div", "p"]) { + document.execCommand( + "defaultParagraphSeparator", + false, + defaultParagraphSeparator + ); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // In this case, the inserting line break is the last visible thing in the + // block. Therefore, additional <br> or a preformatted linefeed is also + // required to make the new line visible. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + '<span contenteditable=""><br><br></span>', + `A <br> and additional <br> should be inserted when ${t.name}` + ); + } else { + assert_in_array( + container.innerHTML, + [ + '<span contenteditable="">\n<br></span>', + '<span contenteditable="">\n\n</span>', + ], + `A linefeed and additional line breaker should be inserted when ${t.name}` + ); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${whiteSpace}">{}</span> (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + const brElement = document.createElement("br"); + try { + container.appendChild(brElement); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // Even if the <span> element is followed by an invisible <br>, it does + // not make the new line in the <span> element visible. Therefore, + // inserting additional line break is required in this case too. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + '<span contenteditable=""><br><br></span><br>', + `A <br> and additional <br> should be inserted when ${t.name}` + ); + } else { + assert_in_array( + container.innerHTML, + [ + `<span contenteditable="">\n\n</span><br>`, + `<span contenteditable="">\n<br></span><br>`, + ], + `A linefeed and additional line break should be inserted when ${t.name}` + ); + } + } finally { + brElement.remove(); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${ + whiteSpace + }">{}</span> followed by a <br> (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + const divElement = document.createElement("div"); + divElement.textContent = "efg"; + try { + container.appendChild(divElement); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // When the <span> element is followed by a <div>, making empty last + // line visible requires an invisible <br> after a line break. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + '<span contenteditable=""><br><br></span><div>efg</div>', + `A <br> and additional <br> should be inserted when ${t.name}` + ); + } else { + assert_in_array( + container.innerHTML, + [ + `<span contenteditable="">\n\n</span><div>efg</div>`, + `<span contenteditable="">\n<br></span><div>efg</div>`, + ], + `A linefeed and additional line break should be inserted when ${t.name}` + ); + } + } finally { + divElement.remove(); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${ + whiteSpace + }">{}</span> followed by a <div> (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + const text = document.createTextNode("abc"); + try { + container.appendChild(text); + utils.setupEditingHost("{}"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // Even if the <span> element is followed by visible text, it does + // not make the new line in the <span> element visible. Therefore, + // inserting additional line break is required in this case too. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + '<span contenteditable=""><br><br></span>abc', + `A <br> and additional <br> should be inserted when ${t.name}` + ); + } else { + assert_in_array( + container.innerHTML, + [ + `<span contenteditable="">\n\n</span>abc`, + `<span contenteditable="">\n<br></span>abc`, + ], + `A linefeed and additional line break should be inserted when ${t.name}` + ); + } + } finally { + text.remove(); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${ + whiteSpace + }">{}</span> followed by text (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + utils.setupEditingHost("{}<br>"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // In this case, there is a <br> element which makes the new line (last + // line) visible. Therefore, only a line break should be inserted. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + `<span contenteditable=""><br><br></span>`, + `A <br> should be inserted when ${t.name}` + ); + } else { + assert_equals( + container.innerHTML, + `<span contenteditable="">\n<br></span>`, + `A <br> should be inserted when ${t.name}` + ); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${whiteSpace}">{}<br></span> (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + utils.setupEditingHost("[]abcd"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + assert_equals( + container.innerHTML, + `<span contenteditable="">${ + isPreformatted ? "\n" : "<br>" + }abcd</span>`, + `${ + isPreformatted ? "A linefeed" : "A <br>" + } should be inserted when ${t.name}` + ); + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${whiteSpace}">[]abcd</span> (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + utils.setupEditingHost("ab[]cd"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + assert_equals( + container.innerHTML, + `<span contenteditable="">ab${ + isPreformatted ? "\n" : "<br>" + }cd</span>`, + `${ + isPreformatted ? "A linefeed" : "A <br>" + } should be inserted when ${t.name}` + ); + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${whiteSpace}">ab[]cd</span> (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + utils.setupEditingHost("abcd[]"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // In this case, the inserting line break is the last visible thing in the + // block. Therefore, additional line break is also required to make the + // new line visible. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + `<span contenteditable="">abcd<br><br></span>`, + `A <br> and additional <br> should be inserted when ${t.name}` + ); + } else { + assert_in_array( + container.innerHTML, + [ + `<span contenteditable="">abcd\n<br></span>`, + `<span contenteditable="">abcd\n\n</span>`, + ], + `A linefeed and additional line break should be inserted when ${t.name}` + ); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${whiteSpace}">abcd[]</span> (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + const brElement = document.createElement("br"); + try { + container.appendChild(brElement); + utils.setupEditingHost("abcd[]"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // Even if the <span> element is followed by an invisible <br>, it does + // not make the new line in the <span> element visible. Therefore, + // inserting additional line break is required in this case too. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + '<span contenteditable="">abcd<br><br></span><br>', + `A <br> and additional <br> should be inserted when ${t.name}` + ); + } else { + assert_in_array( + container.innerHTML, + [ + `<span contenteditable="">abcd\n<br></span><br>`, + `<span contenteditable="">abcd\n\n</span><br>`, + ], + `A linefeed and additional line break should be inserted when ${t.name}` + ); + } + } finally { + brElement.remove(); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${ + whiteSpace + }">abcd[]</span> followed by a <br> element (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + const divElement = document.createElement("div"); + divElement.textContent = "efg"; + try { + container.appendChild(divElement); + utils.setupEditingHost("abcd[]"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // When the <span> element is followed by a <div>, making empty last + // line visible requires an invisible <br> after a line break. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + '<span contenteditable="">abcd<br><br></span><div>efg</div>', + `A <br> and additional <br> should be inserted when ${t.name}` + ); + } else { + assert_in_array( + container.innerHTML, + [ + `<span contenteditable="">abcd\n<br></span><div>efg</div>`, + `<span contenteditable="">abcd\n\n</span><div>efg</div>`, + ], + `A linefeed and additional line break should be inserted when ${t.name}` + ); + } + } finally { + divElement.remove(); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${ + whiteSpace + }">abcd[]</span> followed by a <div> element (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); + + promise_test(async t => { + editingHost.setAttribute( + "style", + `display:${display};white-space:${whiteSpace}` + ); + const text = document.createTextNode("efg"); + try { + container.appendChild(text); + utils.setupEditingHost("abcd[]"); + await utils.sendEnterKey(modifiers); + editingHost.removeAttribute("style"); + // Even if the <span> element is followed by visible text, it does + // not make the new line in the <span> element visible. Therefore, + // inserting additional line break is required in this case too. + if (!isPreformatted) { + assert_equals( + container.innerHTML, + '<span contenteditable="">abcd<br><br></span>efg', + `A <br> and additional <br> should be inserted when ${t.name}` + ); + } else { + assert_in_array( + container.innerHTML, + [ + `<span contenteditable="">abcd\n<br></span>efg`, + `<span contenteditable="">abcd\n\n</span>efg`, + ], + `A linefeed and additional line break should be inserted when ${t.name}` + ); + } + } finally { + text.remove(); + } + }, `${ + testingInsertParagraph ? "insertParagraph" : "insertLineBreak" + } in <span contenteditable style="display:${ + display + };white-space:${ + whiteSpace + }">abcd[]</span> followed by text (defaultParagraphSeparator=${ + defaultParagraphSeparator + })`); +} + +</script> diff --git a/testing/web-platform/tests/editing/other/insertparagraph-in-non-splittable-element.html b/testing/web-platform/tests/editing/other/insertparagraph-in-non-splittable-element.html new file mode 100644 index 0000000000..c77862fecb --- /dev/null +++ b/testing/web-platform/tests/editing/other/insertparagraph-in-non-splittable-element.html @@ -0,0 +1,142 @@ +<!doctype html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>Test for inserting paragraph in non-splittable elements</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<div contenteditable></div> +<script> +"use strict"; + +const editingHost = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editingHost); + +const tests = [ + { + selector: "button", + initial: "<div><button>abc</button></div>", + // <button> can have <br> so that it should be handled as insertLineBreak. + expected: "<div><button><br>abc</button></div>", + }, + { + selector: "caption", + initial: "<div><table><caption>abc</caption><tbody><tr><td>abc</td></tr></tbody></table></div>", + // <caption> can have paragraphs so that it should be handled as in a block. + expected: "<div><table><caption><br>abc</caption><tbody><tr><td>abc</td></tr></tbody></table></div>", + }, + { + selector: "col", + initial: "<div><table><colgroup><col></colgroup><tbody><tr><td>abc</td></tr></tbody></table></div>", + // <col> and its table parents cannot have paragraphs nor <br>, therefore, + // it should be handled outside <table> or the first cell in the table. + expected: [ + "<div><table><colgroup><col></colgroup><tbody><tr><td><br>abc</td></tr></tbody></table></div>", // handled with the first cell case + "<div><br></div><div><table><colgroup><col></colgroup><tbody><tr><td>abc</td></tr></tbody></table></div>", // handled outside the table case + ], + }, + { + selector: "colgroup", + initial: "<div><table><colgroup><col></colgroup><tbody><tr><td>abc</td></tr></tbody></table></div>", + // <colgroup> and its table parents cannot have paragraphs nor <br>, + // therefore, it should be handled outside <table> or the the first cell + // in the table. + expected: [ + "<div><table><colgroup><col></colgroup><tbody><tr><td><br>abc</td></tr></tbody></table></div>", // handled with the first cell case + "<div><br></div><div><table><colgroup><col></colgroup><tbody><tr><td>abc</td></tr></tbody></table></div>", // handled outside the table case + ], + }, + { + selector: "iframe", + initial: "<div><iframe srcdoc=\"abc\"></iframe></div>", + // <iframe> is a replaced element so that it should be handled outside of it. + expected: "<div><br></div><div><iframe srcdoc=\"abc\"></iframe></div>", + }, + { + selector: "legend", + initial: "<div><fieldset><legend>abc</legend></fieldset></div>", + // <fieldset> cannot have multiple <legend> elements so that it should not + // be split, and it cannot have paragraphs so that <br> element should be + // inserted instead. + expected: "<div><fieldset><legend><br>abc</legend></fieldset></div>", + }, + { + selector: "meter", + initial: "<div><meter max=\"100\" value=\"50\">abc</meter></div>", + // <meter> is a replaced element so that it should be handled outside of it. + expected: "<div><br></div><div><meter max=\"100\" value=\"50\">abc</meter></div>", + }, + { + selector: "optgroup", + initial: "<div><select><optgroup><option>abc</option></optgroup></select></div>", + // <optgroup> is a part of <select> and they are replaced so that it should + // be handled outside of it. + expected: "<div><br></div><div><select><optgroup><option>abc</option></optgroup></select></div>", + }, + { + selector: "option", + initial: "<div><select><option>abc</option></select></div>", + // <option> is a part of <select> and they are replaced so that it should + // be handled outside of it. + expected: "<div><select><option>abc</option></select></div>", + }, + { + selector: "progress", + initial: "<div><progress max=\"100\" value=\"50\">abc</progress></div>", + // <meter> is a replaced element so that it should be handled outside of it. + expected: "<div><br></div><div><progress max=\"100\" value=\"50\">abc</progress></div>", + }, + { + selector: "select", + initial: "<div><select><option>abc</option></select></div>", + // <select> is a replaced element so that it should be handled outside of it. + expected: "<div><br></div><div><select><option>abc</option></select></div>", + }, + { + selector: "table", + initial: "<div><table><tbody><tr><td>abc</td></tr></tbody></table></div>", + // <table> cannot have paragraphs nor <br>, therefore, it should be handled + // outside or in the first cell in the table. + expected: [ + "<div><table><tbody><tr><td><br>abc</td></tr></tbody></table></div>", // handled in the first cell case + "<div><br></div><div><table><tbody><tr><td>abc</td></tr></tbody></table></div>", // handled outside the table case + ], + }, + { + selector: "tbody", + initial: "<div><table><tbody><tr><td>abc</td></tr></tbody></table></div>", + // <tbody> and its parent cannot have paragraphs nor <br>, therefore, + // it should be handled outside <table> or the first cell in the table. + expected: [ + "<div><table><tbody><tr><td><br>abc</td></tr></tbody></table></div>", // handled in the next cell case + "<div><br></div><div><table><tbody><tr><td>abc</td></tr></tbody></table></div>", // handled outside the table case + ], + }, + { + selector: "tr", + initial: "<div><table><tbody><tr><td>abc</td></tr></tbody></table></div>", + // <tr> and its table parents cannot have paragraphs nor <br>, therefore, + // it should be handled outside <table> or the first cell in the table. + expected: [ + "<div><table><tbody><tr><td><br>abc</td></tr></tbody></table></div>", // handled in the next cell case + "<div><br></div><div><table><tbody><tr><td>abc</td></tr></tbody></table></div>", // handled outside the table case + ], + }, +]; + +for (const currentTest of tests) { + promise_test(async t => { + editingHost.innerHTML = currentTest.initial; + getSelection().collapse(editingHost.querySelector(currentTest.selector), 0); + await utils.sendEnterKey(); + if (Array.isArray(currentTest.expected)) { + assert_in_array(editingHost.innerHTML, currentTest.expected); + } else { + assert_equals(editingHost.innerHTML, currentTest.expected); + } + }, `insertParagraph in ${currentTest.selector} of ${currentTest.initial}`); +} +</script> diff --git a/testing/web-platform/tests/editing/other/insertparagraph-with-white-space-style.tentative.html b/testing/web-platform/tests/editing/other/insertparagraph-with-white-space-style.tentative.html new file mode 100644 index 0000000000..3c9f1848f5 --- /dev/null +++ b/testing/web-platform/tests/editing/other/insertparagraph-with-white-space-style.tentative.html @@ -0,0 +1,475 @@ +<!doctype html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<meta name="variant" content="?white-space=pre&command=insertParagraph"> +<meta name="variant" content="?white-space=pre-wrap&command=insertParagraph"> +<meta name="variant" content="?white-space=pre-line&command=insertParagraph"> +<meta name="variant" content="?white-space=nowrap&command=insertParagraph"> +<meta name="variant" content="?white-space=pre&command=insertText"> +<meta name="variant" content="?white-space=pre-wrap&command=insertText"> +<meta name="variant" content="?white-space=pre-line&command=insertText"> +<meta name="variant" content="?white-space=nowrap&command=insertText"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../include/editor-test-utils.js"></script> +<link rel=stylesheet href=../include/reset.css> +<title>insertparagraph in white-space specified element</title> +<body><div contenteditable></div></body> +<script> +/** + * This test checks how insertParagraph command and insertText of "\n" works + * in valid styles of parent (editing host itself or in an splittable element). + */ +const params = new URLSearchParams(location.search); +const style = params.get("white-space"); +const isNewLineSignificant = style == "pre" || style == "pre-wrap" || style == "pre-line"; +const command = params.get("command"); +const editingHost = document.querySelector("div[contenteditable]"); +function execInsertTextOrParagraphCommand() { + if (command == "insertParagraph") { + document.execCommand(command); + } else { + // Inserting a linefeed by insertText command should be equivalent of insertParagraph + document.execCommand(command, false, "\n"); + } +} +for (const defaultParagraphSeparator of ["div", "p"]) { + document.execCommand("defaultParagraphSeparator", false, defaultParagraphSeparator); + for (const display of ["block", "inline", "inline-block"]) { + // Inserting paragraph or inserting a linefeed in a text node which is + // a direct child of the editing host. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`abc[]`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // If the editing host is a block, at least the new paragraph should be + // the default paragraph separator element. + if (display == "block") { + assert_in_array( + editingHost.innerHTML, + [ + `abc<${defaultParagraphSeparator}><br></${defaultParagraphSeparator}>`, + `<${defaultParagraphSeparator}>abc</${defaultParagraphSeparator}><${defaultParagraphSeparator}><br></${defaultParagraphSeparator}>`, + ], + "New paragraph should be inserted at end of the editing host" + ); + } + // Otherwise, i.e., the editing host is inline, insert a line break (a + // linefeed or <br>) should be inserted instead because it's better + // look for the users. + // Note that an extra line break is required for making the last line + // visible because the editing host is the last visible inline in the + // parent block (<body>). + else if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `abc\n\n`, + `abc\n<br>`, + ], + "A linefeed should be inserted at end" + ); + } else { + assert_equals( + editingHost.innerHTML, + `abc<br><br>`, + "A <br> should be inserted at end" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}">abc[]</div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Same as above test except the caret position. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`[]abc`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + if (display == "block") { + assert_in_array( + editingHost.innerHTML, + [ + `<${defaultParagraphSeparator}><br></${defaultParagraphSeparator}>abc`, + `<${defaultParagraphSeparator}><br></${defaultParagraphSeparator}><${defaultParagraphSeparator}>abc</${defaultParagraphSeparator}>`, + ], + "New paragraph should be inserted at start of the editing host" + ); + } else if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `\nabc`, + `\nabc<br>`, + ], + "A linefeed should be inserted at start" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<br>abc`, + `<br>abc<br>`, + ], + "A <br> should be inserted at start" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}">[]abc</div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Same as above test except the caret position. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`a[]bc`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + if (display == "block") { + assert_in_array( + editingHost.innerHTML, + [ + `a<${defaultParagraphSeparator}>bc</${defaultParagraphSeparator}>`, + `<${defaultParagraphSeparator}>a</${defaultParagraphSeparator}><${defaultParagraphSeparator}>bc</${defaultParagraphSeparator}>`, + ], + "New paragraph should split the text" + ); + } else if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `a\nbc`, + `a\nbc<br>`, + ], + "A linefeed should be inserted" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `a<br>bc`, + `a<br>bc<br>`, + ], + "A <br> should be inserted" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}">a[]bc</div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Inserting paragraph or inserting a linefeed in a text node after + // executing the "italic" command. The paragraph or line break result + // should be same as above, but the text in the new paragraph or new line + // should be wrapped in <i>. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`abc[]`); + editingHost.getBoundingClientRect(); + document.execCommand("italic"); + execInsertTextOrParagraphCommand(); + document.execCommand("inserttext", false, "def"); + if (display == "block") { + assert_in_array( + editingHost.innerHTML, + [ + `abc<${defaultParagraphSeparator}><i>def</i></${defaultParagraphSeparator}>`, + `<${defaultParagraphSeparator}>abc</${defaultParagraphSeparator}><${defaultParagraphSeparator}><i>def</i></${defaultParagraphSeparator}>`, + `<${defaultParagraphSeparator}>abc</${defaultParagraphSeparator}><${defaultParagraphSeparator}><i>def<br></i></${defaultParagraphSeparator}>`, + `<${defaultParagraphSeparator}>abc</${defaultParagraphSeparator}><${defaultParagraphSeparator}><i>def</i><br></${defaultParagraphSeparator}>`, + ], + "New paragraph should be inserted at end of the editing host whose text should be italic" + ); + } else if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `abc\n<i>def</i>`, + `abc\n<i>def\n</i>`, + `abc\n<i>def<br></i>`, + `abc\n<i>def</i>\n`, + `abc\n<i>def</i><br>`, + ], + "The new line should be italic" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `abc<br><i>def</i>`, + `abc<br><i>def<br></i>`, + `abc<br><i>def</i><br>`, + ], + "The new line should be italic" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}">abc[]</div> (defaultParagraphSeparator: ${defaultParagraphSeparator}) (preserving temporary inline style test)`); + + // Inserting paragraph or inserting a linefeed in a text node which is + // wrapped in a <b>. The paragraph or line break result should be same as + // above, but the <b> element should be duplicated in the new paragraph + // or shouldn't be split if inserting a line break. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<b>abc[]</b>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + document.execCommand("inserttext", false, "def"); + if (display == "block") { + assert_in_array( + editingHost.innerHTML, + [ + `<b>abc</b><${defaultParagraphSeparator}><b>def</b></${defaultParagraphSeparator}>`, + `<${defaultParagraphSeparator}><b>abc</b></${defaultParagraphSeparator}><${defaultParagraphSeparator}><b>def</b></${defaultParagraphSeparator}>`, + `<${defaultParagraphSeparator}><b>abc</b></${defaultParagraphSeparator}><${defaultParagraphSeparator}><b>def<br></b></${defaultParagraphSeparator}>`, + `<${defaultParagraphSeparator}><b>abc</b></${defaultParagraphSeparator}><${defaultParagraphSeparator}><b>def</b><br></${defaultParagraphSeparator}>`, + ], + "New paragraph should be inserted at end of the editing host whose text should be bold" + ); + } else if (isNewLineSignificant) { + assert_in_array( + editingHost.innerHTML, + [ + `<b>abc\ndef</b>`, + `<b>abc\ndef\n</b>`, + `<b>abc\ndef<br></b>`, + `<b>abc\ndef</b>\n`, + `<b>abc\ndef</b><br>`, + ], + "The new line should be bold" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<b>abc<br>def</b>`, + `<b>abc<br>def<br></b>`, + `<b>abc<br>def</b><br>`, + ], + "The new line should be bold" + ); + } + }, `<div contenteditable style="white-space:${style}; display:${display}"><b>abc[]</b></div> (defaultParagraphSeparator: ${defaultParagraphSeparator}) (preserving inline style test)`); + + for (const paragraph of ["div", "p"]) { + // Inserting paragraph or inserting a linefeed in a splittable paragraph + // (<p> or <div>) whose `white-space` is specified. + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph} style="white-space:${style}">abc[]</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // Even if the editing host is inline, the command should be handled + // with in the splittable paragraph. Therefore, the paragraph should + // be just split and the style attribute should be cloned to keep same + // style in the new paragraph. + assert_equals( + editingHost.innerHTML, + `<${paragraph} style="white-space:${style}">abc</${paragraph}><${paragraph} style="white-space:${style}"><br></${paragraph}>`, + "New paragraph should be inserted at end of the paragraph" + ); + }, `<div contenteditable style="display:${display}"><${paragraph} style="white-space:${style}">abc[]</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Same as above test except the caret position. + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph} style="white-space:${style}">[]abc</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} style="white-space:${style}"><br></${paragraph}><${paragraph} style="white-space:${style}">abc</${paragraph}>`, + `<${paragraph} style="white-space:${style}"><br></${paragraph}><${paragraph} style="white-space:${style}">abc<br></${paragraph}>`, + ], + "New paragraph should be inserted at start of the paragraph" + ); + }, `<div contenteditable style="display:${display}"><${paragraph} style="white-space:${style}">[]abc</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Same as above test except the caret position. + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph} style="white-space:${style}">a[]bc</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} style="white-space:${style}">a</${paragraph}><${paragraph} style="white-space:${style}">bc</${paragraph}>`, + `<${paragraph} style="white-space:${style}">a</${paragraph}><${paragraph} style="white-space:${style}">bc<br></${paragraph}>`, + ], + "The paragraph should be split" + ); + }, `<div contenteditable style="display:${display}"><${paragraph} style="white-space:${style}">a[]bc</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Inserting paragraph or inserting a linefeed in a splittable paragraph + // in the editing host whose `white-space` is specified. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph}>abc[]</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // Same as previous tests, the splittable paragraph should be split. + // The `white-space` style of the ancestor block or the inline editing + // host should not affect to the behavior because we can just split + // the paragraph. + assert_equals( + editingHost.innerHTML, + `<${paragraph}>abc</${paragraph}><${paragraph}><br></${paragraph}>`, + "New paragraph should be inserted at end of the paragraph" + ); + }, `<div contenteditable style="display:${display}; white-space:${style}"><${paragraph}>abc[]</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Same as above test except the caret position. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph}>[]abc</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph}><br></${paragraph}><${paragraph}>abc</${paragraph}>`, + `<${paragraph}><br></${paragraph}><${paragraph}>abc<br></${paragraph}>`, + ], + "New paragraph should be inserted at start of the paragraph" + ); + }, `<div contenteditable style="display:${display}; white-space:${style}"><${paragraph}>[]abc</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Same as above test except the caret position. + test(() => { + editingHost.style.whiteSpace = style; + editingHost.style.display = display; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph}>a[]bc</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph}>a</${paragraph}><${paragraph}>bc</${paragraph}>`, + `<${paragraph}>a</${paragraph}><${paragraph}>bc<br></${paragraph}>`, + ], + "The paragraph should be split" + ); + }, `<div contenteditable style="display:${display}; white-space:${style}"><${paragraph}>a[]bc</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Inserting paragraph or inserting a linefeed in a splittable paragraph + // element whose `display` and `white-space` are specified. + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = "block"; + const utils = new EditorTestUtils(editingHost); + const styleAttr = `style="display:${display}; white-space:${style}"`; + utils.setupEditingHost(`<${paragraph} ${styleAttr}>abc[]</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + // If the paragraph is a normal block, we can just split the paragraph. + if (display == "block") { + assert_equals( + editingHost.innerHTML, + `<${paragraph} ${styleAttr}>abc</${paragraph}><${paragraph} ${styleAttr}><br></${paragraph}>`, + "New paragraph should be inserted at end of the paragraph" + ); + } + // Otherwise, the paragraph is an inline, splitting the paragraph + // element does not look like inserting paragraph for the users + // because it would just duplicate the inlined element. + // Therefore, the split paragraph should be wrapped into the new + // paragraph (considered with the default paragraph separator) at + // least. However, <p> cannot contain <p> nor <div> which may be + // styled as inline. Therefore, <div> should be used for the new + // paragraph even if the default paragraph separator is <p>. + else { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}>abc</${paragraph}><div><${paragraph} ${styleAttr}><br></${paragraph}></div>`, + `<div><${paragraph} ${styleAttr}>abc</${paragraph}></div><div><${paragraph} ${styleAttr}><br></${paragraph}></div>`, + ], + "New paragraph should be inserted at end of the paragraph which is wrapped by a new <div>" + ); + } + }, `<div contenteditable><${paragraph} style="display:${display}; white-space:${style}">abc[]</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Same as above test except the caret position. + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = "block"; + const utils = new EditorTestUtils(editingHost); + const styleAttr = `style="display:${display}; white-space:${style}"`; + utils.setupEditingHost(`<${paragraph} ${styleAttr}>[]abc</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + if (display == "block") { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}><br></${paragraph}><${paragraph} ${styleAttr}>abc</${paragraph}>`, + `<${paragraph} ${styleAttr}><br></${paragraph}><${paragraph} ${styleAttr}>abc<br></${paragraph}>`, + ], + "New paragraph should be inserted at start of the paragraph" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<div><${paragraph} ${styleAttr}><br></${paragraph}></div><${paragraph} ${styleAttr}>abc</${paragraph}>`, + `<div><${paragraph} ${styleAttr}><br></${paragraph}></div><${paragraph} ${styleAttr}>abc<br></${paragraph}>`, + `<div><${paragraph} ${styleAttr}><br></${paragraph}></div><div><${paragraph} ${styleAttr}>abc</${paragraph}></div>`, + `<div><${paragraph} ${styleAttr}><br></${paragraph}></div><div><${paragraph} ${styleAttr}>abc<br></${paragraph}></div>`, + ], + "New paragraph should be inserted at start of the paragraph which is wrapped by a new <div>" + ); + } + + }, `<div contenteditable><${paragraph} style="display:${display}; white-space:${style}">[]abc</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + + // Same as above test except the caret position. + test(() => { + editingHost.style.whiteSpace = "normal"; + editingHost.style.display = "block"; + const styleAttr = `style="display:${display}; white-space:${style}"`; + const utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(`<${paragraph} ${styleAttr}>a[]bc</${paragraph}>`); + editingHost.getBoundingClientRect(); + execInsertTextOrParagraphCommand(); + if (display == "block") { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}>a</${paragraph}><${paragraph} ${styleAttr}>bc</${paragraph}>`, + `<${paragraph} ${styleAttr}>a</${paragraph}><${paragraph} ${styleAttr}>bc<br></${paragraph}>`, + ], + "The paragraph should be split" + ); + } else { + assert_in_array( + editingHost.innerHTML, + [ + `<${paragraph} ${styleAttr}>a</${paragraph}><div><${paragraph} ${styleAttr}>bc</${paragraph}></div>`, + `<${paragraph} ${styleAttr}>a</${paragraph}><div><${paragraph} ${styleAttr}>bc<br></${paragraph}></div>`, + `<div><${paragraph} ${styleAttr}>a</${paragraph}></div><div><${paragraph} ${styleAttr}>bc</${paragraph}></div>`, + `<div><${paragraph} ${styleAttr}>a</${paragraph}></div><div><${paragraph} ${styleAttr}>bc<br></${paragraph}></div>`, + ], + "The paragraph should be split and the latter one should be wrapped by a new <div>" + ); + } + }, `<div contenteditable><${paragraph} style="display:${display}; white-space:${style}">a[]bc</${paragraph}></div> (defaultParagraphSeparator: ${defaultParagraphSeparator})`); + } + } +} +</script> diff --git a/testing/web-platform/tests/editing/other/inserttext-after-bold-in-font-face-monospace.html b/testing/web-platform/tests/editing/other/inserttext-after-bold-in-font-face-monospace.html new file mode 100644 index 0000000000..cc937f28f8 --- /dev/null +++ b/testing/web-platform/tests/editing/other/inserttext-after-bold-in-font-face-monospace.html @@ -0,0 +1,32 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div contenteditable><div><br></div></div> +<script> +"use strict"; + +const editingHost = document.querySelector("div[contenteditable]"); +test(() => { + editingHost.focus(); + document.execCommand("fontName", false, "monospace"); + document.execCommand("insertText", false, "abc"); + document.execCommand("insertParagraph"); + document.execCommand("insertText", false, "def "); + document.execCommand("bold"); + document.execCommand("insertText", false, "g"); + assert_in_array( + editingHost.querySelector("div + div").innerHTML, + [ + '<font face="monospace">def <b>g</b></font>', + '<font face="monospace">def <b>g<br></b></font>', + ] + ); +}, ""); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/inserttext-at-end-of-block-when-br-always-block.html b/testing/web-platform/tests/editing/other/inserttext-at-end-of-block-when-br-always-block.html new file mode 100644 index 0000000000..922b8bd1c8 --- /dev/null +++ b/testing/web-platform/tests/editing/other/inserttext-at-end-of-block-when-br-always-block.html @@ -0,0 +1,36 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Typing white-space and another character at end of a block should preserve the white-space when br element is always block</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style> +br { display: block; } +</style> +<script> +"use strict"; + +addEventListener("load", () => { + test( + () => { + const editingHost = document.querySelector("div[contenteditable]"); + editingHost.focus(); + getSelection().collapse(editingHost.firstChild, "abc".length); + document.execCommand("insertText", false, " "); + document.execCommand("insertText", false, "d"); + assert_in_array( + editingHost.innerHTML, + [ + "abc d", + "abc d<br>", + ] + ); + }, + "Inserting white-space and a character at block end should keep the white-space even before block <br> element" + ); +}, {once: true}); +</script> +</head> +<body><div contenteditable>abc</div></body> +</html> diff --git a/testing/web-platform/tests/editing/other/join-different-white-space-style-left-line-and-right-paragraph.html b/testing/web-platform/tests/editing/other/join-different-white-space-style-left-line-and-right-paragraph.html new file mode 100644 index 0000000000..48fa581115 --- /dev/null +++ b/testing/web-platform/tests/editing/other/join-different-white-space-style-left-line-and-right-paragraph.html @@ -0,0 +1,899 @@ +<!doctype html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre-line"> + +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre-line"> + +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre-line"> +<title>Tests for joining first line of right paragraph with its preceding line</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<style> +.pre { + white-space: pre; +} +.preWrap { + white-space: pre-wrap; +} +.preLine { + white-space: pre-line; +} +.nowrap { + white-space: nowrap; +} +</style> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const testingBackspace = searchParams.get("method") == "backspace"; +const testingSelectBoundary = searchParams.get("method") == "select-boundary"; +const commandName = + testingBackspace || testingSelectBoundary ? "delete" : "forwarddelete"; +const editingHost = document.querySelector("div[contenteditable]"); +const caretInLeft = (() => { + if (testingSelectBoundary) { + return "["; + } + return testingBackspace ? "" : "[]"; +})(); +const caretInRight = (() => { + if (testingSelectBoundary) { + return "]"; + } + return testingBackspace ? "[]" : ""; +})(); +const leftWhiteSpace = `white-space:${searchParams.get("left-white-space")}`; +const rightWhiteSpace = `white-space:${searchParams.get("right-white-space")}`; +const leftWhiteSpacePreserveLineBreaks = + searchParams.get("left-white-space") == "pre" || + searchParams.get("left-white-space") == "pre-wrap" || + searchParams.get("left-white-space") == "pre-line"; +const rightWhiteSpacePreserveLineBreaks = + searchParams.get("right-white-space") == "pre" || + searchParams.get("right-white-space") == "pre-wrap" || + searchParams.get("right-white-space") == "pre-line"; +const leftWhiteSpaceIsNormal = + searchParams.get("left-white-space") == "normal"; +const rightWhiteSpaceIsNormal = + searchParams.get("right-white-space") == "normal"; +const leftWhiteSpaceClass = (() => { + switch (searchParams.get("left-white-space")) { + case "pre": + return "pre"; + case "pre-wrap": + return "preWrap"; + case "pre-line": + return "preLine"; + case "nowrap": + return "nowrap"; + default: + return null; + } +})(); +const rightWhiteSpaceClass = (() => { + switch (searchParams.get("right-white-space")) { + case "pre": + return "pre"; + case "pre-wrap": + return "preWrap"; + case "pre-line": + return "preLine"; + case "nowrap": + return "nowrap"; + default: + return null; + } +})(); +const utils = new EditorTestUtils(editingHost); + +const tests = [ + // The cases that the preceding line is a child of parent block whose + // white-space is normal. + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}">${caretInRight}def\nghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}">${caretInRight}def<br>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abcdef" + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + } + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def\nghi</b></div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + `abc<span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def<br>ghi</b></div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abcdef" + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + ]; + } + return [ + `abc<b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + `abc<span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>ghi\njkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<b ${aAttrsInLeftBlock}>def</b><span ${aAttrsInLeftBlock}>ghi</span>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + `abc<span ${aAttrsInLeftBlock}><b>def</b>ghi</span>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>ghi<br>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abc<b>def</b>ghi" + + `<div style="${rightWhiteSpace}">jkl</div>`, + ]; + } + return [ + `abc<b ${aAttrsInLeftBlock}>def</b><span ${aAttrsInLeftBlock}>ghi</span>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + `abc<span ${aAttrsInLeftBlock}><b>def</b>ghi</span>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>\nghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `abc<span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b><br>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abc<b>def</b>" + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + } + return [ + `abc<b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `abc<span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def\n</b>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `abc<span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def<br></b>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abc<b>def</b>" + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + } + return [ + `abc<b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `abc<span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abcdef" + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + } + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div></div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abcdef" + + `<div style="${rightWhiteSpace}"><div>ghi</div></div>`, + ]; + } + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div></div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi\njkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi\njkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi<br>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abcdef" + + `<div style="${rightWhiteSpace}">ghi<br>jkl</div>`, + ]; + } + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi<br>jkl</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div>\njkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>\njkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div><br>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abcdef" + + `<div style="${rightWhiteSpace}"><div>ghi</div><br>jkl</div>`, + ]; + } + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div><br>jkl</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def\nghi</div>jkl\nmno</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl\nmno</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def<br>ghi</div>jkl<br>mno</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abcdef" + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + } + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div>jkl\nmno</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl\nmno</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || !leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `abc${caretInLeft}` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div>jkl<br>mno</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + "abcdef" + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + } + return [ + `abc<span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + }, + skip: !leftWhiteSpaceIsNormal, + }, + + // The cases that the preceding line is a child of block element and has + // different white-space with <span>. + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}">${caretInRight}def\nghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<br>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span>def` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def\nghi</b></div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def<br>ghi</b></div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span><b>def</b>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>ghi\njkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><b ${aAttrsInLeftBlock}>def</b><span ${aAttrsInLeftBlock}>ghi</span>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}><b>def</b>ghi</span>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>ghi<br>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span><b>def</b>ghi` + + `<div style="${rightWhiteSpace}">jkl</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><b ${aAttrsInLeftBlock}>def</b><span ${aAttrsInLeftBlock}>ghi</span>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}><b>def</b>ghi</span>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>\nghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b><br>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span><b>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def\n</b>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def<br></b>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span><b>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><b ${aAttrsInLeftBlock}>def</b>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}><b>def</b></span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span>def` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div></div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span>def` + + `<div style="${rightWhiteSpace}"><div>ghi</div></div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div></div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi\njkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi\njkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi<br>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span>def` + + `<div style="${rightWhiteSpace}">ghi<br>jkl</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}">ghi<br>jkl</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div>\njkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>\njkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div><br>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span>def` + + `<div style="${rightWhiteSpace}"><div>ghi</div><br>jkl</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div><br>jkl</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def\nghi</div>jkl\nmno</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl\nmno</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def<br>ghi</div>jkl<br>mno</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span>def` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div>jkl\nmno</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl\nmno</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || leftWhiteSpaceIsNormal, + }, + { + initialHTML: + `<span style="${leftWhiteSpace}">abc${caretInLeft}</span>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div>jkl<br>mno</div>`, + expectedHTML: aAttrsInLeftBlock => { + if (rightWhiteSpaceIsNormal) { + return [ + `<span style="${leftWhiteSpace}">abc</span>def` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + } + return [ + `<span style="${leftWhiteSpace}">abc</span><span ${aAttrsInLeftBlock}>def</span>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + }, + skip: leftWhiteSpaceIsNormal, + }, +]; + +const rightStyleAttr = new RegExp(`style="${rightWhiteSpace}"`, "g"); +const leftStyleAttr = new RegExp(`style="${leftWhiteSpace}"`, "g"); +const styledRightDiv = new RegExp(`<div style="${rightWhiteSpace}">`, "g"); +for (const t of tests) { + if (t.skip) { + continue; + } + promise_test(async () => { + utils.setupEditingHost(t.initialHTML); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + assert_in_array( + editingHost.innerHTML, + t.expectedHTML(`style="${rightWhiteSpace}"`), + "white-space should be preserved by <span> elements" + ); + }, `${commandName} at ${t.initialHTML.replace(/\n/g, "\\n")}`); + + if (rightWhiteSpaceClass !== null) { + // Replace style attribute with class attribute. + const initialHTMLWithClass = + t.initialHTML.replace( + rightStyleAttr, + `class="${rightWhiteSpaceClass}"` + ); + if (initialHTMLWithClass != t.initialHTML) { + promise_test(async () => { + utils.setupEditingHost(initialHTMLWithClass); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + const expectedHTMLs = []; + for (const styleAndOrClassAttr of [ + `style="${rightWhiteSpace}"`, + `class="${rightWhiteSpaceClass}" style="${rightWhiteSpace}"`, + `style="${rightWhiteSpace}" class="${rightWhiteSpaceClass}"`, + ]) { + for (const origExpectedHTML of t.expectedHTML(styleAndOrClassAttr)) { + expectedHTMLs.push( + origExpectedHTML.replace( + styledRightDiv, + `<div class="${rightWhiteSpaceClass}">` + ) + ); + } + } + assert_in_array( + editingHost.innerHTML, + expectedHTMLs, + "white-space should be preserved by <span> elements with class or style attribute" + ); + }, `${commandName} at ${initialHTMLWithClass.replace(/\n/g, "\\n")}`); + } + } + + if (leftWhiteSpaceClass !== null) { + // Replace style attribute with class attribute. + const initialHTMLWithClass = + t.initialHTML.replace( + leftStyleAttr, + `class="${leftWhiteSpaceClass}"` + ); + if (initialHTMLWithClass != t.initialHTML) { + promise_test(async () => { + utils.setupEditingHost(initialHTMLWithClass); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + const expectedHTMLs = []; + for (const styleAndOrClassAttr of [ + `style="${rightWhiteSpace}"`, + `class="${rightWhiteSpaceClass}" style="${rightWhiteSpace}"`, + `style="${rightWhiteSpace}" class="${rightWhiteSpaceClass}"`, + ]) { + for (const origExpectedHTML of t.expectedHTML(styleAndOrClassAttr)) { + expectedHTMLs.push( + origExpectedHTML.replace( + leftStyleAttr, + `class="${leftWhiteSpaceClass}"` + ) + ); + } + } + assert_in_array( + editingHost.innerHTML, + expectedHTMLs, + "white-space should be preserved by <span> elements with class or style attribute" + ); + }, `${commandName} at ${initialHTMLWithClass.replace(/\n/g, "\\n")}`); + } + } +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/join-different-white-space-style-left-paragraph-and-right-line.html b/testing/web-platform/tests/editing/other/join-different-white-space-style-left-paragraph-and-right-line.html new file mode 100644 index 0000000000..b55cacc44b --- /dev/null +++ b/testing/web-platform/tests/editing/other/join-different-white-space-style-left-paragraph-and-right-line.html @@ -0,0 +1,493 @@ +<!doctype html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre-line"> + +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre-line"> + +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre-line"> +<title>Tests for joining left paragraph and its following line</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<style> +.pre { + white-space: pre; +} +.preWrap { + white-space: pre-wrap; +} +.preLine { + white-space: pre-line; +} +.nowrap { + white-space: nowrap; +} +</style> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const testingBackspace = searchParams.get("method") == "backspace"; +const testingSelectBoundary = searchParams.get("method") == "select-boundary"; +const commandName = + testingBackspace || testingSelectBoundary ? "delete" : "forwarddelete"; +const editingHost = document.querySelector("div[contenteditable]"); +const caretInLeft = (() => { + if (testingSelectBoundary) { + return "["; + } + return testingBackspace ? "" : "[]"; +})(); +const caretInRight = (() => { + if (testingSelectBoundary) { + return "]"; + } + return testingBackspace ? "[]" : ""; +})(); +const leftWhiteSpace = `white-space:${searchParams.get("left-white-space")}`; +const rightWhiteSpace = `white-space:${searchParams.get("right-white-space")}`; +const leftWhiteSpacePreserveLineBreaks = + searchParams.get("left-white-space") == "pre" || + searchParams.get("left-white-space") == "pre-wrap" || + searchParams.get("left-white-space") == "pre-line"; +const rightWhiteSpacePreserveLineBreaks = + searchParams.get("right-white-space") == "pre" || + searchParams.get("right-white-space") == "pre-wrap" || + searchParams.get("right-white-space") == "pre-line"; +const leftWhiteSpaceIsNormal = + searchParams.get("left-white-space") == "normal"; +const rightWhiteSpaceIsNormal = + searchParams.get("right-white-space") == "normal"; +const leftWhiteSpaceClass = (() => { + switch (searchParams.get("left-white-space")) { + case "pre": + return "pre"; + case "pre-wrap": + return "preWrap"; + case "pre-line": + return "preLine"; + case "nowrap": + return "nowrap"; + default: + return null; + } +})(); +const rightWhiteSpaceClass = (() => { + switch (searchParams.get("right-white-space")) { + case "pre": + return "pre"; + case "pre-wrap": + return "preWrap"; + case "pre-line": + return "preLine"; + case "nowrap": + return "nowrap"; + default: + return null; + } +})(); +const utils = new EditorTestUtils(editingHost); + +const tests = [ + // The cases that the following line is a child of parent block whose + // white-space is normal. + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `${caretInRight}def`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>`, + ]; + }, + skip: !rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `${caretInRight}def<br>ghi`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + "ghi", + ]; + }, + skip: !rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `${caretInRight}def<div>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + "<div>ghi</div>", + ]; + }, + skip: !rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<b>${caretInRight}def</b>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>`, + ]; + }, + skip: !rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<b>${caretInRight}def<br>ghi</b>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + "<b>ghi</b>", + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + "<b>ghi</b>", + ]; + }, + skip: !rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<b>${caretInRight}def</b><br>ghi`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + "ghi", + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + "ghi", + ]; + }, + skip: !rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<b>${caretInRight}def<br></b>ghi`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + "ghi", + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + "ghi", + ]; + }, + skip: !rightWhiteSpaceIsNormal, + }, + + // The cases that the following line is a child of block element and has + // different white-space with <span>. + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}">${caretInRight}def</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>`, + ]; + }, + skip: rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}">${caretInRight}def\nghi</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}">${caretInRight}def<br>ghi</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + ]; + }, + skip: rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}"><b>${caretInRight}def\nghi</b></span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<span style="${rightWhiteSpace}"><b>ghi</b></span>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<span style="${rightWhiteSpace}"><b>ghi</b></span>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}"><b>${caretInRight}def<br>ghi</b></span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<span style="${rightWhiteSpace}"><b>ghi</b></span>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<span style="${rightWhiteSpace}"><b>ghi</b></span>`, + ]; + }, + skip: rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}"><b>${caretInRight}def</b>\nghi</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}"><b>${caretInRight}def</b><br>ghi</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + ]; + }, + skip: rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}"><b>${caretInRight}def\n</b>ghi</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}"><b>${caretInRight}def<br></b>ghi</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<span style="${rightWhiteSpace}">ghi</span>`, + ]; + }, + skip: rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}"><b>${caretInRight}def\nghi</b>jkl</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<span style="${rightWhiteSpace}"><b>ghi</b>jkl</span>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<span style="${rightWhiteSpace}"><b>ghi</b>jkl</span>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks || rightWhiteSpaceIsNormal, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<span style="${rightWhiteSpace}"><b>${caretInRight}def<br>ghi</b>jkl</span>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<span style="${rightWhiteSpace}"><b>ghi</b>jkl</span>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<span style="${rightWhiteSpace}"><b>ghi</b>jkl</span>`, + ]; + }, + skip: rightWhiteSpaceIsNormal, + }, +]; + +const rightStyleAttr = new RegExp(`style="${rightWhiteSpace}"`, "g"); +const leftStyleAttr = new RegExp(`style="${leftWhiteSpace}"`, "g"); +const rightStyledSpan = new RegExp(`</div><span style="${rightWhiteSpace}">`, "g"); +for (const t of tests) { + if (t.skip) { + continue; + } + promise_test(async () => { + utils.setupEditingHost(t.initialHTML); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + assert_in_array( + editingHost.innerHTML, + t.expectedHTML(`style="${rightWhiteSpace}"`), + "white-space should be preserved by <span> elements" + ); + }, `${commandName} at ${t.initialHTML.replace(/\n/g, "\\n")}`); + + if (rightWhiteSpaceClass !== null) { + // Replace style attribute with class attribute. + const initialHTMLWithClass = + t.initialHTML.replace( + rightStyleAttr, + `class="${rightWhiteSpaceClass}"` + ); + if (initialHTMLWithClass != t.initialHTML) { + promise_test(async () => { + utils.setupEditingHost(initialHTMLWithClass); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + const expectedHTMLs = []; + for (const styleAndOrClassAttr of [ + `style="${rightWhiteSpace}"`, + `class="${rightWhiteSpaceClass}" style="${rightWhiteSpace}"`, + `style="${rightWhiteSpace}" class="${rightWhiteSpaceClass}"`, + ]) { + for (const origExpectedHTML of t.expectedHTML(styleAndOrClassAttr)) { + expectedHTMLs.push( + origExpectedHTML.replace( + rightStyledSpan, + `</div><span class="${rightWhiteSpaceClass}">` + ) + ); + } + } + assert_in_array( + editingHost.innerHTML, + expectedHTMLs, + "white-space should be preserved by <span> elements with class or style attribute" + ); + }, `${commandName} at ${initialHTMLWithClass.replace(/\n/g, "\\n")}`); + } + } + + if (leftWhiteSpaceClass !== null) { + // Replace style attribute with class attribute. + const initialHTMLWithClass = + t.initialHTML.replace( + leftStyleAttr, + `class="${leftWhiteSpaceClass}"` + ); + if (initialHTMLWithClass != t.initialHTML) { + promise_test(async () => { + utils.setupEditingHost(initialHTMLWithClass); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + const expectedHTMLs = []; + for (const styleAndOrClassAttr of [ + `style="${rightWhiteSpace}"`, + `class="${rightWhiteSpaceClass}" style="${rightWhiteSpace}"`, + `style="${rightWhiteSpace}" class="${rightWhiteSpaceClass}"`, + ]) { + for (const origExpectedHTML of t.expectedHTML(styleAndOrClassAttr)) { + expectedHTMLs.push( + origExpectedHTML.replace( + leftStyleAttr, + `class="${leftWhiteSpaceClass}"` + ) + ); + } + } + assert_in_array( + editingHost.innerHTML, + expectedHTMLs, + "white-space should be preserved by <span> elements with class or style attribute" + ); + }, `${commandName} at ${initialHTMLWithClass.replace(/\n/g, "\\n")}`); + } + } +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/join-different-white-space-style-paragraphs.html b/testing/web-platform/tests/editing/other/join-different-white-space-style-paragraphs.html new file mode 100644 index 0000000000..605f2a4483 --- /dev/null +++ b/testing/web-platform/tests/editing/other/join-different-white-space-style-paragraphs.html @@ -0,0 +1,499 @@ +<!doctype html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=backspace&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=backspace&left-white-space=nowrap&right-white-space=pre-line"> + +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=forwarddelete&left-white-space=nowrap&right-white-space=pre-line"> + +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=normal&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=pre-line"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-wrap&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=pre-line&right-white-space=nowrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=normal"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre-wrap"> +<meta name="variant" content="?method=select-boundary&left-white-space=nowrap&right-white-space=pre-line"> +<title>Tests for joining paragraphs which have different white-space styles</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<style> +.pre { + white-space: pre; +} +.preWrap { + white-space: pre-wrap; +} +.preLine { + white-space: pre-line; +} +.nowrap { + white-space: nowrap; +} +</style> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const testingBackspace = searchParams.get("method") == "backspace"; +const testingSelectBoundary = searchParams.get("method") == "select-boundary"; +const commandName = + testingBackspace || testingSelectBoundary ? "delete" : "forwarddelete"; +const editingHost = document.querySelector("div[contenteditable]"); +const caretInLeft = (() => { + if (testingSelectBoundary) { + return "["; + } + return testingBackspace ? "" : "[]"; +})(); +const caretInRight = (() => { + if (testingSelectBoundary) { + return "]"; + } + return testingBackspace ? "[]" : ""; +})(); +const leftWhiteSpace = `white-space:${searchParams.get("left-white-space")}`; +const rightWhiteSpace = `white-space:${searchParams.get("right-white-space")}`; +const leftWhiteSpacePreserveLineBreaks = + searchParams.get("left-white-space") == "pre" || + searchParams.get("left-white-space") == "pre-wrap" || + searchParams.get("left-white-space") == "pre-line"; +const rightWhiteSpacePreserveLineBreaks = + searchParams.get("right-white-space") == "pre" || + searchParams.get("right-white-space") == "pre-wrap" || + searchParams.get("right-white-space") == "pre-line"; +const leftWhiteSpaceClass = (() => { + switch (searchParams.get("left-white-space")) { + case "pre": + return "pre"; + case "pre-wrap": + return "preWrap"; + case "pre-line": + return "preLine"; + case "nowrap": + return "nowrap"; + default: + return null; + } +})(); +const rightWhiteSpaceClass = (() => { + switch (searchParams.get("right-white-space")) { + case "pre": + return "pre"; + case "pre-wrap": + return "preWrap"; + case "pre-line": + return "preLine"; + case "nowrap": + return "nowrap"; + default: + return null; + } +})(); +const utils = new EditorTestUtils(editingHost); + +const tests = [ + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}">${caretInRight}def</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>`, + ]; + }, + }, + // Only first line of the right paragraph should be merged into the left paragraph. + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}">${caretInRight}def\nghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<br>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + }, + // `white-space` should be preserved with <b>. + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def\nghi</b></div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def<br>ghi</b></div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b ${aAttrsInLeftBlock}>def</b></span></div>` + + `<div style="${rightWhiteSpace}"><b>ghi</b></div>`, + ]; + }, + }, + // `white-space` should be preserved with <b> as far as possible, and create <span> for no container part. + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>ghi\njkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b><span ${aAttrsInLeftBlock}>ghi</span></div>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b>ghi</span></div>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>ghi<br>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b><span ${aAttrsInLeftBlock}>ghi</span></div>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b>ghi</span></div>` + + `<div style="${rightWhiteSpace}">jkl</div>`, + ]; + }, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b>\nghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def</b><br>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def\n</b>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><b>${caretInRight}def<br></b>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<b ${aAttrsInLeftBlock}>def</b></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}><b>def</b></span></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + }, + // nested paragraph cases + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}">ghi</div>`, + ]; + }, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div></div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}"><div>ghi</div></div>`, + ]; + }, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi\njkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}">ghi\njkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def</div>ghi<br>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}">ghi<br>jkl</div>`, + ]; + }, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div>jkl\nmno</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl\nmno</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}">${caretInRight}def<div>ghi</div>jkl<br>mno</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl<br>mno</div>`, + ]; + }, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def\nghi</div>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl</div>`, + ]; + }, + skip: !rightWhiteSpacePreserveLineBreaks, + }, + { + initialHTML: + `<div style="${leftWhiteSpace}">abc${caretInLeft}</div>` + + `<div style="${rightWhiteSpace}"><div>${caretInRight}def<br>ghi</div>jkl</div>`, + expectedHTML: aAttrsInLeftBlock => { + return [ + `<div style="${leftWhiteSpace}">abc<span ${aAttrsInLeftBlock}>def</span></div>` + + `<div style="${rightWhiteSpace}"><div>ghi</div>jkl</div>`, + ]; + }, + }, +]; + +const betweenDivs = /<\/div><div /; +const rightStyleAttr = new RegExp(`style="${rightWhiteSpace}"`, "g"); +const leftStyleAttr = new RegExp(`style="${leftWhiteSpace}"`, "g"); +const styledRightDiv = new RegExp(`<div style="${rightWhiteSpace}">`, "g"); +const styledLeftDiv = new RegExp(`<div style="${leftWhiteSpace}">`, "g"); +for (const t of tests) { + if (t.skip) { + continue; + } + promise_test(async () => { + utils.setupEditingHost(t.initialHTML); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + assert_in_array( + editingHost.innerHTML, + t.expectedHTML(`style="${rightWhiteSpace}"`), + "white-space should be preserved by <span> elements" + ); + }, `${commandName} at ${t.initialHTML.replace(/\n/g, "\\n")}`); + + // Repeat same tests with inserting a line break between the paragraphs. + const initialHTMLWithLineBreak = + t.initialHTML.replace(betweenDivs, "</div>\n<div "); + promise_test(async () => { + utils.setupEditingHost(initialHTMLWithLineBreak); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + assert_in_array( + editingHost.innerHTML, + t.expectedHTML(`style="${rightWhiteSpace}"`), + "white-space should be preserved by <span> elements (testing with a line break between paragraphs)" + ); + }, `${commandName} at ${initialHTMLWithLineBreak.replace(/\n/g, "\\n")}`); + + if (rightWhiteSpaceClass !== null) { + // Replace style attribute with class attribute. + const initialHTMLWithClass = + t.initialHTML.replace( + rightStyleAttr, + `class="${rightWhiteSpaceClass}"` + ); + if (initialHTMLWithClass != t.initialHTML) { + promise_test(async () => { + utils.setupEditingHost(initialHTMLWithClass); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + const expectedHTMLs = []; + for (const styleAndOrClassAttr of [ + `style="${rightWhiteSpace}"`, + `class="${rightWhiteSpaceClass}" style="${rightWhiteSpace}"`, + `style="${rightWhiteSpace}" class="${rightWhiteSpaceClass}"`, + ]) { + for (const origExpectedHTML of t.expectedHTML(styleAndOrClassAttr)) { + expectedHTMLs.push( + origExpectedHTML.replace( + styledRightDiv, + `<div class="${rightWhiteSpaceClass}">` + ) + ); + } + } + assert_in_array( + editingHost.innerHTML, + expectedHTMLs, + "white-space should be preserved by <span> elements with class or style attribute" + ); + }, `${commandName} at ${initialHTMLWithClass.replace(/\n/g, "\\n")}`); + } + } + + if (leftWhiteSpaceClass !== null) { + // Replace style attribute with class attribute. + const initialHTMLWithClass = + t.initialHTML.replace( + leftStyleAttr, + `class="${leftWhiteSpaceClass}"` + ); + if (initialHTMLWithClass != t.initialHTML) { + promise_test(async () => { + utils.setupEditingHost(initialHTMLWithClass); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + const expectedHTMLs = []; + for (const styleAndOrClassAttr of [ + `style="${rightWhiteSpace}"`, + `class="${rightWhiteSpaceClass}" style="${rightWhiteSpace}"`, + `style="${rightWhiteSpace}" class="${rightWhiteSpaceClass}"`, + ]) { + for (const origExpectedHTML of t.expectedHTML(styleAndOrClassAttr)) { + expectedHTMLs.push( + origExpectedHTML.replace( + leftStyleAttr, + `class="${leftWhiteSpaceClass}"` + ) + ); + } + } + assert_in_array( + editingHost.innerHTML, + expectedHTMLs, + "white-space should be preserved by <span> elements with class or style attribute" + ); + }, `${commandName} at ${initialHTMLWithClass.replace(/\n/g, "\\n")}`); + } + } +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/join-pre-and-other-block.html b/testing/web-platform/tests/editing/other/join-pre-and-other-block.html new file mode 100644 index 0000000000..39e455a848 --- /dev/null +++ b/testing/web-platform/tests/editing/other/join-pre-and-other-block.html @@ -0,0 +1,329 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="variant" content="?method=backspace&block=div"> +<meta name="variant" content="?method=backspace&block=p"> +<meta name="variant" content="?method=backspace&block=blockquote"> +<meta name="variant" content="?method=forwarddelete&block=div"> +<meta name="variant" content="?method=forwarddelete&block=p"> +<meta name="variant" content="?method=forwarddelete&block=blockquote"> +<meta name="variant" content="?method=select-boundary&block=div"> +<meta name="variant" content="?method=select-boundary&block=p"> +<meta name="variant" content="?method=select-boundary&block=blockquote"> +<title>Tests for joining pre and other block element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const searchParams = new URLSearchParams(document.location.search); +const testingBackspace = searchParams.get("method") == "backspace"; +const testingSelectBoundary = searchParams.get("method") == "select-boundary"; +const commandName = + testingBackspace || testingSelectBoundary ? "delete" : "forwarddelete"; +const editingHost = document.querySelector("div[contenteditable]"); +const caretInLeft = (() => { + if (testingSelectBoundary) { + return "["; + } + return testingBackspace ? "" : "[]"; +})(); +const caretInRight = (() => { + if (testingSelectBoundary) { + return "]"; + } + return testingBackspace ? "[]" : ""; +})(); +const tag = searchParams.get("block"); + +// These expectations are odd because they don't preserve white-space style +// coming from another element. However, this is traditional behavior so that +// browsers should not change the behavior. +const tests = [ + { + initialHTML: + `<pre>abc${caretInLeft}</pre>` + + `<${tag}>${caretInRight}def</${tag}>`, + expectedHTML: [ + "<pre>abcdef</pre>", + ], + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre>${caretInRight}def</pre>`, + expectedHTML: [ + `<${tag}>abcdef</${tag}>`, + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<span style="white-space:pre">def</span></${tag}>`, + ], + }, + { + initialHTML: + `<pre>abc${caretInLeft}</pre>` + + `<${tag}>${caretInRight}def<br>ghi</${tag}>`, + expectedHTML: [ + `<pre>abcdef</pre>` + + `<${tag}>ghi</${tag}>`, + ], + }, + { + initialHTML: + `<pre>abc${caretInLeft}</pre>` + + `<${tag}>${caretInRight}def<div>ghi</div></${tag}>`, + expectedHTML: [ + "<pre>abcdef</pre>" + + `<${tag}><div>ghi</div></${tag}>`, + ], + skip: tag == "p", + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre>${caretInRight}def\nghi</pre>`, + expectedHTML: [ + `<${tag}>abcdef</${tag}>` + + "<pre>ghi</pre>", + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<span style="white-space:pre">def</span></${tag}>` + + "<pre>ghi</pre>", + ], + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre>${caretInRight}def<br>ghi</pre>`, + expectedHTML: [ + `<${tag}>abcdef</${tag}>` + + "<pre>ghi</pre>", + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<span style="white-space:pre">def</span></${tag}>` + + "<pre>ghi</pre>", + ], + }, + { + initialHTML: + `<pre>abc${caretInLeft}</pre>` + + `<${tag}><b>${caretInRight}def</b></${tag}>`, + expectedHTML: [ + "<pre>abc<b>def</b></pre>", + ], + }, + { + initialHTML: + `<pre>abc${caretInLeft}</pre>` + + `<${tag}><b>${caretInRight}def<br>ghi</b></${tag}>`, + expectedHTML: [ + "<pre>abc<b>def</b></pre>" + + `<${tag}><b>ghi</b></${tag}>`, + ], + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre><b>${caretInRight}def\nghi</b></pre>`, + expectedHTML: [ + `<${tag}>abc<b>def</b></${tag}>` + + "<pre><b>ghi</b></pre>", + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<b style="white-space:pre">def</b></${tag}>` + + "<pre><b>ghi</b></pre>", + `<${tag}>abc<span style="white-space:pre"><b>def</b></span></${tag}>` + + "<pre><b>ghi</b></pre>", + ], + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre><b>${caretInRight}def<br>ghi</b></pre>`, + expectedHTML: [ + `<${tag}>abc<b>def</b></${tag}>` + + "<pre><b>ghi</b></pre>", + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<b style="white-space:pre">def</b></${tag}>` + + "<pre><b>ghi</b></pre>", + `<${tag}>abc<span style="white-space:pre"><b>def</b></span></${tag}>` + + "<pre><b>ghi</b></pre>", + ], + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre><b>${caretInRight}def</b>\nghi</pre>`, + expectedHTML: [ + `<${tag}>abc<b>def</b></${tag}>` + + "<pre>ghi</pre>", + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<b style="white-space:pre">def</b></${tag}>` + + "<pre>ghi</pre>", + `<${tag}>abc<span style="white-space:pre"><b>def</b></span></${tag}>` + + "<pre>ghi</pre>", + ], + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre><b>${caretInRight}def</b><br>ghi</pre>`, + expectedHTML: [ + `<${tag}>abc<b>def</b></${tag}>` + + "<pre>ghi</pre>", + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<b style="white-space:pre">def</b></${tag}>` + + "<pre>ghi</pre>", + `<${tag}>abc<span style="white-space:pre"><b>def</b></span></${tag}>` + + "<pre>ghi</pre>", + ], + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre><b>${caretInRight}def\n</b>ghi</pre>`, + expectedHTML: [ + `<${tag}>abc<b>def</b></${tag}>` + + "<pre>ghi</pre>", + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<b style="white-space:pre">def</b></${tag}>` + + `<pre>ghi</pre>`, + `<${tag}>abc<span style="white-space:pre"><b>def</b></span></${tag}>` + + "<pre>ghi</pre>", + ], + }, + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre><b>${caretInRight}def<br></b>ghi</pre>`, + expectedHTML: [ + `<${tag}>abc<b>def</b></${tag}>` + + "<pre>ghi</pre>", + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<b style="white-space:pre">def</b></${tag}>` + + "<pre>ghi</pre>", + `<${tag}>abc<span style="white-space:pre"><b>def</b></span></${tag}>` + + "<pre>ghi</pre>", + ], + }, + // One linefeed at start of <pre> should be ignored. + // Note that if setupEditingHost() does not touch the text node in <pre>, + // the leading line break is ignored, but if it touches the text node, + // the value is set to as-is. Therefore, the following tests can work + // with empty caretInRight value. + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre>\ndef\nghi</pre>`, + expectedHTML: [ + `<${tag}>abcdef</${tag}>` + + `<pre>ghi</pre>`, + ], + expectedHTMLWithStyledPre: [ + `<${tag}>abc<span style="white-space:pre">def</span></${tag}>` + + "<pre>ghi</pre>", + ], + skip: caretInRight !== "", + }, + // When there are two line breaks at start of <pre>, the first one should be + // ignored by the parser but the second one should make empty first line. + // Therefore, the first empty line should be removed. + { + initialHTML: + `<${tag}>abc${caretInLeft}</${tag}>` + + `<pre>\n\ndef\nghi</pre>`, + expectedHTML: [ + `<${tag}>abc</${tag}>` + + "<pre>def\nghi</pre>", + ], + skip: caretInRight !== "", + }, +]; +const utils = new EditorTestUtils(editingHost); + +const betweenBlockAndPre = new RegExp(`</${tag}><pre>`); +const betweenPreAndBlock = new RegExp(`</pre><${tag}>`); +function putStyleElement() { + const styleElement = document.createElement("style"); + styleElement.textContent = "pre { white-space: pre; }"; + document.head.appendChild(styleElement); +} + +for (const specifyPreStyle of [false, true]) { + for (const t of tests) { + if (t.skip) { + continue; + } + if (specifyPreStyle && !t.expectedHTMLWithStyledPre) { + continue; + } + promise_test(async () => { + if (specifyPreStyle) { + putStyleElement(); + } + try { + utils.setupEditingHost(t.initialHTML); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + assert_in_array( + editingHost.innerHTML, + specifyPreStyle ? t.expectedHTMLWithStyledPre : t.expectedHTML, + `white-space should${ + !specifyPreStyle ? " not" : "" + } be preserved by <span> elements` + ); + } finally { + if (specifyPreStyle) { + document.querySelector("style")?.remove(); + } + } + }, `${commandName} at ${t.initialHTML.replace(/\n/g, "\\n")}${ + specifyPreStyle ? " (with <style>pre { white-space: pre; }</style>)" : "" + }`); + + // Repeat same tests with inserting a line break between the paragraphs. + const initialHTMLWithLineBreak = + t.initialHTML + .replace(betweenBlockAndPre, `</${tag}>\n<pre>`) + .replace(betweenPreAndBlock, `</pre>\n<${tag}>`); + promise_test(async () => { + if (specifyPreStyle) { + putStyleElement(); + } + try { + utils.setupEditingHost(initialHTMLWithLineBreak); + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + utils.normalizeStyleAttributeValues(); + assert_in_array( + editingHost.innerHTML, + specifyPreStyle ? t.expectedHTMLWithStyledPre : t.expectedHTML, + `white-space should${ + !specifyPreStyle ? " not" : "" + } be preserved by <span> elements (testing with a line break between paragraphs)` + ); + } finally { + if (specifyPreStyle) { + document.querySelector("style")?.remove(); + } + } + }, `${commandName} at ${initialHTMLWithLineBreak.replace(/\n/g, "\\n")}${ + specifyPreStyle ? " (with <style>pre { white-space: pre; }</style>)" : "" + }`); + } +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/justify-preserving-selection.tentative.html b/testing/web-platform/tests/editing/other/justify-preserving-selection.tentative.html new file mode 100644 index 0000000000..94a63e8505 --- /dev/null +++ b/testing/web-platform/tests/editing/other/justify-preserving-selection.tentative.html @@ -0,0 +1,148 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?styleWithCSS=false&command=justifyCenter"> +<meta name="variant" content="?styleWithCSS=false&command=justifyFull"> +<meta name="variant" content="?styleWithCSS=false&command=justifyLeft"> +<meta name="variant" content="?styleWithCSS=false&command=justifyRight"> +<meta name="variant" content="?styleWithCSS=true&command=justifyCenter"> +<meta name="variant" content="?styleWithCSS=true&command=justifyFull"> +<meta name="variant" content="?styleWithCSS=true&command=justifyLeft"> +<meta name="variant" content="?styleWithCSS=true&command=justifyRight"> +<title>Test preserving selection after justifying selected content</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const editor = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editor); +const searchParams = new URLSearchParams(document.location.search); +const styleWithCSS = searchParams.get("styleWithCSS"); +const command = searchParams.get("command"); +document.execCommand("styleWithCSS", false, styleWithCSS); + +// Note that it's not scope of this test how browsers to align the selected +// content. + +// html: Initial HTML which will be set editor.innerHTML, it should contain +// selection range with a pair of "[" or "{" and "]" or "}". +// expectedSelectedString: After executing "outdent", compared with +// getSelection().toString().replace(/[ \n\r\t]+/g, "") +const tests = [ + { + html: "<div>a[b]c</div>", + expectedSelectedString: "b", + }, + { + html: "<address>a[b]c</address>", // <address> cannot have align attribute + expectedSelectedString: "b", + }, + { + html: "<div>a[bc</div>" + + "<div>de]f</div>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc</div>" + + "<address>de]f</address>", + expectedSelectedString: "bcde", + }, + { + html: "<address>a[bc</address>" + + "<div>de]f</div>", + expectedSelectedString: "bcde", + }, + { + html: "<address>a[bc</address>" + + "<address>de]f</address>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[b]c</li></ul>", + expectedSelectedString: "b", + }, + { + html: "<ul><li>a[bc</li><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[bc</li><li>de]f</li><li>ghi</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>abc</li><li>d[ef</li><li>gh]i</li></ul>", + expectedSelectedString: "efgh", + }, + { + html: "<ul><li>abc</li><li>d[e]f</li><li>ghi</li></ul>", + expectedSelectedString: "e", + }, + { + html: "<ul><li>a[bc</li></ul>" + + "<div>de]f</div>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>abc</li><li>d[ef</li></ul>" + + "<div>gh]i</div>", + expectedSelectedString: "efgh", + }, + { + html: "<div>a[bc</div>" + + "<ul><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc</div>" + + "<ul><li>de]f</li><li>ghi</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<table><tr><td>a[b]c</td></tr></table>", + expectedSelectedString: "b", + }, + { + html: "<table><tr><td>a[bc</td><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<table><tr><td>a[bc</td></tr><tr><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<div>a[bc</div>" + + "<table><tr><td>de]f</td></tr></table>", + expectedSelectedString: "bcde", + }, + { + html: "<table><tr><td>a[bc</td></tr></table>" + + "<div>de]f</div>", + expectedSelectedString: "bcde", + }, +]; + +for (const t of tests) { + test(() => { + utils.setupEditingHost(t.html); + document.execCommand(command); + assert_equals( + getSelection().toString().replace(/[ \n\r\t]+/g, ""), + t.expectedSelectedString, + `Result: ${editor.innerHTML}` + ); + }, `Preserve selection after ${command} at ${t.html}`); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/keeping-attributes-at-joining-elements.tentative.html b/testing/web-platform/tests/editing/other/keeping-attributes-at-joining-elements.tentative.html new file mode 100644 index 0000000000..99a0dab56a --- /dev/null +++ b/testing/web-platform/tests/editing/other/keeping-attributes-at-joining-elements.tentative.html @@ -0,0 +1,1167 @@ +<!doctype html> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?method=backspace"> +<meta name="variant" content="?method=forwarddelete"> +<title>Not merging attributes at joining elements in contenteditable</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</style> +<div contenteditable></div> +<script> +"use strict"; + +const testingBackspace = + new URLSearchParams(document.location.search).get("method") == "backspace"; +const caretForBackSpace = testingBackspace ? "[]" : ""; +const caretForForwardDelete = testingBackspace ? "" : "[]"; +document.execCommand("defaultParagraphSeparator", false, "div"); +const utils = + new EditorTestUtils(document.querySelector("div[contenteditable]")); + +// DO NOT USE multi-line comment in this file, then, you can comment out +// unnecessary tests when you need to attach the browser with a debugger. + +// At joining 2 elements, the attributes shouldn't be merged, and from point of +// view of JS/DOM, both element kept after the join and element deleted from the +// DOM tree should have attributes as-is. +promise_test(async t => { + utils.setupEditingHost( + `<div id="left">abc${caretForForwardDelete}</div><div id="right">${caretForBackSpace}def</div>` + ); + const leftNode = document.getElementById("left"); + const rightNode = document.getElementById("right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("id"), + "left", + `The left node should keep having id=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("id"), + "right", + `The right node should keep having id=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <div id=\"left\"> and <div id=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<div class="left">abc${caretForForwardDelete}</div><div class="right">${caretForBackSpace}def</div>` + ); + const leftNode = utils.editingHost.querySelector(".left"); + const rightNode = utils.editingHost.querySelector(".right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <div class=\"left\"> and <div class=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<div style="font-size:0.8rem">abc${caretForForwardDelete}</div><div style="font-weight:bold">${caretForBackSpace}def</div>` + ); + const leftNode = utils.editingHost.querySelector("div[style]"); + const rightNode = leftNode.nextSibling; + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + // style attribute values shouldn't be touched in this but case, but it's + // okay if the values are not merged. + test(() => { + assert_true( + leftNode.getAttribute("style").includes("font-size"), + `The left node should keep having style attribute containing font-size (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + leftNode.getAttribute("style").includes("font-weight"), + `The left node should have font-weight in its style attribute (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + }); + test(() => { + assert_true( + rightNode.getAttribute("style").includes("font-weight"), + `The right node should keep having style attribute containing font-size (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + rightNode.getAttribute("style").includes("font-style"), + `The right node should have font-size in its style attribute (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <div style=\"font-size:0.8rem\"> and <div style=\"font-weight:bold\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<div data-foo="left">abc${caretForForwardDelete}</div><div data-bar="right">${caretForBackSpace}def</div>` + ); + const leftNode = utils.editingHost.querySelector("[data-foo=left]"); + const rightNode = utils.editingHost.querySelector("[data-bar=right]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + leftNode.hasAttribute("data-bar"), + `The left node shouldn't have data-bar attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-bar"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + rightNode.hasAttribute("data-foo"), + `The right node shouldn't have data-foo attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <div data-foo=\"left\"> and <div data-bar=\"right\">"); + +// Same tests for list-item elements because they may be handled in a different +// path. +promise_test(async t => { + utils.setupEditingHost( + `<ul><li id="left">abc${caretForForwardDelete}</li><li id="right">${caretForBackSpace}def</li></ul>` + ); + const leftNode = document.getElementById("left"); + const rightNode = document.getElementById("right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("id"), + "left", + `The left node should keep having id=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("id"), + "right", + `The right node should keep having id=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <li id=\"left\"> and <li id=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<ul><li class="left">abc${caretForForwardDelete}</li><li class="right">${caretForBackSpace}def</li></ul>` + ); + const leftNode = utils.editingHost.querySelector(".left"); + const rightNode = utils.editingHost.querySelector(".right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <li class=\"left\"> and <li class=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<ul><li style="font-size:0.8rem">abc${caretForForwardDelete}</li><li style="font-weight:bold">${caretForBackSpace}def</li></ul>` + ); + const leftNode = utils.editingHost.querySelector("li[style]"); + const rightNode = leftNode.nextSibling; + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + // style attribute values shouldn't be touched in this but case, but it's + // okay if the values are not merged. + test(() => { + assert_true( + leftNode.getAttribute("style").includes("font-size"), + `The left node should keep having style attribute containing font-size (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + leftNode.getAttribute("style").includes("font-weight"), + `The left node should have font-weight in its style attribute (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + }); + test(() => { + assert_true( + rightNode.getAttribute("style").includes("font-weight"), + `The right node should keep having style attribute containing font-size (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + rightNode.getAttribute("style").includes("font-style"), + `The right node should have font-size in its style attribute (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <li style=\"font-size:0.8rem\"> and <li style=\"font-weight:bold\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<ul><li data-foo="left">abc${caretForForwardDelete}</li><li data-bar="right">${caretForBackSpace}def</li></ul>` + ); + const leftNode = utils.editingHost.querySelector("[data-foo=left]"); + const rightNode = utils.editingHost.querySelector("[data-bar=right]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + leftNode.hasAttribute("data-bar"), + `The left node shouldn't have data-bar attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-bar"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + rightNode.hasAttribute("data-foo"), + `The right node shouldn't have data-foo attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <li data-foo=\"left\"> and <li data-bar=\"right\">"); + +// Same tests for <dt> elements because they may be handled in a different +// path. +promise_test(async t => { + utils.setupEditingHost( + `<dl><dt id="left">abc${caretForForwardDelete}</dt><dt id="right">${caretForBackSpace}def</dt></dl>` + ); + const leftNode = document.getElementById("left"); + const rightNode = document.getElementById("right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("id"), + "left", + `The left node should keep having id=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("id"), + "right", + `The right node should keep having id=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dt id=\"left\"> and <dt id=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dt class="left">abc${caretForForwardDelete}</dt><dt class="right">${caretForBackSpace}def</dt></dl>` + ); + const leftNode = utils.editingHost.querySelector(".left"); + const rightNode = utils.editingHost.querySelector(".right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dt class=\"left\"> and <dt class=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dt style="font-size:0.8rem">abc${caretForForwardDelete}</dt><dt style="font-weight:bold">${caretForBackSpace}def</dt></dl>` + ); + const leftNode = utils.editingHost.querySelector("dt[style]"); + const rightNode = leftNode.nextSibling; + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + // style attribute values shouldn't be touched in this but case, but it's + // okay if the values are not merged. + test(() => { + assert_true( + leftNode.getAttribute("style").includes("font-size"), + `The left node should keep having style attribute containing font-size (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + leftNode.getAttribute("style").includes("font-weight"), + `The left node should have font-weight in its style attribute (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + }); + test(() => { + assert_true( + rightNode.getAttribute("style").includes("font-weight"), + `The right node should keep having style attribute containing font-size (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + rightNode.getAttribute("style").includes("font-style"), + `The right node should have font-size in its style attribute (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dt style=\"font-size:0.8rem\"> and <dt style=\"font-weight:bold\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dt data-foo="left">abc${caretForForwardDelete}</dt><dt data-bar="right">${caretForBackSpace}def</dt></dl>` + ); + const leftNode = utils.editingHost.querySelector("[data-foo=left]"); + const rightNode = utils.editingHost.querySelector("[data-bar=right]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + leftNode.hasAttribute("data-bar"), + `The left node shouldn't have data-bar attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-bar"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + rightNode.hasAttribute("data-foo"), + `The right node shouldn't have data-foo attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dt data-foo=\"left\"> and <dt data-bar=\"right\">"); + +// Same tests for <dd> elements because they may be handled in a different +// path. +promise_test(async t => { + utils.setupEditingHost( + `<dl><dd id="left">abc${caretForForwardDelete}</dd><dd id="right">${caretForBackSpace}def</dd></dl>` + ); + const leftNode = document.getElementById("left"); + const rightNode = document.getElementById("right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("id"), + "left", + `The left node should keep having id=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("id"), + "right", + `The right node should keep having id=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dd id=\"left\"> and <dd id=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dd class="left">abc${caretForForwardDelete}</dd><dd class="right">${caretForBackSpace}def</dd></dl>` + ); + const leftNode = utils.editingHost.querySelector(".left"); + const rightNode = utils.editingHost.querySelector(".right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dd class=\"left\"> and <dd class=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dd style="font-size:0.8rem">abc${caretForForwardDelete}</dd><dd style="font-weight:bold">${caretForBackSpace}def</dd></dl>` + ); + const leftNode = utils.editingHost.querySelector("dd[style]"); + const rightNode = leftNode.nextSibling; + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + // style attribute values shouldn't be touched in this but case, but it's + // okay if the values are not merged. + test(() => { + assert_true( + leftNode.getAttribute("style").includes("font-size"), + `The left node should keep having style attribute containing font-size (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + leftNode.getAttribute("style").includes("font-weight"), + `The left node should have font-weight in its style attribute (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + }); + test(() => { + assert_true( + rightNode.getAttribute("style").includes("font-weight"), + `The right node should keep having style attribute containing font-size (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + rightNode.getAttribute("style").includes("font-style"), + `The right node should have font-size in its style attribute (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dd style=\"font-size:0.8rem\"> and <dd style=\"font-weight:bold\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dd data-foo="left">abc${caretForForwardDelete}</dd><dd data-bar="right">${caretForBackSpace}def</dd></dl>` + ); + const leftNode = utils.editingHost.querySelector("[data-foo=left]"); + const rightNode = utils.editingHost.querySelector("[data-bar=right]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + leftNode.hasAttribute("data-bar"), + `The left node shouldn't have data-bar attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-bar"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + rightNode.hasAttribute("data-foo"), + `The right node shouldn't have data-foo attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dd data-foo=\"left\"> and <dd data-bar=\"right\">"); + +// Same tests for <dt> and <dd> because they may be handled in a different +// path. +promise_test(async t => { + utils.setupEditingHost( + `<dl><dt id="left">abc${caretForForwardDelete}</dt><dd id="right">${caretForBackSpace}def</dd></dl>` + ); + const leftNode = document.getElementById("left"); + const rightNode = document.getElementById("right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("id"), + "left", + `The left node should keep having id=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("id"), + "right", + `The right node should keep having id=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dt id=\"left\"> and <dd id=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dt class="left">abc${caretForForwardDelete}</dt><dd class="right">${caretForBackSpace}def</dd></dl>` + ); + const leftNode = utils.editingHost.querySelector(".left"); + const rightNode = utils.editingHost.querySelector(".right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dt class=\"left\"> and <dd class=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dt style="font-size:0.8rem">abc${caretForForwardDelete}</dt><dd style="font-weight:bold">${caretForBackSpace}def</dd></dl>` + ); + const leftNode = utils.editingHost.querySelector("dt[style]"); + const rightNode = utils.editingHost.querySelector("dd[style]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + // style attribute values shouldn't be touched in this but case, but it's + // okay if the values are not merged. + test(() => { + assert_true( + leftNode.getAttribute("style").includes("font-size"), + `The left node should keep having style attribute containing font-size (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + leftNode.getAttribute("style").includes("font-weight"), + `The left node should have font-weight in its style attribute (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + }); + test(() => { + assert_true( + rightNode.getAttribute("style").includes("font-weight"), + `The right node should keep having style attribute containing font-size (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + rightNode.getAttribute("style").includes("font-style"), + `The right node should have font-size in its style attribute (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dt style=\"font-size:0.8rem\"> and <dd style=\"font-weight:bold\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dt data-foo="left">abc${caretForForwardDelete}</dt><dd data-bar="right">${caretForBackSpace}def</dd></dl>` + ); + const leftNode = utils.editingHost.querySelector("[data-foo=left]"); + const rightNode = utils.editingHost.querySelector("[data-bar=right]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + leftNode.hasAttribute("data-bar"), + `The left node shouldn't have data-bar attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-bar"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + rightNode.hasAttribute("data-foo"), + `The right node shouldn't have data-foo attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dt data-foo=\"left\"> and <dd data-bar=\"right\">"); + +// Same tests for <dd> and <dt> because they may be handled in a different +// path. +promise_test(async t => { + utils.setupEditingHost( + `<dl><dd id="left">abc${caretForForwardDelete}</dd><dt id="right">${caretForBackSpace}def</dt></dl>` + ); + const leftNode = document.getElementById("left"); + const rightNode = document.getElementById("right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("id"), + "left", + `The left node should keep having id=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("id"), + "right", + `The right node should keep having id=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dd id=\"left\"> and <dt id=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dd class="left">abc${caretForForwardDelete}</dd><dt class="right">${caretForBackSpace}def</dt></dl>` + ); + const leftNode = utils.editingHost.querySelector(".left"); + const rightNode = utils.editingHost.querySelector(".right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dd class=\"left\"> and <dt class=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dd style="font-size:0.8rem">abc${caretForForwardDelete}</dd><dt style="font-weight:bold">${caretForBackSpace}def</dt></dl>` + ); + const leftNode = utils.editingHost.querySelector("dd[style]"); + const rightNode = utils.editingHost.querySelector("dt[style]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + // style attribute values shouldn't be touched in this but case, but it's + // okay if the values are not merged. + test(() => { + assert_true( + leftNode.getAttribute("style").includes("font-size"), + `The left node should keep having style attribute containing font-size (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + leftNode.getAttribute("style").includes("font-weight"), + `The left node should have font-weight in its style attribute (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + }); + test(() => { + assert_true( + rightNode.getAttribute("style").includes("font-weight"), + `The right node should keep having style attribute containing font-size (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + rightNode.getAttribute("style").includes("font-style"), + `The right node should have font-size in its style attribute (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dd style=\"font-size:0.8rem\"> and <dt style=\"font-weight:bold\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<dl><dd data-foo="left">abc${caretForForwardDelete}</dd><dt data-bar="right">${caretForBackSpace}def</dt></dl>` + ); + const leftNode = utils.editingHost.querySelector("[data-foo=left]"); + const rightNode = utils.editingHost.querySelector("[data-bar=right]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + leftNode.hasAttribute("data-bar"), + `The left node shouldn't have data-bar attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-bar"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + rightNode.hasAttribute("data-foo"), + `The right node shouldn't have data-foo attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <dd data-foo=\"left\"> and <dt data-bar=\"right\">"); + +// Same tests for <h3> and <div> because they may be handled in a different +// path. +promise_test(async t => { + utils.setupEditingHost( + `<h3 id="left">abc${caretForForwardDelete}</h3><div id="right">${caretForBackSpace}def</div>` + ); + const leftNode = document.getElementById("left"); + const rightNode = document.getElementById("right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("id"), + "left", + `The left node should keep having id=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("id"), + "right", + `The right node should keep having id=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <h3 id=\"left\"> and <div id=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<h3 class="left">abc${caretForForwardDelete}</h3><div class="right">${caretForBackSpace}def</div>` + ); + const leftNode = utils.editingHost.querySelector(".left"); + const rightNode = utils.editingHost.querySelector(".right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <h3 class=\"left\"> and <div class=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<h3 style="font-size:0.8rem">abc${caretForForwardDelete}</h3><div style="font-weight:bold">${caretForBackSpace}def</div>` + ); + const leftNode = utils.editingHost.querySelector("h3[style]"); + const rightNode = utils.editingHost.querySelector("div[style]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + // style attribute values shouldn't be touched in this but case, but it's + // okay if the values are not merged. + test(() => { + assert_true( + leftNode.getAttribute("style").includes("font-size"), + `The left node should keep having style attribute containing font-size (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + leftNode.getAttribute("style").includes("font-weight"), + `The left node should have font-weight in its style attribute (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + }); + test(() => { + assert_true( + rightNode.getAttribute("style").includes("font-weight"), + `The right node should keep having style attribute containing font-size (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + rightNode.getAttribute("style").includes("font-style"), + `The right node should have font-size in its style attribute (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <h3 style=\"font-size:0.8rem\"> and <div style=\"font-weight:bold\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<h3 data-foo="left">abc${caretForForwardDelete}</h3><div data-bar="right">${caretForBackSpace}def</div>` + ); + const leftNode = utils.editingHost.querySelector("[data-foo=left]"); + const rightNode = utils.editingHost.querySelector("[data-bar=right]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + leftNode.hasAttribute("data-bar"), + `The left node shouldn't have data-bar attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-bar"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + rightNode.hasAttribute("data-foo"), + `The right node shouldn't have data-foo attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <h3 data-foo=\"left\"> and <div data-bar=\"right\">"); + +// Same tests for <div> and <h3> because they may be handled in a different +// path. +promise_test(async t => { + utils.setupEditingHost( + `<div id="left">abc${caretForForwardDelete}</div><h3 id="right">${caretForBackSpace}def</h3>` + ); + const leftNode = document.getElementById("left"); + const rightNode = document.getElementById("right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("id"), + "left", + `The left node should keep having id=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("id"), + "right", + `The right node should keep having id=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <div id=\"left\"> and <h3 id=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<div class="left">abc${caretForForwardDelete}</div><h3 class="right">${caretForBackSpace}def</h3>` + ); + const leftNode = utils.editingHost.querySelector(".left"); + const rightNode = utils.editingHost.querySelector(".right"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("class"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("class"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <div class=\"left\"> and <h3 class=\"right\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<div style="font-size:0.8rem">abc${caretForForwardDelete}</div><h3 style="font-weight:bold">${caretForBackSpace}def</h3>` + ); + const leftNode = utils.editingHost.querySelector("div[style]"); + const rightNode = utils.editingHost.querySelector("h3[style]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + // style attribute values shouldn't be touched in this but case, but it's + // okay if the values are not merged. + test(() => { + assert_true( + leftNode.getAttribute("style").includes("font-size"), + `The left node should keep having style attribute containing font-size (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + leftNode.getAttribute("style").includes("font-weight"), + `The left node should have font-weight in its style attribute (style="${leftNode.getAttribute("style")}", ${t.name})` + ); + }); + test(() => { + assert_true( + rightNode.getAttribute("style").includes("font-weight"), + `The right node should keep having style attribute containing font-size (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + assert_false( + rightNode.getAttribute("style").includes("font-style"), + `The right node should have font-size in its style attribute (style="${rightNode.getAttribute("style")}", ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <div style=\"font-size:0.8rem\"> and <h3 style=\"font-weight:bold\">"); + +promise_test(async t => { + utils.setupEditingHost( + `<div data-foo="left">abc${caretForForwardDelete}</div><h3 data-bar="right">${caretForBackSpace}def</h3>` + ); + const leftNode = utils.editingHost.querySelector("[data-foo=left]"); + const rightNode = utils.editingHost.querySelector("[data-bar=right]"); + + await (testingBackspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + + test(() => { + assert_equals( + leftNode.getAttribute("data-foo"), + "left", + `The left node should keep having class=left (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + leftNode.hasAttribute("data-bar"), + `The left node shouldn't have data-bar attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_equals( + rightNode.getAttribute("data-bar"), + "right", + `The right node should keep having class=right (isConnected=${rightNode.isConnected}, ${t.name})` + ); + }); + test(() => { + assert_false( + rightNode.hasAttribute("data-foo"), + `The right node shouldn't have data-foo attribute (isConnected=${leftNode.isConnected}, ${t.name})` + ); + }); + assert_equals( + leftNode.isConnected ^ rightNode.isConnected, + 1, + `One should stay in the document and the other should be disconnected (${utils.editingHost.innerHTML}, ${t.name})` + ); +}, "Joining <div data-foo=\"left\"> and <h3 data-bar=\"right\">"); + + +</script> diff --git a/testing/web-platform/tests/editing/other/legacy-edit-command.html b/testing/web-platform/tests/editing/other/legacy-edit-command.html new file mode 100644 index 0000000000..81ff89d81b --- /dev/null +++ b/testing/web-platform/tests/editing/other/legacy-edit-command.html @@ -0,0 +1,117 @@ +<!doctype html> +<html> +<meta charset=utf-8> +<meta name="variant" content="?command=increaseFontSize"> +<meta name="variant" content="?command=decreaseFontSize"> +<meta name="variant" content="?command=getHTML"> +<meta name="variant" content="?command=insertBrOrReturn¶m=true"> +<meta name="variant" content="?command=insertBrOrReturn¶m=false"> +<meta name="variant" content="?command=heading¶m=h1"> +<meta name="variant" content="?command=heading¶m=h2"> +<meta name="variant" content="?command=heading¶m=h3"> +<meta name="variant" content="?command=heading¶m=h4"> +<meta name="variant" content="?command=heading¶m=h5"> +<meta name="variant" content="?command=heading¶m=h6"> +<meta name="variant" content="?command=contentReadOnly¶m=true"> +<meta name="variant" content="?command=contentReadOnly¶m=false"> +<meta name="variant" content="?command=readonly¶m=true"> +<meta name="variant" content="?command=readonly¶m=false"> +<title>Test legacy commands won't work</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +</head> +<body contenteditable></body> +<script> +"use strict"; + +const testParams = new URLSearchParams(location.search.substring(1)); +const command = testParams.get("command"); +const param = testParams.has("param") ? testParams.get("param") : undefined; + +const editor = document.body; + +promise_test(async () => { + await new Promise(resolve => addEventListener("load", resolve, {once: true})); +}, "Wait for load..."); + +promise_test(async () => { + function reset() { + editor.innerHTML = "<p>abc</p>"; + editor.focus(); + getSelection().setBaseAndExtent( + editor.querySelector("p").firstChild, + 1, + editor.querySelector("p").firstChild, + 2 + ); + } + test(() => { + reset(); + let inputEvents = []; + function onInput(event) { + inputEvents.push(event); + } + editor.addEventListener("input", onInput); + try { + assert_equals( + document.execCommand(command, false, param), + false, + "result should be false" + ); + assert_equals( + editor.outerHTML, + "<body contenteditable=\"\"><p>abc</p></body>", + "editable content shouldn't be modified" + ); + assert_equals( + inputEvents.length, + 0, + "no input events should be fired" + ); + } finally { + editor.removeEventListener("input", onInput); + } + }, `execCommand("${command}", false, ${param === undefined ? "undefined" : `"${param}"`})`); + test(() => { + reset(); + assert_equals( + document.queryCommandState(command), + false, + "result should be false" + ); + }, `queryCommandState("${command}")`); + test(() => { + reset(); + assert_equals( + document.queryCommandEnabled(command), + false, + "result should be false" + ); + }, `queryCommandEnabled("${command}")`); + test(() => { + reset(); + assert_equals( + document.queryCommandIndeterm(command), + false, + "result should be false" + ); + }, `queryCommandIndeterm("${command}")`); + test(() => { + reset(); + assert_equals( + document.queryCommandValue(command), + "", + "result should be empty string" + ); + }, `queryCommandValue("${command}")`); + test(() => { + reset(); + assert_equals( + document.queryCommandSupported(command), + false, + "result should be false" + ); + }, `queryCommandSupported("${command}")`); +}, `command: "${command}", param: "${param}"`); +</script> +</html> diff --git a/testing/web-platform/tests/editing/other/link-boundaries-insertion.html b/testing/web-platform/tests/editing/other/link-boundaries-insertion.html new file mode 100644 index 0000000000..567cc33ad9 --- /dev/null +++ b/testing/web-platform/tests/editing/other/link-boundaries-insertion.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Placing selection and typing inside empty elements</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../include/editor-test-utils.js"></script> + +<div contenteditable></div> + +<script> + const utils = new EditorTestUtils( document.querySelector( 'div[contenteditable]' ) ); + + test( () => { + utils.setupEditingHost( `<p><a href="https://example.com" id="test-end">Link</a></p>` ); + + const target = document.querySelector( '#test-end' ); + const range = document.createRange(); + const selection = getSelection(); + + range.selectNodeContents( target ); + selection.removeAllRanges(); + selection.addRange( range ); + selection.collapseToEnd(); + + document.execCommand( 'insertText', false, 'a' ); + assert_equals( target.innerHTML, 'Linka', 'The text should be inserted into the link' ); + }, 'Insert text into the selection at the end of a link' ); + + test( () => { + utils.setupEditingHost( `<p><a href="https://example.com" id="test-beginning">Link</a></p>` ); + + const target = document.querySelector( '#test-beginning' ); + const range = document.createRange(); + const selection = getSelection(); + + range.selectNodeContents( target ); + selection.removeAllRanges(); + selection.addRange( range ); + selection.collapseToStart(); + + document.execCommand( 'insertText', false, 'a' ); + assert_equals( target.innerHTML, 'aLink', 'The text should be inserted into the link' ); + }, 'Insert text into the selection at the beginning of a link' ); +</script> diff --git a/testing/web-platform/tests/editing/other/move-inserted-node-from-DOMNodeInserted-during-exec-command-insertHTML.html b/testing/web-platform/tests/editing/other/move-inserted-node-from-DOMNodeInserted-during-exec-command-insertHTML.html new file mode 100644 index 0000000000..41e012a62e --- /dev/null +++ b/testing/web-platform/tests/editing/other/move-inserted-node-from-DOMNodeInserted-during-exec-command-insertHTML.html @@ -0,0 +1,27 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div contenteditable> +<p id="p1"><br></p> +<p id="p2"></p> +</div> +<script> +"use strict"; +let editor = document.querySelector("[contenteditable]"); +let p1 = document.getElementById("p1"); +let p2 = document.getElementById("p2"); +p1.addEventListener("DOMNodeInserted", event => { + if (event.target.localName === "i") { + p2.appendChild(event.target); + } +}); +document.getSelection().collapse(p1, 0); +document.execCommand("insertHTML", false, + "<b>bold1</b><i>italic1</i><b>bold2</b><i>italic2</i>"); +test(function () { + assert_in_array(p1.innerHTML, ["<b>bold1</b><b>bold2</b><br>", "<b>bold1</b><b>bold2</b>"]); +}, "First <p> element should have only <b> elements"); +test(function () { + assert_equals(p2.innerHTML, "<i>italic1</i><i>italic2</i>"); +}, "Second <p> element should have only <i> elements"); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/editing/other/non-html-document.html b/testing/web-platform/tests/editing/other/non-html-document.html new file mode 100644 index 0000000000..ffd2e6f594 --- /dev/null +++ b/testing/web-platform/tests/editing/other/non-html-document.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<title>Non-HTML document tests</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> + +test(function() { + let xmldoc = + document.implementation.createDocument("http://www.w3.org/1999/xlink", + "html", null); + for (let f of [ + () => xmldoc.execCommand("bold"), + () => xmldoc.queryCommandEnabled("bold"), + () => xmldoc.queryCommandIndeterm("bold"), + () => xmldoc.queryCommandState("bold"), + () => xmldoc.queryCommandSupported("bold"), + () => xmldoc.queryCommandValue("bold"), + ]) { + assert_throws_dom("InvalidStateError", f); + } +}, "editing APIs on an XML document should be disabled"); + +</script> diff --git a/testing/web-platform/tests/editing/other/outdent-preserving-selection.tentative.html b/testing/web-platform/tests/editing/other/outdent-preserving-selection.tentative.html new file mode 100644 index 0000000000..9f299bda49 --- /dev/null +++ b/testing/web-platform/tests/editing/other/outdent-preserving-selection.tentative.html @@ -0,0 +1,192 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?styleWithCSS=false"> +<meta name="variant" content="?styleWithCSS=true"> +<title>Test preserving selection after outdent</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const editor = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editor); +const styleWithCSS = + new URLSearchParams(document.location.search).get("styleWithCSS"); +document.execCommand("styleWithCSS", false, styleWithCSS); + +// Note that it's not scope of this test how browsers to outdent the selected +// content. + +// html: Initial HTML which will be set editor.innerHTML, it should contain +// selection range with a pair of "[" or "{" and "]" or "}". +// expectedSelectedString: After executing "outdent", compared with +// getSelection().toString().replace(/[ \n\r]+/g, "") +const tests = [ + { + html: "<blockquote>a[b]c</blockquote>", + expectedSelectedString: "b", + }, + { + html: "<blockquote><div>a[b]c</div></blockquote>", + expectedSelectedString: "b", + }, + { + html: "<blockquote><div>a[bc</div><div>de]f</div></blockquote>", + expectedSelectedString: "bcde", + }, + { + html: "<blockquote>a[bc</blockquote>" + + "<blockquote>de]f</blockquote>", + expectedSelectedString: "bcde", + }, + { + html: '<div style="margin-left:15px">a[b]c</div>', + expectedSelectedString: "b", + }, + { + html: '<div style="margin-left:15px">a[bc</div>' + + '<div style="margin-left:15px">de]f</div>', + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[b]c</li></ul>", + expectedSelectedString: "b", + }, + { + html: "<ul><li>a[bc</li><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><ul><li>a[b]c</li></ul></ol>", + expectedSelectedString: "b", + }, + { + html: "<ol><ul><li>a[bc</li><li>de]f</li></ul></ol>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><ul><li>a[b]c</li></ul></ul>", + expectedSelectedString: "b", + }, + { + html: "<ul><ul><li>a[bc</li><li>de]f</li></ul></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><li><ul><li>a[b]c</li></ul></li></ol>", + expectedSelectedString: "b", + }, + { + html: "<ol><li><ul><li>a[bc</li><li>de]f</li></ul></li></ol>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li><ul><li>a[b]c</li></ul></li></ul>", + expectedSelectedString: "b", + }, + { + html: "<ul><li><ul><li>a[bc</li><li>de]f</li></ul></li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<blockquote><div>a[bc</div></blockquote>" + + "<ul><ul><li>de]f</li></ul></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<blockquote><div>a[bc</div></blockquote>" + + "<ol><ul><li>de]f</li></ul></ol>", + expectedSelectedString: "bcde", + }, + { + html: '<div style="margin-left:15px">a[bc</div>' + + "<ul><ul><li>de]f</li></ul></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<blockquote><div>a[bc</div></blockquote>" + + "<ul><li><ul><li>de]f</li></ul></li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<blockquote><div>a[bc</div></blockquote>" + + "<ol><li><ul><li>de]f</li></ul></li></ol>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><ul><li>a[bc</li></ul></ol>" + + "<blockquote><div>de]f</div></blockquote>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><ul><li>a[bc</li></ul></ul>" + + '<div style="margin-left:15px">de]f</div>', + expectedSelectedString: "bcde", + }, + { + html: "<ul><li><ul><li>a[bc</li></ul></li></ul>" + + "<blockquote><div>de]f</div></blockquote>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><li><ul><li>a[bc</li></ul></li></ol>" + + "<blockquote><div>de]f</div></blockquote>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>a[bc</li></ul>" + + "<ul><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ul><li>abc</li><li>d[ef</li></ul>" + + "<ul><li>gh]i</li></ul>", + expectedSelectedString: "efgh", + }, + { + html: "<ul><li>abc</li><li>d[ef</li></ul>" + + "<ul><li>gh]i</li><li>jkl</li></ul>", + expectedSelectedString: "efgh", + }, + { + html: "<ol><li>a[bc</li></ol>" + + "<ul><li>de]f</li></ul>", + expectedSelectedString: "bcde", + }, + { + html: "<ol><li>abc</li><li>d[ef</li></ol>" + + "<ul><li>gh]i</li></ul>", + expectedSelectedString: "efgh", + }, + { + html: "<ol><li>abc</li><li>d[ef</li></ol>" + + "<ul><li>gh]i</li><li>jkl</li></ul>", + expectedSelectedString: "efgh", + }, +]; + +for (const t of tests) { + test(() => { + utils.setupEditingHost(t.html); + document.execCommand("outdent"); + assert_equals( + getSelection().toString().replace(/[ \n\r]+/g, ""), + t.expectedSelectedString, + `Result: ${editor.innerHTML}` + ); + }, `Preserve selection after outdent at ${t.html}`); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/recursive-exec-command-calls.tentative.html b/testing/web-platform/tests/editing/other/recursive-exec-command-calls.tentative.html new file mode 100644 index 0000000000..60a3b03099 --- /dev/null +++ b/testing/web-platform/tests/editing/other/recursive-exec-command-calls.tentative.html @@ -0,0 +1,37 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test recursive `Document.execCommand()` calls</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div contenteditable><br></div> +<script> +"use strict"; + +setup({explicit_done: true}); + +/** + * This test checks whether the browser allows or disallows recursive + * Document.execCommand() calls. + * https://github.com/w3c/editing/issues/200#issuecomment-578097441 + */ +function runTests() { + test(function () { + let editor = document.querySelector("div[contenteditable]"); + editor.focus(); + let counter = 0; + editor.addEventListener("input", event => { + if (++counter < 10) { + let result = document.execCommand("insertText", false, `, ${counter}`); + assert_false(result, + '`execCommand("insertText") in the "input" event listener should return `false`'); + } + }); + document.execCommand("insertText", false, "0"); + assert_equals(editor.textContent, "0", + '`execCommand("insertText") in the "input" event listener should do nothing'); + }, "Recursive `Document.execCommand()` shouldn't be supported"); + done(); +} + +window.addEventListener("load", runTests, {once: true}); +</script> diff --git a/testing/web-platform/tests/editing/other/removing-inline-style-specified-by-parent-block.tentative.html b/testing/web-platform/tests/editing/other/removing-inline-style-specified-by-parent-block.tentative.html new file mode 100644 index 0000000000..c799819a38 --- /dev/null +++ b/testing/web-platform/tests/editing/other/removing-inline-style-specified-by-parent-block.tentative.html @@ -0,0 +1,125 @@ +<!doctype html> +<html> +<meta charset=utf-8> +<meta name="variant" content="?b"> +<meta name="variant" content="?i"> +<meta name="variant" content="?u"> +<title>Test removing inline style which is specified by parent block</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> +<div contenteditable></div> +</body> +<script> +"use strict"; + +const tag = location.search.substring(1); +const style = (() => { + switch (tag) { + case "b": + return "font-weight: bold"; + case "i": + return "font-style: italic"; + case "u": + return "text-decoration: underline"; + } +})(); +const cancelingStyle = (() => { + switch (tag) { + case "b": + return "font-weight: normal"; + case "i": + return "font-style: normal"; + case "u": + return ""; // Cannot delete parent underline + } +})(); +const command = (() => { + switch (tag) { + case "b": + return "bold"; + case "i": + return "italic"; + case "u": + return "underline"; + } +})(); +const editor = document.querySelector("div[contenteditable]"); + +promise_test(async () => { + await new Promise(resolve => { + addEventListener("load", () => { + assert_true(true, "The document is loaded"); + resolve(); + }, { once: true }); + }); +}, "Waiting for load..."); + +promise_test(async () => { + document.execCommand("styleWithCSS", false, "false"); + editor.innerHTML = `<p style="${style}">foo</p>`; + editor.focus(); + getSelection().selectAllChildren(editor.querySelector("p")); + document.execCommand(command); + assert_in_array( + editor.innerHTML, + [ + "<p>foo</p>", + "<p>foo<br></p>", + "<p style=\"\">foo</p>", + "<p style=\"\">foo<br></p>", + ]); +}, "Disabling style to text, it's applied to the parent block"); + +promise_test(async () => { + document.execCommand("styleWithCSS", false, "false"); + editor.innerHTML = `<p>foo</p>`; + editor.setAttribute("style", style); + editor.focus(); + getSelection().selectAllChildren(editor.querySelector("p")); + document.execCommand(command); + if (cancelingStyle !== "") { + assert_in_array( + editor.innerHTML, + [ + `<p><span style="${cancelingStyle};">foo</span></p>`, + `<p><span style="${cancelingStyle};">foo</span><br></p>`, + ]); + } else { + assert_in_array( + editor.innerHTML, + [ + "<p>foo</p>", + "<p>foo<br></p>", + ]); + } + assert_equals(editor.getAttribute("style"), style); + editor.removeAttribute("style"); +}, "Disabling style to text, it's applied to the editing host"); + +promise_test(async () => { + document.execCommand("styleWithCSS", false, "false"); + editor.innerHTML = `<p>foo</p>`; + document.body.setAttribute("style", style); + editor.focus(); + getSelection().selectAllChildren(editor.querySelector("p")); + document.execCommand(command); + if (cancelingStyle !== "") { + assert_in_array( + editor.innerHTML, + [ + `<p><span style="${cancelingStyle};">foo</span></p>`, + `<p><span style="${cancelingStyle};">foo</span><br></p>`, + ]); + } else { + assert_in_array( + editor.innerHTML, + [ + "<p>foo</p>", + "<p>foo<br></p>", + ]); + } + assert_equals(document.body.getAttribute("style"), style); + document.body.removeAttribute("style"); +}, "Disabling style to text, it's applied to the body"); +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/editing/other/restoration.html b/testing/web-platform/tests/editing/other/restoration.html new file mode 100644 index 0000000000..4c53008b41 --- /dev/null +++ b/testing/web-platform/tests/editing/other/restoration.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Restoration of style tests</title> +<!-- +No spec, based on: https://bugzilla.mozilla.org/show_bug.cgi?id=1250805 +If the user presses Ctrl+B and then hits Enter and then types text, the text +should still be bold. Hitting Enter shouldn't make it forget. And so too for +other commands. +--> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div contenteditable></div> +<script> +var div = document.querySelector("div"); + +function doTestInner(cmd, param, startBold) { + div.innerHTML = startBold ? "<b>foo</b>bar" : "foobar"; + getSelection().collapse(startBold ? div.firstChild.firstChild + : div.firstChild, 3); + + // Set/unset bold, then run command and see if it's still there + assert_true(document.execCommand("bold", false, ""), + "execCommand needs to return true for bold"); + + assert_true(document.execCommand(cmd, false, param), + "execCommand needs to return true for " + cmd + " " + param); + + assert_equals(document.queryCommandState("bold"), !startBold, + "bold state"); + + assert_true(document.execCommand("inserttext", false, "x"), + "execCommand needs to return true for inserttext x"); + + // Find the new text node and check that it's actually bold (or not) + var node = div; + while (node) { + if (node.nodeType == Node.TEXT_NODE && node.nodeValue.indexOf("x") != -1) { + assert_in_array(getComputedStyle(node.parentNode).fontWeight, + !startBold ? ["700", "bold"] : ["400", "normal"], + "font-weight"); + return; + } + if (node.firstChild) { + node = node.firstChild; + continue; + } + while (node != div && !node.nextSibling) { + node = node.parentNode; + } + if (node == div) { + assert_unreached("x not found!"); + break; + } + node = node.nextSibling; + } +} + +function doTest(cmd, param) { + if (param === undefined) { + param = ""; + } + + test(function() { + doTestInner(cmd, param, true); + }, cmd + " " + param + " starting bold"); + + test(function() { + doTestInner(cmd, param, false); + }, cmd + " " + param + " starting not bold"); +} + +doTest("insertparagraph"); +doTest("insertlinebreak"); +doTest("delete"); +doTest("forwarddelete"); +doTest("insertorderedlist"); +doTest("insertunorderedlist"); +doTest("indent"); +// Outdent does nothing here, but should be harmless. +doTest("outdent"); +doTest("justifyleft"); +doTest("justifyright"); +doTest("justifycenter"); +doTest("justifyfull"); +doTest("formatblock", "div"); +doTest("formatblock", "blockquote"); +doTest("inserthorizontalrule"); +doTest("insertimage", "a"); +doTest("inserttext", "bar"); +</script> diff --git a/testing/web-platform/tests/editing/other/select-all-and-delete-in-html-element-having-contenteditable.html b/testing/web-platform/tests/editing/other/select-all-and-delete-in-html-element-having-contenteditable.html new file mode 100644 index 0000000000..ab324c6d03 --- /dev/null +++ b/testing/web-platform/tests/editing/other/select-all-and-delete-in-html-element-having-contenteditable.html @@ -0,0 +1,151 @@ +<!doctype html> +<html contenteditable> +<head> +<meta charset=utf-8> +<title>Test "Select all" and deletion work with <html contenteditable></title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +</head> +<body> +<script> +"use strict"; + +const kBackspaceKey = "\uE003"; +const kDeleteKey = "\uE017"; +const kMeta = "\uE03d"; +const kControl = "\uE009"; + +async function selectAllWithKey(elementToSelectAll) { + if (elementToSelectAll.length === 0) { + throw "element to select all must not be empty"; + } + getSelection().collapse(elementToSelectAll, 0); + try { + await new test_driver.Actions() + .keyDown(kControl) + .keyDown("a") + .keyUp("a") + .keyUp(kControl) + .send(); + if (!getSelection().isCollapsed) { + return; + } + await new test_driver.Actions() + .keyDown(kMeta) + .keyDown("a") + .keyUp("a") + .keyUp(kMeta) + .send(); + if (!getSelection().isCollapsed) { + return; + } + } catch (ex) { + throw ex; + } + throw "Neither Control-A nor Meta-A does select all contents"; +} + +function deleteWithBackspaceKey() { + return new test_driver.Actions() + .keyDown(kBackspaceKey) + .keyUp(kBackspaceKey) + .send(); +} + +function deleteWithDeleteKey() { + return new test_driver.Actions() + .keyDown(kDeleteKey) + .keyUp(kDeleteKey) + .send(); +} + +promise_test(async () => { + document.body.innerHTML = "abc"; + await selectAllWithKey(document.body); + await deleteWithBackspaceKey(); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, "Select All, then, Backspace"); + +promise_test(async () => { + document.body.innerHTML = "abc"; + await selectAllWithKey(document.body); + await deleteWithDeleteKey(); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, "Select All, then, Delete"); + +promise_test(async () => { + document.body.innerHTML = "abc"; + document.execCommand("selectall"); + await deleteWithBackspaceKey(); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'execCommand("selectall"), then, Backspace'); + +promise_test(async () => { + document.body.innerHTML = "abc"; + document.execCommand("selectall"); + await deleteWithDeleteKey(); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'execCommand("selectall"), then, Delete'); + +promise_test(async () => { + document.body.innerHTML = "abc"; + await selectAllWithKey(document.body); + document.execCommand("forwarddelete", false, false); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'Select All, then, execCommand("forwarddelete")'); + +promise_test(async () => { + document.body.innerHTML = "abc"; + await selectAllWithKey(document.body); + document.execCommand("delete", false, false); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'Select All, then, execCommand("delete")'); + +test(() => { + document.body.innerHTML = "abc"; + document.execCommand("selectall"); + document.execCommand("forwarddelete", false, false); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'execCommand("selectall"), then, execCommand("forwarddelete")'); + +test(() => { + document.body.innerHTML = "abc"; + document.execCommand("selectall"); + document.execCommand("delete", false, false); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'execCommand("selectall"), then, execCommand("delete")'); + +promise_test(async () => { + document.body.innerHTML = "abc"; + getSelection().selectAllChildren(document.documentElement); + await deleteWithBackspaceKey(); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'getSelection().selectAllChildren(document.documentElement), then, Backspace'); + +promise_test(async () => { + document.body.innerHTML = "abc"; + getSelection().selectAllChildren(document.documentElement); + await deleteWithDeleteKey(); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'getSelection().selectAllChildren(document.documentElement), then, Delete'); + +test(() => { + document.body.innerHTML = "abc"; + getSelection().selectAllChildren(document.documentElement); + document.execCommand("forwarddelete", false, false); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'getSelection().selectAllChildren(document.documentElement), then, execCommand("forwarddelete")'); + +test(() => { + document.body.innerHTML = "abc"; + getSelection().selectAllChildren(document.documentElement); + document.execCommand("delete", false, false); + assert_in_array(document.body.innerHTML, ["", "<br>"]); +}, 'getSelection().selectAllChildren(document.documentElement), then, execCommand("delete")'); + +</script> +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/editing/other/selectall-in-editinghost.html b/testing/web-platform/tests/editing/other/selectall-in-editinghost.html new file mode 100644 index 0000000000..680817a771 --- /dev/null +++ b/testing/web-platform/tests/editing/other/selectall-in-editinghost.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset=utf-8> +<title>Select All in focused editor</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +"use strict"; + +addEventListener("DOMContentLoaded", () => { + const editingHost = document.querySelector("div[contenteditable]"); + test(() => { + editingHost.focus(); + document.execCommand("selectAll"); + assert_false( + getSelection().isCollapsed, + 'Selection should not be collapsed after calling document.execCommand("selectAll")' + ); + const rangeText = getSelection().toString(); + assert_false( + rangeText.includes("preceding text"), + "Selection should not contain the preceding text of the editing host" + ); + assert_true( + rangeText.includes("editable text"), + "Selection should contain the editable text in the editing host" + ); + assert_false( + rangeText.includes("following text"), + "Selection should not contain the following text of the editing host" + ); + getSelection().removeAllRanges(); + }, "execCommand('selectAll') should select all content in the editing host"); + + test(() => { + editingHost.focus(); + getSelection().removeAllRanges(); + document.execCommand("selectAll"); + assert_false( + getSelection().isCollapsed, + 'Selection should not be collapsed after calling document.execCommand("selectAll")' + ); + const rangeText = getSelection().toString(); + assert_false( + rangeText.includes("preceding text"), + "Selection should not contain the preceding text of the editing host" + ); + assert_true( + rangeText.includes("editable text"), + "Selection should contain the editable text in the editing host" + ); + assert_false( + rangeText.includes("following text"), + "Selection should not contain the following text of the editing host" + ); + getSelection().removeAllRanges(); + }, "execCommand('selectAll') should select all content in the editing host when it has focus but no selection range"); + + test(() => { + editingHost.focus(); + editingHost.innerHTML = "preceding editable text<input value='input value'>following editable text"; + getSelection().collapse(editingHost.querySelector("input"), 0); + document.execCommand("selectAll"); + assert_false( + getSelection().isCollapsed, + 'Selection should not be collapsed after calling document.execCommand("selectAll")' + ); + const rangeText = getSelection().toString(); + assert_false( + rangeText.includes("preceding text"), + "Selection should not contain the preceding text of the editing host" + ); + assert_true( + rangeText.includes("preceding editable text"), + "Selection should contain the preceding editable text of <input> in the editing host" + ); + assert_true( + rangeText.includes("following editable text"), + "Selection should contain the following editable text of <input> in the editing host" + ); + assert_false( + rangeText.includes("following text"), + "Selection should not contain the following text of the editing host" + ); + getSelection().removeAllRanges(); + }, "execCommand('selectAll') should select all content in the editing host when selection collapsed in the <input>"); + + test(() => { + editingHost.focus(); + editingHost.innerHTML = "preceding editable text<textarea>textarea value</textarea>following editable text"; + getSelection().collapse(editingHost.querySelector("textarea"), 0); + document.execCommand("selectAll"); + assert_false( + getSelection().isCollapsed, + 'Selection should not be collapsed after calling document.execCommand("selectAll")' + ); + const rangeText = getSelection().toString(); + assert_false( + rangeText.includes("preceding text"), + "Selection should not contain the preceding text of the editing host" + ); + assert_true( + rangeText.includes("preceding editable text"), + "Selection should contain the preceding editable text of <textarea> in the editing host" + ); + assert_true( + rangeText.includes("following editable text"), + "Selection should contain the following editable text of <textarea> in the editing host" + ); + assert_false( + rangeText.includes("following text"), + "Selection should not contain the following text of the editing host" + ); + getSelection().removeAllRanges(); + }, "execCommand('selectAll') should select all content in the editing host when selection collapsed in the <textarea>"); +}); +</script> +</head> +<body> +<p>preceding text</p> +<div contenteditable>editable text</div> +<p>following text</p> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/selectall-without-focus.html b/testing/web-platform/tests/editing/other/selectall-without-focus.html new file mode 100644 index 0000000000..508fdc47a2 --- /dev/null +++ b/testing/web-platform/tests/editing/other/selectall-without-focus.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<head> +<meta charset=utf-8> +<title>Select All without focus should select not select only in the editing host</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +"use strict"; + +addEventListener("DOMContentLoaded", () => { + test(() => { + document.head.remove(); + document.execCommand("selectAll"); + assert_false( + getSelection().isCollapsed, + 'Selection should not be collapsed after calling document.execCommand("selectAll")' + ); + const rangeText = getSelection().toString(); + assert_true( + rangeText.includes("preceding text"), + "Selection should contain the preceding text of the editing host" + ); + assert_true( + rangeText.includes("editable text"), + "Selection should contain the editable text in the editing host" + ); + getSelection().removeAllRanges(); + }, "execCommand('selectAll') should select all content in the document even if the document body ends with editable content"); + + test(() => { + document.querySelector("p").innerHTML = "preceding text <input value='input value'>"; + getSelection().collapse(document.querySelector("input"), 0); + document.execCommand("selectAll"); + assert_false( + getSelection().isCollapsed, + 'Selection should not be collapsed after calling document.execCommand("selectAll")' + ); + const rangeText = getSelection().toString(); + assert_true( + rangeText.includes("preceding text"), + "Selection should contain the preceding text of the editing host" + ); + assert_true( + rangeText.includes("editable text"), + "Selection should contain the editable text in the editing host" + ); + getSelection().removeAllRanges(); + }, "execCommand('selectAll') should select all content in the document when selection is in <input>"); + + test(() => { + document.querySelector("p").innerHTML = "preceding text <textarea>textarea value</textarea>"; + getSelection().collapse(document.querySelector("textarea"), 0); + document.execCommand("selectAll"); + assert_false( + getSelection().isCollapsed, + 'Selection should not be collapsed after calling document.execCommand("selectAll")' + ); + const rangeText = getSelection().toString(); + assert_true( + rangeText.includes("preceding text"), + "Selection should contain the preceding text of the editing host" + ); + assert_true( + rangeText.includes("editable text"), + "Selection should contain the editable text in the editing host" + ); + getSelection().removeAllRanges(); + }, "execCommand('selectAll') should select all content in the document when selection is in <textarea>"); +}); +</script> +</head> +<p>preceding text</p> +<div contenteditable>editable text diff --git a/testing/web-platform/tests/editing/other/setting-value-of-textcontrol-immediately-after-hidden.html b/testing/web-platform/tests/editing/other/setting-value-of-textcontrol-immediately-after-hidden.html new file mode 100644 index 0000000000..f8a867f078 --- /dev/null +++ b/testing/web-platform/tests/editing/other/setting-value-of-textcontrol-immediately-after-hidden.html @@ -0,0 +1,118 @@ +<!doctype html> +<meta charset=utf-8> +<meta name="variant" content="?editor=input&hide-target=editor"> +<meta name="variant" content="?editor=textarea&hide-target=editor"> +<meta name="variant" content="?editor=input&hide-target=parent"> +<meta name="variant" content="?editor=textarea&hide-target=parent"> +<title>Testing edit action in zombie editor</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<body> +<script> +"use strict"; + +const params = new URLSearchParams(location.search); + +/** + * The expected results is based on Chrome 93. + * The behavior is reasonable because JS API which does not require focus keeps + * working even if it's hidden. + */ + +function init() { + const div = document.createElement("div"); + const editor = document.createElement(params.get("editor")); + const hideTarget = params.get("hide-target") == "editor" ? editor : div; + editor.value = "default value"; + div.appendChild(editor); + document.body.appendChild(div); + return [ hideTarget, editor ]; +} + +function finalize(editor) { + editor.blur(); + editor.parentNode.remove(); + document.body.getBoundingClientRect(); +} + +promise_test(async () => { + await new Promise(resolve => addEventListener("load", resolve, {once: true})); +}, "Wait for load event"); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + hideTarget.style.display = "none"; + editor.value = "new value"; + assert_equals(editor.value, "new value", "The value should be set properly"); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}>.value = "new value" (without focus)`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.focus(); + hideTarget.style.display = "none"; + editor.value = "new value"; + assert_equals(editor.value, "new value", "The value should be set properly"); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}>.value = "new value" (with focus)`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.focus(); + editor.blur(); + hideTarget.style.display = "none"; + editor.value = "new value"; + assert_equals(editor.value, "new value", "The value should be set properly"); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}>.value = "new value" (after blur)`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + hideTarget.style.display = "none"; + editor.setRangeText("new", 0, "default".length); + assert_equals(editor.value, "new value", "The value should be set properly by setRangeText"); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}>.setRangeText("new", 0, "default".length) (without focus)`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.focus(); + hideTarget.style.display = "none"; + editor.setRangeText("new", 0, "default".length); + assert_equals(editor.value, "new value", "The value should be set properly by setRangeText"); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}>.setRangeText("new", 0, "default".length) (with focus)`); + +promise_test(async () => { + const [hideTarget, editor] = init(); + try { + editor.focus(); + editor.blur(); + hideTarget.style.display = "none"; + editor.setRangeText("new", 0, "default".length); + assert_equals(editor.value, "new value", "The value should be set properly by setRangeText"); + } finally { + finalize(editor); + } +}, `<${params.get("editor")}>.setRangeText("new", 0, "default".length) (after blur)`); + +</script> +</body> diff --git a/testing/web-platform/tests/editing/other/typing-around-link-element-after-joining-paragraphs.html b/testing/web-platform/tests/editing/other/typing-around-link-element-after-joining-paragraphs.html new file mode 100644 index 0000000000..4934530c9b --- /dev/null +++ b/testing/web-platform/tests/editing/other/typing-around-link-element-after-joining-paragraphs.html @@ -0,0 +1,194 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?action=Backspace"> +<meta name="variant" content="?action=Delete"> +<title>Typing after joining paragraph shouldn't be inserted into the link</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +const params = new URLSearchParams(location.search.substring(1)); +const backspace = params.get("action") == "Backspace"; +const bracketsForBackspace = backspace ? "[]" : ""; +const bracketsForDelete = backspace ? "" : "[]"; + +const editingHost = document.querySelector("div[contenteditable]"); +const utils = new EditorTestUtils(editingHost); + +function addPromiseTest(aTest) { + promise_test(async () => { + editingHost.focus(); + utils.setupEditingHost(aTest.innerHTML); + await (backspace ? utils.sendBackspaceKey() : utils.sendDeleteKey()); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + utils.normalizeStyleAttributeValues(); + if (Array.isArray(aTest.expectedResult)) { + assert_in_array(editingHost.innerHTML, aTest.expectedResult); + } else { + assert_equals(editingHost.innerHTML, aTest.expectedResult); + } + }, `${backspace ? "Backspace" : "Delete"} in "${aTest.innerHTML}"`); +} + +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY<span style="color:rgb(255, 0, 0)">bar</span></p>` +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a><br></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY<span style="color:rgb(255, 0, 0)">bar</span></p>` +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }<br></a></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY<span style="color:rgb(255, 0, 0)">bar</span></p>` +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY<span style="color:rgb(255, 0, 0)">bar</span></p>` +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a><br></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY<span style="color:rgb(255, 0, 0)">bar</span></p>` +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }<br></a></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY<span style="color:rgb(255, 0, 0)">bar</span></p>` +}); +addPromiseTest({ + innerHTML: `<a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a><br><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY<span style="color:rgb(255, 0, 0)">bar</span>`, +}); +addPromiseTest({ + innerHTML: `<a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }<br></a><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY<span style="color:rgb(255, 0, 0)">bar</span>`, +}); + +// Should clear only the link style. +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo${ + bracketsForDelete + }</b></a></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo</b></a><b>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo${ + bracketsForDelete + }</b></a><br></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo</b></a><b>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo${ + bracketsForDelete + }</b><br></a></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo</b></a><b>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo${ + bracketsForDelete + }</b></a></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo</b></a><b>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo${ + bracketsForDelete + }</b></a><br></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo</b></a><b>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo${ + bracketsForDelete + }</b><br></a></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo</b></a><b>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo${ + bracketsForDelete + }</b></a><br><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo</b></a><b>XY</b><span style="color:rgb(255, 0, 0)">bar</span>`, +}); +addPromiseTest({ + innerHTML: `<a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo${ + bracketsForDelete + }</b><br></a><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<a href="about:blank" style="color:rgb(0, 0, 255)"><b>foo</b></a><b>XY</b><span style="color:rgb(255, 0, 0)">bar</span>`, +}); + +addPromiseTest({ + innerHTML: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a></b></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a></b><br></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }<br></a></b></p><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a></b></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a></b><br></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }<br></a></b></p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span>`, + expectedResult: `<p><b><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY</b><span style="color:rgb(255, 0, 0)">bar</span></p>`, +}); +addPromiseTest({ + innerHTML: `<b><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }</a></b><br><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<b><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY</b><span style="color:rgb(255, 0, 0)">bar</span>`, +}); +addPromiseTest({ + innerHTML: `<b><a href="about:blank" style="color:rgb(0, 0, 255)">foo${ + bracketsForDelete + }<br></a></b><p><span style="color:rgb(255, 0, 0)">${bracketsForBackspace}bar</span></p>`, + expectedResult: `<b><a href="about:blank" style="color:rgb(0, 0, 255)">foo</a>XY</b><span style="color:rgb(255, 0, 0)">bar</span>`, +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/typing-around-link-element-at-collapsed-selection.tentative.html b/testing/web-platform/tests/editing/other/typing-around-link-element-at-collapsed-selection.tentative.html new file mode 100644 index 0000000000..2b2e304aba --- /dev/null +++ b/testing/web-platform/tests/editing/other/typing-around-link-element-at-collapsed-selection.tentative.html @@ -0,0 +1,635 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?target=ContentEditable"> +<meta name="variant" content="?target=ContentEditable&parent=b"> +<meta name="variant" content="?target=ContentEditable&child=b"> +<meta name="variant" content="?target=ContentEditable&parent=b&child=i"> +<meta name="variant" content="?target=DesignMode"> +<meta name="variant" content="?target=DesignMode&parent=b"> +<meta name="variant" content="?target=DesignMode&child=b"> +<meta name="variant" content="?target=DesignMode&parent=b&child=i"> +<title>Testing inserting content around link element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<style> +.bold { + font-weight: bold; +} +</style> +</head> +<body> +<div contenteditable></div> +<iframe srcdoc=" + <!doctype html> + <html> + <script>document.designMode='on';</script> + <script src='/resources/testdriver.js'></script> + <script src='/resources/testdriver-vendor.js'></script> + <script src='/resources/testdriver-actions.js'></script> + <body></body> + </html>"></iframe> +<script> +"use strict"; + +const params = new URLSearchParams(location.search.substring(1)); +const kTarget = params.get("target"); +const kParentTag = params.get("parent") === null + ? ["", ""] + : [`<${params.get("parent")}>`, `</${params.get("parent")}>`]; +const kChildTag = params.get("child") === null + ? ["", ""] + : [`<${params.get("child")}>`, `</${params.get("child")}>`]; +const kLinkDesc = (() => { + let result = "" + if (kParentTag[0] !== "") { + result += `in ${kParentTag[0]} `; + if (kChildTag[0] !== "") { + result += "and "; + } + } + if (kChildTag[0] !== "") { + result += `containing ${kChildTag[0]} `; + } + return result; +})(); +const kNewContainerOfLink = (() => { + if (kParentTag !== "" && kChildTag !== "") { + return [`${kParentTag[0]}${kChildTag[0]}`, `${kChildTag[1]}${kParentTag[1]}`]; + } + if (kParentTag !== "") { + return kParentTag; + } + if (kChildTag !== "") { + return kChildTag; + } + return ["", ""]; +})(); +const kSelectorForTextNodeContainer = kChildTag[0] === "" + ? "a" + : `a > ${kChildTag[0].substr(1, kChildTag[0].length - 2)}`; + +function getEditingHost() { + return kTarget === "ContentEditable" + ? document.querySelector("div[contenteditable]") + : document.querySelector("iframe").contentDocument.body; +} + +function addPromiseTest(test) { + promise_test(async () => { + let editingHost = getEditingHost(); + let utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(test.innerHTML); + utils.window.focus(); + utils.document.body.focus(); + editingHost.focus(); + await test.run(utils); + if (Array.isArray(test.expectedResult)) { + assert_in_array(editingHost.innerHTML, test.expectedResult); + } else { + assert_equals(editingHost.innerHTML, test.expectedResult); + } + }, `${test.description.trim()} in ${test.innerHTML}`); +} + +promise_test(async () => { + await new Promise(resolve => { + addEventListener("load", resolve, { once: true }); + }); +}, ""); + +if (kChildTag[0] === "") { + // Immediately after creating a link with Document.execCommand. + + addPromiseTest({ + description: `Replacing text in a link ${kLinkDesc}with "XY"`, + innerHTML: `<p>${kParentTag[0]}[abc]${kParentTag[1]}</p>`, + run: async (utils) => { + utils.document.execCommand("createLink", false, "about:blank"); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}<a href="about:blank">XY${kParentTag[1]}</a></p>`, + `<p>${kParentTag[0]}<a href="about:blank">XY${kParentTag[1]}</a><br></p>`, + ], + }); + + addPromiseTest({ + description: `Inserting "XY" after making a link ${kLinkDesc}(following Selection.collapseToEnd)`, + innerHTML: `<p>${kParentTag[0]}[abc]${kParentTag[1]}</p>`, + run: async (utils) => { + utils.document.execCommand("createLink", false, "about:blank"); + utils.selection.collapseToEnd(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}<a href="about:blank">abc</a>XY${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}<a href="about:blank">abc</a>XY${kParentTag[1]}<br></p>`, + ], + }); + + addPromiseTest({ + description: `Inserting "XY" after making a link ${kLinkDesc}(following ArrowRight key press)`, + innerHTML: `<p>${kParentTag[0]}[abc]${kParentTag[1]}</p>`, + run: async (utils) => { + utils.document.execCommand("createLink", false, "about:blank"); + await utils.sendArrowRightKey(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}<a href="about:blank">abc</a>XY${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}<a href="about:blank">abc</a>XY${kParentTag[1]}<br></p>`, + ], + }); + + addPromiseTest({ + description: `Inserting "XY" after making a link ${kLinkDesc}(following End key press)`, + innerHTML: `<p>${kParentTag[0]}[abc]${kParentTag[1]}</p>`, + run: async (utils) => { + utils.document.execCommand("createLink", false, "about:blank"); + if (!navigator.platform.includes("Mac")) { + await utils.sendEndKey(); + } else { + await utils.sendArrowRightKey(utils.kMetaKey); + } + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}<a href="about:blank">abc</a>XY${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}<a href="about:blank">abc</a>XY${kParentTag[1]}<br></p>`, + ], + }); + + addPromiseTest({ + description: `Inserting "XY" after making a link ${kLinkDesc}(following Selection.collapseToStart)`, + innerHTML: `<p>${kParentTag[0]}[abc]${kParentTag[1]}</p>`, + run: async (utils) => { + utils.document.execCommand("createLink", false, "about:blank"); + utils.selection.collapseToStart(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}XY<a href="about:blank">abc</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}XY<a href="about:blank">abc</a>${kParentTag[1]}<br></p>`, + ], + }); + + addPromiseTest({ + description: `Inserting "XY" after making a link ${kLinkDesc}(following ArrowLeft key press)`, + innerHTML: `<p>${kParentTag[0]}[abc]${kParentTag[1]}</p>`, + run: async (utils) => { + utils.document.execCommand("createLink", false, "about:blank"); + await utils.sendArrowLeftKey(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}XY<a href="about:blank">abc</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}XY<a href="about:blank">abc</a>${kParentTag[1]}<br></p>`, + ], + }); + + addPromiseTest({ + description: `Inserting "XY" after making a link ${kLinkDesc}(following Home key press)`, + innerHTML: `<p>${kParentTag[0]}[abc]${kParentTag[1]}</p>`, + run: async (utils) => { + utils.document.execCommand("createLink", false, "about:blank"); + if (!navigator.platform.includes("Mac")) { + await utils.sendHomeKey(); + } else { + await utils.sendArrowLeftKey(utils.kMetaKey); + } + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}XY<a href="about:blank">abc</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}XY<a href="about:blank">abc</a>${kParentTag[1]}<br></p>`, + ], + }); +} + +addPromiseTest({ + description: `Inserting "XY" after setting caret position to middle of a link ${kLinkDesc}(Selection.collapse)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + utils.selection.collapse(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, 2); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abXYc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abXYc${kChildTag[1]}</a>${kParentTag[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after setting caret position to middle of a link ${kLinkDesc}(Selection.addRange)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + utils.selection.removeAllRanges(); + let range = utils.document.createRange(); + range.setStart(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, 2); + utils.selection.addRange(range); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abXYc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abXYc${kChildTag[1]}</a>${kParentTag[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after setting caret position to start of a link ${kLinkDesc}(Selection.collapse)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}ab[]c${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + utils.selection.collapse(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, 0); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[2]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after setting caret position to start of a link ${kLinkDesc}(Selection.addRange)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}ab[]c${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + utils.selection.removeAllRanges(); + let range = utils.document.createRange(); + range.setStart(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, 0); + utils.selection.addRange(range); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[2]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after setting caret position to end of a link ${kLinkDesc}(Selection.collapse)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}ab[]c${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + utils.selection.collapse(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, "abc".length); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[2]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after setting caret position to end of a link ${kLinkDesc}(Selection.addRange)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}ab[]c${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + utils.selection.collapse(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, "abc".length); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[2]}<br></p>`, + ], +}); + +// Type text after moving caret with Range API. +addPromiseTest({ + description: `Inserting "XY" after modifying caret position to middle of a link ${kLinkDesc}`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + let range = utils.selection.getRangeAt(0); + range.setStart(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, 2); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abXYc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abXYc${kChildTag[1]}</a>${kParentTag[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after modifying caret position to start of a link ${kLinkDesc}`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}ab[]c${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + let range = utils.selection.getRangeAt(0); + range.setStart(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, 0); + range.setEnd(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, 0); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after modifying caret position to end of a link ${kLinkDesc}`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}ab[]c${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + let range = utils.selection.getRangeAt(0); + range.setStart(utils.editingHost.querySelector(kSelectorForTextNodeContainer).firstChild, "abc".length); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +// Type text after deleting character immediately before/after a link. +addPromiseTest({ + description: `Inserting "XY" after deleting following character of a link ${kLinkDesc}(Backspace)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abc${kChildTag[1]}</a>d[]${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendBackspaceKey(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting following character of a link ${kLinkDesc}(execCommand("delete"))`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abc${kChildTag[1]}</a>d[]${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.document.execCommand("delete", false); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting a previous character of a link ${kLinkDesc}(Delete)`, + innerHTML: `<p>${kParentTag[0]}[]z<a href="about:blank">${kChildTag[0]}abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendDeleteKey(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting a previous character of a link ${kLinkDesc}(execCommand("forwarddelete"))`, + innerHTML: `<p>${kParentTag[0]}[]z<a href="about:blank">${kChildTag[0]}abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.document.execCommand("forwarddelete", false); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +// Type text after deleting the last character in a link. +addPromiseTest({ + description: `Inserting "XY" after deleting last character of a link ${kLinkDesc}(Backspace)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abcd[]${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendBackspaceKey(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[1]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting last character of a link ${kLinkDesc}(execCommand("delete"))`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abcd[]${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.document.execCommand("delete", false); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[1]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting last character of a link ${kLinkDesc}(Delete)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abc[]d${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendDeleteKey(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting last character of a link ${kLinkDesc}(execCommand("forwarddelete"))`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abc[]d${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.document.execCommand("forwarddelete", false); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +// Type text after deleting the first character in a link. +addPromiseTest({ + description: `Inserting "XY" after deleting first character of a link ${kLinkDesc}(Backspace)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}z[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendBackspaceKey(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting first character of a link ${kLinkDesc}(execCommand("delete"))`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}z[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.document.execCommand("delete", false); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting first character of a link ${kLinkDesc}(Delete)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}[]zabc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendDeleteKey(); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +addPromiseTest({ + description: `Inserting "XY" after deleting first character of a link ${kLinkDesc}(execCommand("forwarddelete"))`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}[]zabc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.document.execCommand("forwarddelete", false); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); + +// Don't create `<span>` element for preserving specified style of the link and +// don't clone `class` nor `style` attribute of the link. +addPromiseTest({ + description: `Inserting "XY" at start of a link which has a class for bold text`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank" class="bold">${kChildTag[0]}[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank" class="bold">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank" class="bold">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); +addPromiseTest({ + description: `Inserting "XY" at end of a link which has a class for bold text`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank" class="bold">${kChildTag[0]}abc[]${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank" class="bold">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank" class="bold">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); +addPromiseTest({ + description: `Inserting "XY" at start of a link which has inline style for bold text`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank" style="font-weight: bold">${kChildTag[0]}[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank" style="font-weight: bold">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank" style="font-weight: bold">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); +addPromiseTest({ + description: `Inserting "XY" at end of a link which has inline style for bold text`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank" style="font-weight: bold">${kChildTag[0]}abc[]${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank" style="font-weight: bold">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank" style="font-weight: bold">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); +document.execCommand("styleWithCSS", false, "true"); +addPromiseTest({ + description: `Inserting "XY" at start of a link which has a class for bold text (in CSS mode)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank" class="bold">${kChildTag[0]}[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank" class="bold">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank" class="bold">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); +addPromiseTest({ + description: `Inserting "XY" at end of a link which has a class for bold text (in CSS mode)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank" class="bold">${kChildTag[0]}abc[]${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank" class="bold">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank" class="bold">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); +addPromiseTest({ + description: `Inserting "XY" at start of a link which has inline style for bold text (in CSS mode)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank" style="font-weight: bold">${kChildTag[0]}[]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank" style="font-weight: bold">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank" style="font-weight: bold">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ], +}); +document.execCommand("styleWithCSS", false, "false"); +addPromiseTest({ + description: `Inserting "XY" at end of a link which has inline style for bold text (in CSS mode)`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank" style="font-weight: bold">${kChildTag[0]}abc[]${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank" style="font-weight: bold">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank" style="font-weight: bold">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ], +}); +document.execCommand("styleWithCSS", false, "false"); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/typing-around-link-element-at-non-collapsed-selection.tentative.html b/testing/web-platform/tests/editing/other/typing-around-link-element-at-non-collapsed-selection.tentative.html new file mode 100644 index 0000000000..a9e5790c35 --- /dev/null +++ b/testing/web-platform/tests/editing/other/typing-around-link-element-at-non-collapsed-selection.tentative.html @@ -0,0 +1,214 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?target=ContentEditable"> +<meta name="variant" content="?target=ContentEditable&parent=b"> +<meta name="variant" content="?target=ContentEditable&child=b"> +<meta name="variant" content="?target=ContentEditable&parent=b&child=i"> +<meta name="variant" content="?target=DesignMode"> +<meta name="variant" content="?target=DesignMode&parent=b"> +<meta name="variant" content="?target=DesignMode&child=b"> +<meta name="variant" content="?target=DesignMode&parent=b&child=i"> +<title>Testing inserting content at non-collapsed selection around link element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<iframe srcdoc=" + <!doctype html> + <html> + <script>document.designMode='on';</script> + <script src='/resources/testdriver.js'></script> + <script src='/resources/testdriver-vendor.js'></script> + <script src='/resources/testdriver-actions.js'></script> + <body></body> + </html>"></iframe> +<script> +"use strict"; + +const params = new URLSearchParams(location.search.substring(1)); +const kTarget = params.get("target"); +const kParentTag = params.get("parent") === null + ? ["", ""] + : [`<${params.get("parent")}>`, `</${params.get("parent")}>`]; +const kChildTag = params.get("child") === null + ? ["", ""] + : [`<${params.get("child")}>`, `</${params.get("child")}>`]; +const kLinkDesc = (() => { + let result = "" + if (kParentTag[0] !== "") { + result += `in ${kParentTag[0]} `; + if (kChildTag[0] !== "") { + result += "and "; + } + } + if (kChildTag[0] !== "") { + result += `containing ${kChildTag[0]} `; + } + return result; +})(); +const kNewContainerOfLink = (() => { + if (kParentTag !== "" && kChildTag !== "") { + return [`${kParentTag[0]}${kChildTag[0]}`, `${kChildTag[1]}${kParentTag[1]}`]; + } + if (kParentTag !== "") { + return kParentTag; + } + if (kChildTag !== "") { + return kChildTag; + } + return ["", ""]; +})(); + +function getEditingHost() { + return kTarget === "ContentEditable" + ? document.querySelector("div[contenteditable]") + : document.querySelector("iframe").contentDocument.body; +} + +function addPromiseTest(test) { + promise_test(async () => { + let editingHost = getEditingHost(); + let utils = new EditorTestUtils(editingHost); + utils.setupEditingHost(test.innerHTML); + utils.window.focus(); + utils.document.body.focus(); + editingHost.focus(); + await test.run(utils); + if (Array.isArray(test.expectedResult)) { + assert_in_array(editingHost.innerHTML, test.expectedResult); + } else { + assert_equals(editingHost.innerHTML, test.expectedResult); + } + }, `${test.description} in ${test.innerHTML}`); +} + +promise_test(async () => { + await new Promise(resolve => { + addEventListener("load", resolve, { once: true }); + }); +}, ""); + +for (const test of [ + ["Direct typing", utils => {}], + ["Backspace", utils => { return utils.sendBackspaceKey(); }], + ["Delete", utils => { return utils.sendDeleteKey(); }], + ["execCommand(\"delete\")", utils => { utils.document.execCommand("delete", false); }], + ["execCommand(\"forwarddelete\")", utils => { utils.document.execCommand("forwarddelete", false); }], + ]) { + + addPromiseTest({ + description: `Inserting "XY" after deleting first character of a link ${kLinkDesc}(${test[0]})`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}[z]abc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await test[1](utils); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: (() => { + if (test[0] === "Direct typing") { + return [ + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}XYabc${kChildTag[1]}</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}XYabc${kChildTag[1]}</a>${kParentTag[1]}<br></p>`, + ]; + } + return [ + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}XY<a href="about:blank">abc</a>${kNewContainerOfLink[1]}<br></p>`, + ]; + })(), + }); + + addPromiseTest({ + description: `Inserting "XY" after deleting last character in a non-collapsed range of a link ${kLinkDesc}(${test[0]})`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abc[d]${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await test[1](utils); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: (() => { + if (test[0] === "Direct typing") { + return [ + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abcXY${kChildTag[1]}</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}abcXY${kChildTag[1]}</a>${kParentTag[1]}<br></p>`, + ]; + } + return [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">abc</a>XY${kNewContainerOfLink[1]}<br></p>`, + ]; + })(), + }); + + addPromiseTest({ + description: `Inserting "XY" after deleting text after middle of a link ${kLinkDesc}(${test[0]})`, + innerHTML: `<p>${kParentTag[0]}<a href="about:blank">${kChildTag[0]}ab[cd${kChildTag[1]}</a>de]f${kParentTag[1]}</p>`, + run: async (utils) => { + await test[1](utils); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kNewContainerOfLink[0]}<a href="about:blank">ab</a>XY${kChildTag[1]}f${kParentTag[1]}</p>`, + `<p>${kNewContainerOfLink[0]}<a href="about:blank">ab</a>XY${kChildTag[1]}f${kParentTag[1]}<br></p>`, + ], + }); + + addPromiseTest({ + description: `Inserting "XY" after deleting text before middle of a link ${kLinkDesc}(${test[0]})`, + innerHTML: `<p>${kParentTag[0]}a[bc<a href="about:blank">${kChildTag[0]}de]f${kChildTag[1]}</a>${kParentTag[1]}</p>`, + run: async (utils) => { + await test[1](utils); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + `<p>${kParentTag[0]}aXY<a href="about:blank">${kChildTag[0]}f${kChildTag[1]}</a>${kParentTag[1]}</p>`, + `<p>${kParentTag[0]}aXY<a href="about:blank">${kChildTag[0]}f${kChildTag[1]}</a>${kParentTag[1]}<br></p>`, + ], + }); + + if (kParentTag[0] !== "" || kChildTag[0] !== "") { + continue; + } + + addPromiseTest({ + description: `Inserting "XY" after deleting text between 2 same links (${test[0]})`, + innerHTML: '<p><a href="about:blank">a[bc</a><a href="about:blank">de]f</a></p>', + run: async (utils) => { + await test[1](utils); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + '<p><a href="about:blank">a</a>XY<a href="about:blank">f</a></p>', + '<p><a href="about:blank">a</a>XY<a href="about:blank">f</a><br></p>', + ], + }); + + addPromiseTest({ + description: `Inserting "XY" after deleting text between 2 different links (${test[0]})`, + innerHTML: '<p><a href="about:blank">a[bc</a><a href="http://example.com/">de]f</a></p>', + run: async (utils) => { + await test[1](utils); + await utils.sendKey("X", utils.kShiftKey); + await utils.sendKey("Y", utils.kShiftKey); + }, + expectedResult: [ + '<p><a href="about:blank">a</a>XY<a href="http://example.com/">f</a></p>', + '<p><a href="about:blank">a</a>XY<a href="http://example.com/">f</a><br></p>', + ], + }); +} + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/typing-space-in-editable-button.tentative.html b/testing/web-platform/tests/editing/other/typing-space-in-editable-button.tentative.html new file mode 100644 index 0000000000..0f399378ab --- /dev/null +++ b/testing/web-platform/tests/editing/other/typing-space-in-editable-button.tentative.html @@ -0,0 +1,77 @@ +<!doctype html> +<head> +<meta charset="utf-8"> +<title>Tests for pressing space in editable button element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +</head> +<body> +<button contenteditable>HelloWorld</button> +<div contenteditable><button>HelloWorld</button></div> +<button><div contenteditable>HelloWorld</div></button> +<script> +"use strict"; + +promise_test(async () => { + await new Promise(resolve => { + addEventListener("load", resolve, {once: true}); + }); + const button = document.querySelector("button[contenteditable]"); + getSelection().collapse(button.firstChild, "Hello".length); + let clickEvent = null; + button.addEventListener("click", event => clickEvent = event, {once: true}); + await new this.window.test_driver.Actions() + .keyDown("\uE00D") + .keyUp("\uE00D") + .send(); + assert_equals(button.textContent, "HelloWorld", "The button label shouldn't be changed"); + assert_not_equals(clickEvent, null, "Click event should be fired on the <button>"); +}, "Type space key in <button contenteditable> should be handled by the <button>"); + +promise_test(async () => { + document.querySelector("div[contenteditable]").focus(); + const button = document.querySelector("div[contenteditable] > button"); + getSelection().collapse(button.firstChild, "Hello".length); + let clickEvent = null; + button.addEventListener("click", event => clickEvent = event, {once: true}); + await new this.window.test_driver.Actions() + .keyDown("\uE00D") + .keyUp("\uE00D") + .send(); + assert_equals(button.textContent, "Hello World", "A space should be inserted into the button label"); + assert_equals(clickEvent, null, "Click event should not be fired on the <button>"); +}, "Type space key in editable <button> shouldn't be handled by the <button> when it's not focused"); + +promise_test(async () => { + const button = document.querySelector("div[contenteditable] > button"); + button.textContent = "HelloWorld"; + button.focus(); + let clickEvent = null; + button.addEventListener("click", event => clickEvent = event, {once: true}); + await new this.window.test_driver.Actions() + .keyDown("\uE00D") + .keyUp("\uE00D") + .send(); + assert_equals(button.textContent, "HelloWorld", "The button label shouldn't be changed"); + assert_not_equals(clickEvent, null, "Click event should be fired on the <button>"); +}, "Type space key in editable <button> should be handled by the <button> when it's focused"); + +promise_test(async () => { + const div = document.querySelector("button > div[contenteditable]"); + div.focus(); + getSelection().collapse(div.firstChild, "Hello".length); + let clickEvent = null; + div.parentElement.addEventListener("click", event => clickEvent = event, {once: true}); + await new this.window.test_driver.Actions() + .keyDown("\uE00D") + .keyUp("\uE00D") + .send(); + assert_equals(div.textContent, "Hello World", "A space should be inserted into the button label"); + assert_equals(clickEvent, null, "Click event should not be fired on the <button>"); +}, "Type space key in editable element in <button> shouldn't be handled by the <button>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/typing-space-in-editable-summary.tentative.html b/testing/web-platform/tests/editing/other/typing-space-in-editable-summary.tentative.html new file mode 100644 index 0000000000..30a751d523 --- /dev/null +++ b/testing/web-platform/tests/editing/other/typing-space-in-editable-summary.tentative.html @@ -0,0 +1,90 @@ +<!doctype html> +<head> +<meta charset="utf-8"> +<title>Tests for pressing space in editable summary element</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +</head> +<body> +<details contenteditable><summary>HelloWorld</summary>Details</details> +<details><summary contenteditable>HelloWorld</summary>Details</details> +<details><summary><div contenteditable>HelloWorld</div></summary>Details</details> +<script> +"use strict"; + +promise_test(async () => { + const details = document.querySelector("details[contenteditable]"); + const summary = details.querySelector("summary"); + getSelection().collapse(summary.firstChild, "Hello".length); + summary.focus(); + await new this.window.test_driver.Actions() + .keyDown("\uE00D") + .keyUp("\uE00D") + .send(); + assert_equals( + details.innerHTML, + "<summary>HelloWorld</summary>Details", + "A space shouldn't be inserted into the focused <summary>" + ); + assert_true(details.open, "<details> shouldn't keep collapsed"); +}, "Type space key in editable <summary> should be handled by the <summary> when it's focused"); + +promise_test(async () => { + const details = document.querySelector("details[contenteditable]"); + details.innerHTML = "<summary>HelloWorld</summary>Details"; + details.open = false; + const summary = details.querySelector("summary"); + getSelection().collapse(summary.firstChild, "Hello".length); + details.focus(); + await new this.window.test_driver.Actions() + .keyDown("\uE00D") + .keyUp("\uE00D") + .send(); + assert_equals( + details.innerHTML, + "<summary>Hello World</summary>Details", + "A space should be inserted into the <summary>" + ); + assert_false(details.open, "<details> should keep collapsed"); +}, "Type space key in editable <summary> shouldn't be handled by the <summary> when it's not focused"); + +promise_test(async () => { + const details = document.querySelector("details > summary[contenteditable]").parentNode; + const summary = details.querySelector("summary"); + getSelection().collapse(summary.firstChild, "Hello".length); + summary.focus(); + await new this.window.test_driver.Actions() + .keyDown("\uE00D") + .keyUp("\uE00D") + .send(); + assert_equals( + details.innerHTML, + '<summary contenteditable="">HelloWorld</summary>Details', + "The content of <details> shouldn't be changed" + ); + assert_true(details.open, "<details> shouldn't keep collapsed"); +}, "Type space key in <summary contenteditable> should be handled by the <summary>"); + +promise_test(async () => { + const details = document.querySelector("summary > div[contenteditable]").parentNode.parentNode; + const summary = details.querySelector("summary"); + const editable = summary.querySelector("div[contenteditable]"); + editable.focus(); + getSelection().collapse(editable.firstChild, "Hello".length); + await new this.window.test_driver.Actions() + .keyDown("\uE00D") + .keyUp("\uE00D") + .send(); + assert_equals( + details.innerHTML, + '<summary><div contenteditable="">Hello World</div></summary>Details', + "A space should be inserted" + ); + assert_false(details.open, "<details> should keep collapsed"); +}, "Type space key in editable element in <summary> shouldn't be handled by the <summary>"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/undo-insertparagraph-after-moving-split-nodes.html b/testing/web-platform/tests/editing/other/undo-insertparagraph-after-moving-split-nodes.html new file mode 100644 index 0000000000..c61bcff9e9 --- /dev/null +++ b/testing/web-platform/tests/editing/other/undo-insertparagraph-after-moving-split-nodes.html @@ -0,0 +1,109 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>Undo after splitting nodes are moved</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +document.execCommand("defaultParagraphSeparator", false, "div"); +const utils = + new EditorTestUtils(document.querySelector("div[contenteditable]")); + +promise_test(async t => { + utils.setupEditingHost( + `<div>abc[]def</div><p>ghi</p>` + ); + await utils.sendEnterKey(); + const right = utils.editingHost.querySelector("div + div"); + utils.editingHost.appendChild(right); + // Now, the right <div> is after the <p>, it should be merged into the left + // <div> before the <p>. + document.execCommand("undo"); + assert_in_array( + utils.editingHost.innerHTML, + [ + "<div>abcdef</div><p>ghi</p>", + "<div>abcdef<br></div><p>ghi</p>", + ] + ); +}, "Undo insertParagraph after moving right node to different paragraph"); + +promise_test(async () => { + utils.setupEditingHost( + `<p>abc</p><div>def[]ghi</div>` + ); + await utils.sendEnterKey(); + const left = utils.editingHost.querySelector("div"); + utils.editingHost.insertBefore(left, document.querySelector("p")); + // Now, the left <div> is before the <p>, the right <div> after the <p> should + // be merged into it. + document.execCommand("undo"); + assert_in_array( + utils.editingHost.innerHTML, + [ + "<div>defghi</div><p>abc</p>", + "<div>defghi<br></div><p>abc</p>", + ] + ); +}, "Undo insertParagraph after moving left node to different paragraph"); + +promise_test(async () => { + utils.setupEditingHost( + `<div>abc[]def</div>` + ); + await utils.sendEnterKey(); + const left = utils.editingHost.querySelector("div"); + const right = utils.editingHost.querySelector("div + div"); + left.insertBefore(right, left.firstChild); + // Now, the right <div> is a child node of the left <div>. Its children + // should be merged to the parent. + document.execCommand("undo"); + assert_in_array( + utils.editingHost.innerHTML, + [ + "<div>abcdef</div>", + "<div>abcdef<br></div>", + ] + ); +}, "Undo insertParagraph after moving right node into the left node"); + +promise_test(async () => { + utils.setupEditingHost( + `<div>abc[]def</div>` + ); + await utils.sendEnterKey(); + const left = utils.editingHost.querySelector("div"); + const right = utils.editingHost.querySelector("div + div"); + right.appendChild(left); + // Now, the right <div> is parent of the left <div>. The children of the + // right <div> should be moved to the child left <div>, but the right <div> + // should be removed. + document.execCommand("undo"); + assert_equals( + utils.editingHost.innerHTML, + "", + "The right <div> containing the left <div> should be removed" + ); + assert_in_array( + left.innerHTML, + [ + "abcdef", + "abcdef<br>", + ], + "The left <div> which was disconnected should have the original content" + ); +}, "Undo insertParagraph after moving left node into the right node"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-delete.tentative.html b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-delete.tentative.html new file mode 100644 index 0000000000..1490bf06f5 --- /dev/null +++ b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-delete.tentative.html @@ -0,0 +1,342 @@ +<!doctype html> +<html> +<head> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title>Testing normalizing white-space sequence after execCommand("delete", false, "")</title> +<script src=../include/implementation.js></script> +<script>var testsJsLibraryOnly = true</script> +<script src="../include/tests.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +"use strict"; + +setup({explicit_done: true}); + +function runTests() { + // README: + // These tests based on the behavior of Chrome 83. This test does NOT define + // nor suggest any standard behavior (actually, some expected results might + // look odd), but this test must help you to understand how other browsers + // use different logic to normalize white-space sequence. + + document.body.innerHTML = "<div contenteditable></div>"; + let editor = document.querySelector("div[contenteditable]"); + editor.focus(); + let selection = document.getSelection(); + + function toPlaintext(str) { + return str.replace(/ /g, "\u00A0"); + } + function escape(str) { + return str.replace(/\u00A0/ig, " "); + } + + // Test simple removing in a text node. + // - initialText: Set to data of text node (only entity is handled) + // - expectedText: Set to data of the text node after `execCommand("delete")` + // - white-spaceRange: Set first item to start offset of white-space sequence, + // set second item to number of white-spaces. + for (const currentTest of [ + { initialText: "a ", expectedText: "a", whiteSpaceRange: [1, 1] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 2] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 2] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 3] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 3] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 3] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 4] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 4] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 4] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 4] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 10] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 10] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 10] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 11] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 11] }, + { initialText: "a b", expectedText: "ab", whiteSpaceRange: [1, 1] }, + { initialText: "a b", expectedText: "ab", whiteSpaceRange: [1, 1] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 2] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 2] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 2] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 3] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 3] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 3] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 3] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 10] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 10] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 10] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 11] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 11] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 11] }, + { initialText: " b", expectedText: "b", whiteSpaceRange: [0, 1] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 2] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 2] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 3] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 3] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 3] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 10] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 10] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 10] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 11] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 11] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 11] }, + { initialText: " ", expectedText: "", whiteSpaceRange: [0, 1] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 2] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 3] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 3] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 4] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 4] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 4] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 10] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 10] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 10] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 11] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 11] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 11] }, + ]) { + for (let i = currentTest.whiteSpaceRange[0]; i < currentTest.whiteSpaceRange[0] + currentTest.whiteSpaceRange[1]; i++) { + currentTest.getInitialText = function (aCaretPos) { + return escape(`${toPlaintext(this.initialText).slice(0, aCaretPos)}[]${toPlaintext(this.initialText).slice(aCaretPos)}`); + } + test(function () { + editor.innerHTML = ""; + editor.appendChild(document.createTextNode(toPlaintext(currentTest.initialText))); + selection.collapse(editor.firstChild, i + 1); + document.execCommand("delete", false, ""); + if (currentTest.expectedText.length) { + assert_equals(escape(editor.childNodes.item(0).data), currentTest.expectedText, "Modified text is wrong"); + assert_equals(selection.focusNode, editor.childNodes.item(0), "Selection focus node is wrong"); + assert_equals(selection.focusOffset, i, "Selection focus offset is wrong"); + assert_equals(selection.anchorNode, editor.childNodes.item(0), "Selection anchor node is wrong"); + assert_equals(selection.anchorOffset, i, "Selection anchor offset is wrong"); + } else { + assert_equals(escape(editor.textContent), "", "Modified text is wrong"); + assert_equals(selection.focusNode, editor, "Selection focus node is wrong"); + assert_equals(selection.focusOffset, 0, "Selection focus offset is wrong"); + assert_equals(selection.anchorNode, editor, "Selection anchor node is wrong"); + assert_equals(selection.anchorOffset, 0, "Selection anchor offset is wrong"); + } + }, `execCommand("delete", false, ""): "${currentTest.getInitialText(i + 1)}" (length of whiteSpace sequence: ${currentTest.whiteSpaceRange[1]})`); + } + } + + // Test white space sequence split to multiple text node. + // - initialText: Set to data of text nodes. This must have "|" at least one. + // Then, the text will be split at every "|". + // Same as above test, only is handled at setting. + // "[]" means that caret position. + // - expectedText: Set to data of all text nodes as an array. + // Same as above test, only is handled before comparing. + for (const currentTest of [ + { initialText: "a |[] b", expectedText: ["a []", " b"] }, + { initialText: "a []| b", expectedText: ["a []", " b"] }, + { initialText: "a |[] b", expectedText: ["a []", " b"] }, + { initialText: "a []| b", expectedText: ["a []", " b"] }, + { initialText: "a |[] b", expectedText: ["a []", " b"] }, + { initialText: "a []| b", expectedText: ["a []", " b"] }, + { initialText: "a | [] b", expectedText: ["a []", " b"] }, + { initialText: "a | [] b", expectedText: ["a []", " b"] }, + { initialText: "a | [] b", expectedText: ["a []", " b"] }, + { initialText: "a | [] b", expectedText: ["a []", " b"] }, + + { initialText: "a | [] b", expectedText: ["a ", " [] b"] }, + { initialText: "a | [] b", expectedText: ["a []", " b"] }, + { initialText: "a |[] b", expectedText: ["a []", " b"] }, + { initialText: "a b[] | c", expectedText: ["a [] ", " c"] }, + { initialText: "a b[] | c", expectedText: ["a [] ", " c"] }, + { initialText: "a b[]| c", expectedText: ["a []", " c"] }, + { initialText: "a b|[] c", expectedText: ["a []", " c"] }, + { initialText: "a b[]| c", expectedText: ["a []", " c"] }, + { initialText: "a b|[] c", expectedText: ["a []", " c"] }, + { initialText: "a |b[] c", expectedText: ["a []", " c"] }, + { initialText: "a []|b c", expectedText: ["a []", "b c"] }, + { initialText: "a | b[] c", expectedText: ["a ", " [] c"] }, + + { initialText: "a | |[] c", expectedText: ["a []", " c"] }, + { initialText: "a | |[] c", expectedText: ["a []", " c"] }, + { initialText: "a | []| c", expectedText: ["a []", " c"] }, + { initialText: "a []| | c", expectedText: ["a []", " ", " c"] }, + { initialText: "a [] | | c", expectedText: ["a [] ", " ", " c"] }, + { initialText: "a [] | | c", expectedText: ["a [] ", " ", " c"] }, + { initialText: "a [] | | c", expectedText: ["a [] ", " ", " c"] }, + { initialText: "a || [] c", expectedText: ["a []", " c"] }, + { initialText: "a ||[] c", expectedText: ["a []", " c"] }, + { initialText: "a |[]| c", expectedText: ["a []", " c"] }, + { initialText: "a []|| c", expectedText: ["a []", " c"] }, + { initialText: "a [] || c", expectedText: ["a [] ", "", " c"] }, + ]) { + test(function () { + editor.innerHTML = ""; + let caret = { container: null, offset: -1 }; + for (let text of toPlaintext(currentTest.initialText).split("|")) { + let caretOffset = text.indexOf("[]"); + if (caretOffset >= 0) { + text = text.slice(0, caretOffset) + text.slice(caretOffset + 2); + } + let textNode = document.createTextNode(text); + editor.appendChild(textNode); + if (caretOffset >= 0) { + caret = { container: textNode, offset: caretOffset }; + } + } + selection.collapse(caret.container, caret.offset); + document.execCommand("delete", false, ""); + let child = editor.firstChild; + for (let expectedText of currentTest.expectedText) { + expectedText = toPlaintext(expectedText); + let caretOffset = expectedText.indexOf("[]"); + if (caretOffset >= 0) { + expectedText = expectedText.slice(0, caretOffset) + expectedText.slice(caretOffset + 2); + } + if (!child || child.nodeName !== "#text") { + assert_equals("", escape(expectedText), "Expected text node is not there"); + if (caretOffset >= 0) { + assert_equals(-1, caretOffset, "Selection should be contained in this node"); + } + } else { + assert_equals(escape(child.data), escape(expectedText), "Modified text is wrong"); + if (caretOffset >= 0) { + assert_equals(selection.focusNode, child, "Selection focus node is wrong"); + assert_equals(selection.focusOffset, caretOffset, "Selection focus offset is wrong"); + assert_equals(selection.anchorNode, child, "Selection anchor node is wrong"); + assert_equals(selection.anchorOffset, caretOffset, "Selection anchor offset is wrong"); + } + } + child = child.nextSibling; + } + if (child && child.nodeName === "#text") { + assert_equals(escape(child.data), "", "Unexpected text node is there"); + } + }, `execCommand("delete", false, ""): "${currentTest.initialText}"`); + } + + // Test white spaces around inline element boundary + // - initialHTML: Set to innerHTML of the <div> ("[{" and "]}" set selection to the range) + // - expectedText: Set to innerHTML of the <div> after `execCommand("delete")` + for (const currentTest of [ + { initialHTML: "<span>abc <span> []def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" }, + { initialHTML: "<span>abc <span>[] def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc <span> []def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" }, + { initialHTML: "<span>abc <span>[] def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc <span> []def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" }, + { initialHTML: "<span>abc <span>[] def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc <span> []def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" }, + { initialHTML: "<span>abc <span>[] def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + + { initialHTML: "<span>abc </span><span> []def</span>", expectedHTML: "<span>abc </span><span>def</span>" }, + { initialHTML: "<span>abc </span><span>[] def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> []def</span>", expectedHTML: "<span>abc </span><span>def</span>" }, + { initialHTML: "<span>abc </span><span>[] def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> []def</span>", expectedHTML: "<span>abc </span><span>def</span>" }, + { initialHTML: "<span>abc </span><span>[] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> []def</span>", expectedHTML: "<span>abc </span><span>def</span>" }, + { initialHTML: "<span>abc </span><span>[] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> []def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> []def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> [] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> [] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> []def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> [] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> [] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span>[] def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + + { initialHTML: "<span><span>abc </span> []def</span>", expectedHTML: "<span><span>abc </span>def</span>" }, + { initialHTML: "<span><span>abc </span>[] def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc </span> []def</span>", expectedHTML: "<span><span>abc </span>def</span>" }, + { initialHTML: "<span><span>abc </span>[] def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc </span> []def</span>", expectedHTML: "<span><span>abc </span>def</span>" }, + { initialHTML: "<span><span>abc </span>[] def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc </span> []def</span>", expectedHTML: "<span><span>abc </span>def</span>" }, + { initialHTML: "<span><span>abc </span>[] def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + + { initialHTML: "<span><span>abc </span></span><span> []def</span>", expectedHTML: "<span><span>abc </span></span><span>def</span>" }, + { initialHTML: "<span><span>abc </span></span><span>[] def</span>", expectedHTML: "<span><span>abc </span></span><span> def</span>" }, + { initialHTML: "<span><span>abc []</span></span><span> def</span>", expectedHTML: "<span><span>abc </span></span><span> def</span>" }, + + + { initialHTML: "a<span style=white-space:pre;>b[] </span>c", expectedHTML: "a<span style=\"white-space:pre;\"> </span>c" }, + { initialHTML: "a<span style=white-space:pre;>b [] </span>c", expectedHTML: "a<span style=\"white-space:pre;\">b </span>c" }, + { initialHTML: "a<span style=white-space:pre;>b [] </span>c", expectedHTML: "a<span style=\"white-space:pre;\">b </span>c" }, + { initialHTML: "a<span style=white-space:pre;>b []</span>c", expectedHTML: "a<span style=\"white-space:pre;\">b </span>c" }, + { initialHTML: "a<span style=white-space:pre;>b [] </span>", expectedHTML: "a<span style=\"white-space:pre;\">b </span>" }, + { initialHTML: "a<span style=white-space:pre;> </span>[]b", expectedHTML: "ab" }, + { initialHTML: "a <span style=white-space:pre;>[] </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a []<span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;>b </span>", expectedHTML: "a <span style=\"white-space:pre;\">b </span>" }, + { initialHTML: "a <span style=white-space:pre;>[] </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a []<span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;>b </span>", expectedHTML: "a <span style=\"white-space:pre;\">b </span>" }, + { initialHTML: "<span style=white-space:pre;> [] </span> a", expectedHTML: "<span style=\"white-space:pre;\"> </span> a" }, + { initialHTML: "<span style=white-space:pre;> []</span> a", expectedHTML: "<span style=\"white-space:pre;\"> </span> a" }, + { initialHTML: "<span style=white-space:pre;> </span>[] a", expectedHTML: "<span style=\"white-space:pre;\"> </span> a" }, + { initialHTML: "<span style=white-space:pre;> </span> [] a", expectedHTML: "<span style=\"white-space:pre;\"> </span> a" }, + ]) { + test(function () { + let points = setupDiv(editor, currentTest.initialHTML); + selection.setBaseAndExtent(points[0], points[1], points[2], points[3]); + document.execCommand("delete", false, ""); + assert_equals(editor.innerHTML, currentTest.expectedHTML); + }, `execCommand("delete", false, ""): "${currentTest.initialHTML}"`); + } + + done(); +} + +window.addEventListener("load", runTests, {once: true}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-forwarddelete.tentative.html b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-forwarddelete.tentative.html new file mode 100644 index 0000000000..af5c052c56 --- /dev/null +++ b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-forwarddelete.tentative.html @@ -0,0 +1,357 @@ +<!doctype html> +<html> +<head> +<meta charset=utf-8> +<title>Testing normalizing white space sequence after execCommand("forward", false, "")</title> +<script src=../include/implementation.js></script> +<script>var testsJsLibraryOnly = true</script> +<script src="../include/tests.js"></script> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +"use strict"; + +setup({explicit_done: true}); + +function runTests() { + // README: + // These tests based on the behavior of Chrome 83. This test does NOT define + // nor suggest any standard behavior (actually, some expected results might + // look odd), but this test must help you to understand how other browsers + // use different logic to normalize white-space sequence. + + document.body.innerHTML = "<div contenteditable></div>"; + let editor = document.querySelector("div[contenteditable]"); + editor.focus(); + let selection = document.getSelection(); + + function toPlaintext(str) { + return str.replace(/ /g, "\u00A0"); + } + function escape(str) { + return str.replace(/\u00A0/ig, " "); + } + + // Test simple removing in a text node. + // - initialText: Set to data of text node (only entity is handled) + // - expectedText: Set to data of the text node after `execCommand("forward")` + // - whiteSpaceRange: Set first item to start offset of whitespace sequence, + // set second item to number of white spaces. + for (const currentTest of [ + { initialText: "a ", expectedText: "a", whiteSpaceRange: [1, 1] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 2] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 2] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 3] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 3] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 3] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 4] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 4] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 4] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 4] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 5] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 10] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 10] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 10] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 11] }, + { initialText: "a ", expectedText: "a ", whiteSpaceRange: [1, 11] }, + { initialText: "a b", expectedText: "ab", whiteSpaceRange: [1, 1] }, + { initialText: "a b", expectedText: "ab", whiteSpaceRange: [1, 1] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 2] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 2] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 2] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 3] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 3] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 3] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 3] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 4] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 5] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 10] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 10] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 10] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 11] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 11] }, + { initialText: "a b", expectedText: "a b", whiteSpaceRange: [1, 11] }, + { initialText: " b", expectedText: "b", whiteSpaceRange: [0, 1] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 2] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 2] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 3] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 3] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 3] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 4] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 5] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 10] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 10] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 10] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 11] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 11] }, + { initialText: " b", expectedText: " b", whiteSpaceRange: [0, 11] }, + { initialText: " ", expectedText: "", whiteSpaceRange: [0, 1] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 2] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 3] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 3] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 4] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 4] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 4] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 5] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 10] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 10] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 10] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 11] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 11] }, + { initialText: " ", expectedText: " ", whiteSpaceRange: [0, 11] }, + ]) { + for (let i = currentTest.whiteSpaceRange[0]; i < currentTest.whiteSpaceRange[0] + currentTest.whiteSpaceRange[1]; i++) { + currentTest.getInitialText = function (aCaretPos) { + return escape(`${toPlaintext(this.initialText).slice(0, aCaretPos)}[]${toPlaintext(this.initialText).slice(aCaretPos)}`); + } + test(function () { + editor.innerHTML = ""; + editor.appendChild(document.createTextNode(toPlaintext(currentTest.initialText))); + selection.collapse(editor.firstChild, i); + document.execCommand("forwarddelete", false, ""); + if (currentTest.expectedText.length) { + assert_equals(escape(editor.childNodes.item(0).data), currentTest.expectedText, "Modified text is wrong"); + assert_equals(selection.focusNode, editor.childNodes.item(0), "Selection focus node is wrong"); + assert_equals(selection.focusOffset, i, "Selection focus offset is wrong"); + assert_equals(selection.anchorNode, editor.childNodes.item(0), "Selection anchor node is wrong"); + assert_equals(selection.anchorOffset, i, "Selection anchor offset is wrong"); + } else { + assert_equals(escape(editor.textContent), "", "Modified text is wrong"); + assert_equals(selection.focusNode, editor, "Selection focus node is wrong"); + assert_equals(selection.focusOffset, 0, "Selection focus offset is wrong"); + assert_equals(selection.anchorNode, editor, "Selection anchor node is wrong"); + assert_equals(selection.anchorOffset, 0, "Selection anchor offset is wrong"); + } + }, `execCommand("forwarddelete", false, ""): "${currentTest.getInitialText(i)}" (length of whitespace sequence: ${currentTest.whiteSpaceRange[1]})`); + } + } + + // Test white space sequence split to multiple text node. + // - initialText: Set to data of text nodes. This must have "|" at least one. + // Then, the text will be split at every "|". + // Same as above test, only is handled at setting. + // "[]" means that caret position. + // - expectedText: Set to data of all text nodes as an array. + // Same as above test, only is handled before comparing. + for (const currentTest of [ + { initialText: "a [] | b", expectedText: ["a []", " b"] }, + { initialText: "a [] | b", expectedText: ["a []", " b"] }, + { initialText: "a []| b", expectedText: ["a []", " b"] }, + { initialText: "a []| b", expectedText: ["a []", " b"] }, + { initialText: "a |[] b", expectedText: ["a []", " b"] }, + { initialText: "a |[] b", expectedText: ["a []", " b"] }, + { initialText: "a | [] b", expectedText: ["a ", " [] b"] }, + { initialText: "a |[] b", expectedText: ["a []", " b"] }, + { initialText: "a []| b", expectedText: ["a []", " b"] }, + { initialText: "a [] | b", expectedText: ["a []", " b"] }, + + { initialText: "a [] | b", expectedText: ["a [] ", " b"] }, + { initialText: "a [] | b", expectedText: ["a []", " b"] }, + { initialText: "a []| b", expectedText: ["a []", " b"] }, + { initialText: "a []b | c", expectedText: ["a [] ", " c"] }, + { initialText: "a []b | c", expectedText: ["a [] ", " c"] }, + { initialText: "a []b| c", expectedText: ["a []", " c"] }, + { initialText: "a []|b c", expectedText: ["a []", " c"] }, + { initialText: "a |[]b c", expectedText: ["a []", " c"] }, + { initialText: "a [] |b c", expectedText: ["a []", "b c"] }, + { initialText: "a | []b c", expectedText: ["a ", " [] c"] }, + + { initialText: "a | |[] c", expectedText: ["a ", " []", " c"] }, + { initialText: "a | |[] c", expectedText: ["a ", " []", " c"] }, + { initialText: "a | []| c", expectedText: ["a ", " []", " c"] }, + { initialText: "a |[] | c", expectedText: ["a []", " c"] }, + { initialText: "a []| | c", expectedText: ["a []", " c"] }, + { initialText: "a [] | | c", expectedText: ["a []", " ", " c"] }, + { initialText: "a [] | | c", expectedText: ["a [] ", " ", " c"] }, + { initialText: "a [] | | c", expectedText: ["a [] ", " ", " c"] }, + { initialText: "a || [] c", expectedText: ["a ", "", " [] c"] }, + { initialText: "a ||[] c", expectedText: ["a []", " c"] }, + { initialText: "a |[]| c", expectedText: ["a []", " c"] }, + { initialText: "a []|| c", expectedText: ["a []", " c"] }, + { initialText: "a [] || c", expectedText: ["a []", " c"] }, + { initialText: "a [] || c", expectedText: ["a [] ", "", " c"] }, + ]) { + test(function () { + editor.innerHTML = ""; + let caret = { container: null, offset: -1 }; + for (let text of toPlaintext(currentTest.initialText).split("|")) { + let caretOffset = text.indexOf("[]"); + if (caretOffset >= 0) { + text = text.slice(0, caretOffset) + text.slice(caretOffset + 2); + } + let textNode = document.createTextNode(text); + editor.appendChild(textNode); + if (caretOffset >= 0) { + caret = { container: textNode, offset: caretOffset }; + } + } + selection.collapse(caret.container, caret.offset); + document.execCommand("forwarddelete", false, ""); + let child = editor.firstChild; + for (let expectedText of currentTest.expectedText) { + expectedText = toPlaintext(expectedText); + let caretOffset = expectedText.indexOf("[]"); + if (caretOffset >= 0) { + expectedText = expectedText.slice(0, caretOffset) + expectedText.slice(caretOffset + 2); + } + if (!child || child.nodeName !== "#text") { + assert_equals("", escape(expectedText), "Expected text node is not there"); + if (caretOffset >= 0) { + assert_equals(-1, caretOffset, "Selection should be contained in this node"); + } + } else { + assert_equals(escape(child.data), escape(expectedText), "Modified text is wrong"); + if (caretOffset >= 0) { + assert_equals(selection.focusNode, child, "Selection focus node is wrong"); + assert_equals(selection.focusOffset, caretOffset, "Selection focus offset is wrong"); + assert_equals(selection.anchorNode, child, "Selection anchor node is wrong"); + assert_equals(selection.anchorOffset, caretOffset, "Selection anchor offset is wrong"); + } + } + child = child.nextSibling; + } + if (child && child.nodeName === "#text") { + assert_equals(escape(child.data), "", "Unexpected text node is there"); + } + }, `execCommand("forwarddelete", false, ""): "${currentTest.initialText}"`); + } + + // Test white spaces around inline element boundary + // - initialHTML: Set to innerHTML of the <div> ("[{" and "]}" set selection to the range) + // - expectedText: Set to innerHTML of the <div> after `execCommand("delete")` + for (const currentTest of [ + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc<span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc[] <span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc <span>[] def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc <span>[] def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc []<span> def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc <span>[] def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc <span> [] def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc <span> [] def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + { initialHTML: "<span>abc <span> [] def</span></span>", expectedHTML: "<span>abc <span> def</span></span>" }, + + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc </span>def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc </span>def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc </span>def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc</span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc[] </span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc </span>[] def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc </span>[] def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc []</span> def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc </span>[] def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc </span> [] def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc </span> [] def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + { initialHTML: "<span><span>abc </span> [] def</span>", expectedHTML: "<span><span>abc </span> def</span>" }, + + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span>def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span>def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span>def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc</span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc[] </span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span>[] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span>[] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc []</span><span> def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span>[] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> [] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> [] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + { initialHTML: "<span>abc </span><span> [] def</span>", expectedHTML: "<span>abc </span><span> def</span>" }, + + { initialHTML: "a[]<span style=white-space:pre;>b </span>c", expectedHTML: "a<span style=\"white-space:pre;\"> </span>c" }, + { initialHTML: "a<span style=white-space:pre;>b[] </span>c", expectedHTML: "a<span style=\"white-space:pre;\">b </span>c" }, + { initialHTML: "a<span style=white-space:pre;>b [] </span>c", expectedHTML: "a<span style=\"white-space:pre;\">b </span>c" }, + { initialHTML: "a<span style=white-space:pre;>b [] </span>c", expectedHTML: "a<span style=\"white-space:pre;\">b </span>c" }, + { initialHTML: "a<span style=white-space:pre;>b []</span>c", expectedHTML: "a<span style=\"white-space:pre;\">b </span>" }, + { initialHTML: "a<span style=white-space:pre;>b [] </span>", expectedHTML: "a<span style=\"white-space:pre;\">b </span>" }, + { initialHTML: "a[]<span style=white-space:pre;> </span>b", expectedHTML: "ab" }, + { initialHTML: "a []<span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;>b </span>", expectedHTML: "a <span style=\"white-space:pre;\">b </span>" }, + { initialHTML: "a [] <span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;> </span>", expectedHTML: "a <span style=\"white-space:pre;\"> </span>" }, + { initialHTML: "a [] <span style=white-space:pre;>b </span>", expectedHTML: "a <span style=\"white-space:pre;\">b </span>" }, + { initialHTML: "<span style=white-space:pre;> [] </span> a", expectedHTML: "<span style=\"white-space:pre;\"> </span> a" }, + { initialHTML: "<span style=white-space:pre;> [] </span> a", expectedHTML: "<span style=\"white-space:pre;\"> </span> a" }, + { initialHTML: "<span style=white-space:pre;> []</span> a", expectedHTML: "<span style=\"white-space:pre;\"> </span> a" }, + { initialHTML: "<span style=white-space:pre;> </span>[] a", expectedHTML: "<span style=\"white-space:pre;\"> </span> a" }, + ]) { + test(function () { + let points = setupDiv(editor, currentTest.initialHTML); + selection.setBaseAndExtent(points[0], points[1], points[2], points[3]); + document.execCommand("forwarddelete", false, ""); + assert_equals(editor.innerHTML, currentTest.expectedHTML); + }, `execCommand("forwarddelete", false, ""): "${currentTest.initialHTML}"`); + } + + done(); +} + +window.addEventListener("load", runTests, {once: true}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-insertlinebreak.tentative.html b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-insertlinebreak.tentative.html new file mode 100644 index 0000000000..a961ee77bc --- /dev/null +++ b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-insertlinebreak.tentative.html @@ -0,0 +1,150 @@ +<!doctype html> +<html> +<head> +<meta charset=utf-8> +<title>Testing normalizing white-space sequence after execCommand("insertlinebreak", false, "foo")</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +"use strict"; + +setup({explicit_done: true}); + +function runTests() { + // README: + // These tests based on the behavior of Chrome 83. This test does NOT define + // nor suggest any standard behavior (actually, some expected results might + // look odd), but this test must help you to understand how other browsers + // use different logic to normalize white-space sequence. + + document.body.innerHTML = "<div contenteditable></div>"; + let editor = document.querySelector("div[contenteditable]"); + editor.focus(); + let selection = document.getSelection(); + + function escape(str) { + return typeof(str) === "string" ? str.replace(/\u00A0/ig, " ") : ""; + } + + function generateWhiteSpaces(num, lastIsAlwaysNBSP) { + let str = ""; + for (let i = 0; i < num - 1; i++) { + str += i % 2 ? " " : "\u00A0"; + } + str += lastIsAlwaysNBSP || num % 2 ? "\u00A0" : " "; + return escape(str); + } + function getDescriptionForTextNode(textNode) { + return selection.focusNode === textNode ? + `${escape(textNode.data.slice(0, selection.focusOffset))}[]${escape(textNode.data.slice(selection.focusOffset))}` : + escape(textNode); + } + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 0); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `<br>a b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 1); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a<br>${escape(generateWhiteSpaces(9, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 2); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>${escape(generateWhiteSpaces(8, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 3); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>${escape(generateWhiteSpaces(7, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 4); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>${escape(generateWhiteSpaces(6, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 5); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>${escape(generateWhiteSpaces(5, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 6); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>${escape(generateWhiteSpaces(4, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 7); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>${escape(generateWhiteSpaces(3, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 8); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>${escape(generateWhiteSpaces(2, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 9); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>${escape(generateWhiteSpaces(1, false))}b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 10); + test(function () { + document.execCommand("insertlinebreak", false, ""); + assert_equals(editor.innerHTML, + `a <br>b`, + "Modified text is wrong"); + }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + done(); +} + +window.addEventListener("load", runTests, {once: true}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-insertparagraph.tentative.html b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-insertparagraph.tentative.html new file mode 100644 index 0000000000..854e6b3dae --- /dev/null +++ b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-insertparagraph.tentative.html @@ -0,0 +1,72 @@ +<!doctype html> +<html> +<head> +<meta charset=utf-8> +<title>Testing normalizing white-space sequence after execCommand("insertparagraph", false, "foo")</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +"use strict"; + +setup({explicit_done: true}); + +function runTests() { + // README: + // These tests based on the behavior of Chrome 83. This test does NOT define + // nor suggest any standard behavior (actually, some expected results might + // look odd), but this test must help you to understand how other browsers + // use different logic to normalize white-space sequence. + + document.body.innerHTML = "<div contenteditable></div>"; + let editor = document.querySelector("div[contenteditable]"); + editor.focus(); + let selection = document.getSelection(); + + function escape(str) { + return typeof(str) === "string" ? str.replace(/\u00A0/ig, " ") : ""; + } + + function generateWhiteSpaces(num, lastIsAlwaysNBSP) { + let str = ""; + for (let i = 0; i < num - 1; i++) { + str += i % 2 ? " " : "\u00A0"; + } + str += lastIsAlwaysNBSP || num % 2 ? "\u00A0" : " "; + return escape(str); + } + function getDescriptionForTextNode(textNode) { + return selection.focusNode === textNode ? + `${escape(textNode.data.slice(0, selection.focusOffset))}[]${escape(textNode.data.slice(selection.focusOffset))}` : + escape(textNode); + } + + editor.innerHTML = "<div>a b</div>"; + selection.collapse(editor.firstChild.firstChild, 0); + test(function () { + document.execCommand("insertparagraph", false, ""); + assert_equals(editor.innerHTML, + `<div><br></div><div>a b</div>`, + "Modified text is wrong"); + }, `execCommand("insertparagraph", false, "") at "<div>${getDescriptionForTextNode(editor.firstChild.firstChild)}</div>"`); + + for (let i = 1; i <= 10; i++) { + editor.innerHTML = "<div>a b</div>"; + selection.collapse(editor.firstChild.firstChild, i); + test(function () { + let text = editor.firstChild.firstChild.data; + document.execCommand("insertparagraph", false, ""); + assert_equals(editor.innerHTML, + `<div>${escape(text.slice(0, i))}</div><div>${escape(text.slice(i))}</div>`, + "Modified text is wrong"); + }, `execCommand("insertparagraph", false, "") at "<div>${getDescriptionForTextNode(editor.firstChild.firstChild)}</div>"`); + } + + done(); +} + +window.addEventListener("load", runTests, {once: true}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-inserttext.tentative.html b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-inserttext.tentative.html new file mode 100644 index 0000000000..4b4146b509 --- /dev/null +++ b/testing/web-platform/tests/editing/other/white-spaces-after-execCommand-inserttext.tentative.html @@ -0,0 +1,526 @@ +<!doctype html> +<html> +<head> +<meta charset=utf-8> +<title>Testing normalizing white-space sequence after execCommand("inserttext", false, "foo")</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +"use strict"; + +setup({explicit_done: true}); + +function runTests() { + // README: + // These tests based on the behavior of Chrome 83. This test does NOT define + // nor suggest any standard behavior (actually, some expected results might + // look odd), but this test must help you to understand how other browsers + // use different logic to normalize white-space sequence. + + document.body.innerHTML = "<div contenteditable></div>"; + let editor = document.querySelector("div[contenteditable]"); + editor.focus(); + let selection = document.getSelection(); + + function toPlaintext(str) { + return str.replace(/ /g, "\u00A0"); + } + function escape(str) { + return typeof(str) === "string" ? str.replace(/\u00A0/ig, " ") : ""; + } + + function generateWhiteSpaces(num, lastIsAlwaysNBSP) { + if (!num) { + return ""; + } + let str = ""; + for (let i = 0; i < num - 1; i++) { + str += i % 2 ? " " : "\u00A0"; + } + str += lastIsAlwaysNBSP || num % 2 ? "\u00A0" : " "; + return escape(str); + } + function getDescriptionForTextNode(textNode) { + return selection.focusNode === textNode ? + `${escape(textNode.data.slice(0, selection.focusOffset))}[]${escape(textNode.data.slice(selection.focusOffset))}` : + escape(textNode); + } + + for (let i = 0; i < 12; i++) { + editor.innerHTML = `a${i === 1 ? " " : generateWhiteSpaces(i, false)}b`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(escape(editor.firstChild.data), + `a${i === 0 ? " " : escape(generateWhiteSpaces(i + 1, false))}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 0; i < 12; i++) { + editor.innerHTML = `a${generateWhiteSpaces(i, true)}`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(escape(editor.firstChild.data), + `a${escape(generateWhiteSpaces(i + 1, true))}`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 0; i < 12; i++) { + editor.innerHTML = `${generateWhiteSpaces(i, false)}b`; + selection.collapse(editor.firstChild, i); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(escape(editor.firstChild.data), + `${i === 0 ? " " : escape(generateWhiteSpaces(i + 1, false))}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 0; i < 12; i++) { + editor.innerHTML = `a${i === 0 ? " " : generateWhiteSpaces(i + 1, false)}b`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(escape(editor.firstChild.data), + `a${i === 0 ? " " : escape(generateWhiteSpaces(i + 2, false))}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(escape(editor.firstChild.data), "a b", "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + for (let i = 1; i <= 3; i++) { + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, i); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(escape(editor.firstChild.data), + `a${escape(generateWhiteSpaces(3, false))}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 1; i <= 6; i++) { + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, i); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(escape(editor.firstChild.data), + `a${escape(generateWhiteSpaces(6, false))}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 1; i <= 7; i++) { + editor.innerHTML = "a b"; + selection.collapse(editor.firstChild, i); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(escape(editor.firstChild.data), + `a${escape(generateWhiteSpaces(7, false))}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 0; i < 12; i++) { + editor.innerHTML = `a${generateWhiteSpaces(i)}b`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, "\u00A0"); + assert_equals(escape(editor.firstChild.data), + `a${i === 0 ? " " : escape(generateWhiteSpaces(i + 1, false))}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "\\u00A0") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>${i === 0 ? " " : generateWhiteSpaces(i + 1)}</span>b`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>${escape(generateWhiteSpaces(i + 2, true))}</span>b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span>b"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span>c`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span>c`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span>c"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span> c`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span> c`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span> c"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span> c`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span> c`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span> c"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span><span>c</span>`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span><span>c</span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span><span>c</span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span><span> c</span>`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span><span> c</span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span><span> c</span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span><span> c</span>`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span><span> c</span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span><span> c</span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span><span><span>c</span></span>`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span><span><span>c</span></span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span><span><span>c</span></span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span><span><span> c</span></span>`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span><span><span> c</span></span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span><span><span> c</span></span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span>b${generateWhiteSpaces(i, true)}</span><span><span> c</span></span>`; + selection.collapse(editor.querySelector("span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span>b${escape(generateWhiteSpaces(i + 1, true))}</span><span><span> c</span></span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span><span><span> c</span></span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span><span>b${generateWhiteSpaces(i, true)}</span></span><span>c</span>`; + selection.collapse(editor.querySelector("span span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span><span>b${escape(generateWhiteSpaces(i + 1, true))}</span></span><span>c</span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span><span>${getDescriptionForTextNode(editor.querySelector("span span").firstChild)}</span></span><span>c</span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span><span>b${generateWhiteSpaces(i, true)}</span></span><span> c</span>`; + selection.collapse(editor.querySelector("span span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span><span>b${escape(generateWhiteSpaces(i + 1, true))}</span></span><span> c</span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span><span>${getDescriptionForTextNode(editor.querySelector("span span").firstChild)}</span></span><span> c</span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a<span><span>b${generateWhiteSpaces(i, true)}</span></span><span> c</span>`; + selection.collapse(editor.querySelector("span span").firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a<span><span>b${escape(generateWhiteSpaces(i + 1, true))}</span></span><span> c</span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "a<span><span>${getDescriptionForTextNode(editor.querySelector("span span").firstChild)}</span></span><span> c</span>"`); + } + + for (let i = 2; i < 8; i++) { + editor.innerHTML = "ab"; + selection.collapse(editor.firstChild, 1); + test(function () { + document.execCommand("inserttext", false, " ".repeat(i)); + assert_equals(escape(editor.firstChild.data), + `a${i > 0 ? escape(generateWhiteSpaces(i, false)) : " "}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "${" ".repeat(i)}") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 2; i < 8; i++) { + editor.innerHTML = "a"; + selection.collapse(editor.firstChild, 1); + test(function () { + document.execCommand("inserttext", false, " ".repeat(i)); + assert_equals(escape(editor.firstChild.data), + `a${i > 0 ? escape(generateWhiteSpaces(i, true)) : " "}`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "${" ".repeat(i)}") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 2; i < 8; i++) { + editor.innerHTML = "ab"; + selection.collapse(editor.firstChild, 1); + test(function () { + document.execCommand("inserttext", false, "\u00A0".repeat(i)); + assert_equals(escape(editor.firstChild.data), + `a${i > 0 ? escape(generateWhiteSpaces(i, false)) : " "}b`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "${"\\u00A0".repeat(i)}") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 2; i < 8; i++) { + editor.innerHTML = "a"; + selection.collapse(editor.firstChild, 1); + test(function () { + document.execCommand("inserttext", false, "\u00A0".repeat(i)); + assert_equals(escape(editor.firstChild.data), + `a${i > 0 ? escape(generateWhiteSpaces(i, true)) : " "}`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "${"\\u00A0".repeat(i)}") at "${getDescriptionForTextNode(editor.firstChild)}"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a${generateWhiteSpaces(i, true)}<span style=white-space:pre>b</span>`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a${escape(generateWhiteSpaces(i + 1, true))}<span style=\"white-space:pre\">b</span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}<span style=white-space:pre>b</span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a${generateWhiteSpaces(i, true)}<span style=white-space:pre> </span>`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a${escape(generateWhiteSpaces(i + 1, true))}<span style=\"white-space:pre\"> </span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}<span style=white-space:pre> </span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a${generateWhiteSpaces(i, true)}<span style=white-space:pre> </span>`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, " "); + assert_equals(editor.innerHTML, + `a${escape(generateWhiteSpaces(i + 1, true))}<span style=\"white-space:pre\"> </span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}<span style=white-space:pre> </span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a${generateWhiteSpaces(i, true)}<span style=white-space:pre>b</span>`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, "\u00A0"); + assert_equals(editor.innerHTML, + `a${escape(generateWhiteSpaces(i + 1, true))}<span style=\"white-space:pre\">b</span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "\\u00A0") at "${getDescriptionForTextNode(editor.firstChild)}<span style=white-space:pre>b</span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a${generateWhiteSpaces(i, true)}<span style=white-space:pre> </span>`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, "\u00A0"); + assert_equals(editor.innerHTML, + `a${escape(generateWhiteSpaces(i + 1, true))}<span style=\"white-space:pre\"> </span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "\\u00A0") at "${getDescriptionForTextNode(editor.firstChild)}<span style=white-space:pre> </span>"`); + } + + for (let i = 0; i < 5; i++) { + editor.innerHTML = `a${generateWhiteSpaces(i, true)}<span style=white-space:pre> </span>`; + selection.collapse(editor.firstChild, i + 1); + test(function () { + document.execCommand("inserttext", false, "\u00A0"); + assert_equals(editor.innerHTML, + `a${escape(generateWhiteSpaces(i + 1, true))}<span style=\"white-space:pre\"> </span>`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "\\u00A0") at "${getDescriptionForTextNode(editor.firstChild)}<span style=white-space:pre> </span>"`); + } + + editor.innerHTML = "a c"; + selection.collapse(editor.firstChild, 2); + test(function () { + document.execCommand("inserttext", false, "b"); + assert_equals(escape(editor.firstChild.data), "a b c", "Modified text is wrong"); + }, `execCommand("inserttext", false, "b") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a c"; + selection.collapse(editor.firstChild, 1); + test(function () { + document.execCommand("inserttext", false, "b"); + assert_equals(escape(editor.firstChild.data), `ab${escape(generateWhiteSpaces(4))}c`, "Modified text is wrong"); + }, `execCommand("inserttext", false, "b") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a c"; + selection.collapse(editor.firstChild, 2); + test(function () { + document.execCommand("inserttext", false, "b"); + assert_equals(escape(editor.firstChild.data), `a b${escape(generateWhiteSpaces(3))}c`, "Modified text is wrong"); + }, `execCommand("inserttext", false, "b") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a c"; + selection.collapse(editor.firstChild, 3); + test(function () { + document.execCommand("inserttext", false, "b"); + assert_equals(escape(editor.firstChild.data), + `a${escape(generateWhiteSpaces(2))}b${escape(generateWhiteSpaces(2))}c`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "b") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a c"; + selection.collapse(editor.firstChild, 4); + test(function () { + document.execCommand("inserttext", false, "b"); + assert_equals(escape(editor.firstChild.data), + `a${escape(generateWhiteSpaces(3))}b c`, + "Modified text is wrong"); + }, `execCommand("inserttext", false, "b") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + editor.innerHTML = "a c"; + selection.collapse(editor.firstChild, 5); + test(function () { + document.execCommand("inserttext", false, "b"); + assert_equals(escape(editor.firstChild.data), `a${escape(generateWhiteSpaces(4))}bc`, "Modified text is wrong"); + }, `execCommand("inserttext", false, "b") at "${getDescriptionForTextNode(editor.firstChild)}"`); + + // Test white space sequence split to multiple text node. + // - initialText: Set to data of text nodes. This must have "|" at least one. + // Then, the text will be split at every "|". + // Same as above test, only is handled at setting. + // "[]" means that caret position. + // - expectedText: Set to data of all text nodes as an array. + // Same as above test, only is handled before comparing. + for (const currentTest of [ + { initialText: "a[]|b", expectedText: ["a", "b"] }, + { initialText: "a []|b", expectedText: ["a ", "b"] }, + { initialText: "a |[]b", expectedText: ["a ", "b"] }, + { initialText: "a[] |b", expectedText: ["a ", "b"] }, + { initialText: "a []|b", expectedText: ["a ", "b"] }, + { initialText: "a |[]b", expectedText: ["a ", "b"] }, + { initialText: "a[]| b", expectedText: ["a", " b"] }, + { initialText: "a|[] b", expectedText: ["a", " b"] }, + { initialText: "a| []b", expectedText: ["a", " b"] }, + { initialText: "a[] | b", expectedText: ["a ", " b"] }, + { initialText: "a []| b", expectedText: ["a ", " b"] }, + { initialText: "a |[] b", expectedText: ["a ", " b"] }, + { initialText: "a | []b", expectedText: ["a ", " b"] }, + { initialText: "a[] | b", expectedText: ["a ", " b"] }, + { initialText: "a []| b", expectedText: ["a ", " b"] }, + { initialText: "a |[] b", expectedText: ["a ", " b"] }, + { initialText: "a | []b", expectedText: ["a ", " b"] }, + { initialText: "a[] | b", expectedText: ["a ", " b"] }, + { initialText: "a []| b", expectedText: ["a ", " b"] }, + { initialText: "a |[] b", expectedText: ["a ", " b"] }, + { initialText: "a | []b", expectedText: ["a ", " b"] }, + ]) { + test(function () { + editor.innerHTML = ""; + let caret = { container: null, offset: -1 }; + for (let text of toPlaintext(currentTest.initialText).split("|")) { + let caretOffset = text.indexOf("[]"); + if (caretOffset >= 0) { + text = text.slice(0, caretOffset) + text.slice(caretOffset + 2); + } + let textNode = document.createTextNode(text); + editor.appendChild(textNode); + if (caretOffset >= 0) { + caret = { container: textNode, offset: caretOffset }; + } + } + selection.collapse(caret.container, caret.offset); + document.execCommand("inserttext", false, ""); + let child = editor.firstChild; + for (let expectedText of currentTest.expectedText) { + expectedText = toPlaintext(expectedText); + let caretOffset = expectedText.indexOf("[]"); + if (caretOffset >= 0) { + expectedText = expectedText.slice(0, caretOffset) + expectedText.slice(caretOffset + 2); + } + if (!child || child.nodeName !== "#text") { + assert_equals("", escape(expectedText), "Expected text node is not there"); + if (caretOffset >= 0) { + assert_equals(-1, caretOffset, "Selection should be contained in this node"); + } + } else { + assert_equals(escape(child.data), escape(expectedText), "Modified text is wrong"); + if (caretOffset >= 0) { + assert_equals(selection.focusNode, child, "Selection focus node is wrong"); + assert_equals(selection.focusOffset, caretOffset, "Selection focus offset is wrong"); + assert_equals(selection.anchorNode, child, "Selection anchor node is wrong"); + assert_equals(selection.anchorOffset, caretOffset, "Selection anchor offset is wrong"); + } + } + child = child.nextSibling; + } + if (child && child.nodeName === "#text") { + assert_equals(escape(child.data), "", "Unexpected text node is there"); + } + }, `execCommand("inserttext", false, ""): "${currentTest.initialText}"`); + } + + done(); +} + +window.addEventListener("load", runTests, {once: true}); +</script> +</body> +</html> |