summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/editing/other
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/editing/other')
-rw-r--r--testing/web-platform/tests/editing/other/body-should-not-deleted-even-if-empty.html51
-rw-r--r--testing/web-platform/tests/editing/other/cefalse-boundaries-deletion.html60
-rw-r--r--testing/web-platform/tests/editing/other/cloning-attributes-at-splitting-element.tentative.html522
-rw-r--r--testing/web-platform/tests/editing/other/delete-in-child-of-head.tentative.html418
-rw-r--r--testing/web-platform/tests/editing/other/delete-in-child-of-html.tentative.html449
-rw-r--r--testing/web-platform/tests/editing/other/delete.html149
-rw-r--r--testing/web-platform/tests/editing/other/design-mode-textarea-crash.html14
-rw-r--r--testing/web-platform/tests/editing/other/edit-in-textcontrol-immediately-after-hidden.tentative.html138
-rw-r--r--testing/web-platform/tests/editing/other/editable-state-and-focus-in-shadow-dom-in-designMode.tentative.html252
-rw-r--r--testing/web-platform/tests/editing/other/editing-around-select-element.tentative.html310
-rw-r--r--testing/web-platform/tests/editing/other/editing-div-outside-body.html163
-rw-r--r--testing/web-platform/tests/editing/other/editing-style-of-range-around-void-element-child.tentative.html157
-rw-r--r--testing/web-platform/tests/editing/other/empty-elements-insertion.html118
-rw-r--r--testing/web-platform/tests/editing/other/exec-command-never-throw-exceptions.tentative.html89
-rw-r--r--testing/web-platform/tests/editing/other/exec-command-with-text-editor.tentative.html636
-rw-r--r--testing/web-platform/tests/editing/other/exec-command-without-editable-element.tentative.html526
-rw-r--r--testing/web-platform/tests/editing/other/extra-text-nodes.html43
-rw-r--r--testing/web-platform/tests/editing/other/formatblock-preserving-selection.tentative.html136
-rw-r--r--testing/web-platform/tests/editing/other/indent-preserving-selection.tentative.html103
-rw-r--r--testing/web-platform/tests/editing/other/insert-list-preserving-selection.tentative.html155
-rw-r--r--testing/web-platform/tests/editing/other/insert-paragraph-in-void-element.tentative.html206
-rw-r--r--testing/web-platform/tests/editing/other/insert-text-in-void-element.tentative.html322
-rw-r--r--testing/web-platform/tests/editing/other/insertlinebreak-with-white-space-style.tentative.html426
-rw-r--r--testing/web-platform/tests/editing/other/insertparagraph-in-child-of-head.tentative.html367
-rw-r--r--testing/web-platform/tests/editing/other/insertparagraph-in-child-of-html.tentative.html344
-rw-r--r--testing/web-platform/tests/editing/other/insertparagraph-in-inline-editing-host.tentative.html416
-rw-r--r--testing/web-platform/tests/editing/other/insertparagraph-in-non-splittable-element.html145
-rw-r--r--testing/web-platform/tests/editing/other/insertparagraph-with-white-space-style.tentative.html429
-rw-r--r--testing/web-platform/tests/editing/other/join-different-white-space-style-left-line-and-right-paragraph.html899
-rw-r--r--testing/web-platform/tests/editing/other/join-different-white-space-style-left-paragraph-and-right-line.html493
-rw-r--r--testing/web-platform/tests/editing/other/join-different-white-space-style-paragraphs.html499
-rw-r--r--testing/web-platform/tests/editing/other/join-pre-and-other-block.html329
-rw-r--r--testing/web-platform/tests/editing/other/justify-preserving-selection.tentative.html148
-rw-r--r--testing/web-platform/tests/editing/other/keeping-attributes-at-joining-elements.tentative.html1167
-rw-r--r--testing/web-platform/tests/editing/other/legacy-edit-command.html117
-rw-r--r--testing/web-platform/tests/editing/other/link-boundaries-insertion.html44
-rw-r--r--testing/web-platform/tests/editing/other/move-inserted-node-from-DOMNodeInserted-during-exec-command-insertHTML.html27
-rw-r--r--testing/web-platform/tests/editing/other/non-html-document.html24
-rw-r--r--testing/web-platform/tests/editing/other/outdent-preserving-selection.tentative.html192
-rw-r--r--testing/web-platform/tests/editing/other/recursive-exec-command-calls.tentative.html37
-rw-r--r--testing/web-platform/tests/editing/other/removing-inline-style-specified-by-parent-block.tentative.html125
-rw-r--r--testing/web-platform/tests/editing/other/restoration.html90
-rw-r--r--testing/web-platform/tests/editing/other/select-all-and-delete-in-html-element-having-contenteditable.html151
-rw-r--r--testing/web-platform/tests/editing/other/selectall-in-editinghost.html125
-rw-r--r--testing/web-platform/tests/editing/other/selectall-without-focus.html73
-rw-r--r--testing/web-platform/tests/editing/other/setting-value-of-textcontrol-immediately-after-hidden.html118
-rw-r--r--testing/web-platform/tests/editing/other/typing-around-link-element-at-collapsed-selection.tentative.html635
-rw-r--r--testing/web-platform/tests/editing/other/typing-around-link-element-at-non-collapsed-selection.tentative.html214
-rw-r--r--testing/web-platform/tests/editing/other/undo-insertparagraph-after-moving-split-nodes.html109
-rw-r--r--testing/web-platform/tests/editing/other/white-spaces-after-execCommand-delete.tentative.html342
-rw-r--r--testing/web-platform/tests/editing/other/white-spaces-after-execCommand-forwarddelete.tentative.html357
-rw-r--r--testing/web-platform/tests/editing/other/white-spaces-after-execCommand-insertlinebreak.tentative.html150
-rw-r--r--testing/web-platform/tests/editing/other/white-spaces-after-execCommand-insertparagraph.tentative.html72
-rw-r--r--testing/web-platform/tests/editing/other/white-spaces-after-execCommand-inserttext.tentative.html526
54 files changed, 14207 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">&nbsp;</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">&nbsp;</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">&nbsp;</div>
+ <p id="paragraph-boundaries">Lorem ipsum dolor sit amet.</p>
+ <div contenteditable="false" id="cefalse-boundaries-end">&nbsp;</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.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-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..f2c1fce522
--- /dev/null
+++ b/testing/web-platform/tests/editing/other/exec-command-with-text-editor.tentative.html
@@ -0,0 +1,636 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test that execCommand with &lt;input&gt; or &lt;textarea&gt;</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id="container"></div>
+<script>
+"use strict";
+
+setup({explicit_done: true});
+
+/**
+ * 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");
+ container.innerHTML = "Here <b>is</b> Text: <input id=\"target\">";
+ runTest(document.getElementById("target"), "In <input>");
+ 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: <input id=\"target\">";
+ runTest(document.getElementById("target"), "In <input> in contenteditable");
+ container.innerHTML = "Here <b>is</b> Text: <textarea id=\"target\"></textarea>";
+ runTest(document.getElementById("target"), "In <textarea> in contenteditable");
+
+ done();
+}
+
+function runTest(aTarget, aDescription) {
+ const kIsTextArea = aTarget.tagName === "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: "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/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>&nbsp;def<${tag}></div>`,
+ `<div>abc<span></span>&nbsp;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/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-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..0df107c080
--- /dev/null
+++ b/testing/web-platform/tests/editing/other/insertparagraph-in-inline-editing-host.tentative.html
@@ -0,0 +1,416 @@
+<!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 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 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..21aa495bd6
--- /dev/null
+++ b/testing/web-platform/tests/editing/other/insertparagraph-in-non-splittable-element.html
@@ -0,0 +1,145 @@
+<!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><div>abc</div></caption><tbody><tr><td>abc</td></tr></tbody></table></div>",
+ "<div><table><caption><div><br></div><div>abc</div></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..496c073b5f
--- /dev/null
+++ b/testing/web-platform/tests/editing/other/insertparagraph-with-white-space-style.tentative.html
@@ -0,0 +1,429 @@
+<!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>
+/**
+ * The expected behavior is based on Chrome 91.
+ */
+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 doIt() {
+ 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"]) {
+ // insertparagraph at direct child of editing host.
+ // XXX Oddly, if the outside display value is "block", `white-space`
+ // does not affect to the insertparagraph command.
+ test(() => {
+ editingHost.style.whiteSpace = style;
+ editingHost.style.display = display;
+ const utils = new EditorTestUtils(editingHost);
+ utils.setupEditingHost(`abc[]`);
+ editingHost.getBoundingClientRect();
+ doIt();
+ 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"
+ );
+ } 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})`);
+
+ test(() => {
+ editingHost.style.whiteSpace = style;
+ editingHost.style.display = display;
+ const utils = new EditorTestUtils(editingHost);
+ utils.setupEditingHost(`[]abc`);
+ editingHost.getBoundingClientRect();
+ doIt();
+ 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})`);
+
+ test(() => {
+ editingHost.style.whiteSpace = style;
+ editingHost.style.display = display;
+ const utils = new EditorTestUtils(editingHost);
+ utils.setupEditingHost(`a[]bc`);
+ editingHost.getBoundingClientRect();
+ doIt();
+ 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})`);
+
+ // 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");
+ doIt();
+ 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)`);
+
+ test(() => {
+ editingHost.style.whiteSpace = style;
+ editingHost.style.display = display;
+ const utils = new EditorTestUtils(editingHost);
+ utils.setupEditingHost(`<b>abc[]</b>`);
+ editingHost.getBoundingClientRect();
+ doIt();
+ 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"]) {
+ // insertparagraph in existing paragraph whose `white-space` is specified.
+ // XXX Oddly, the white-space style of the ancestor block/inline is
+ // ignored.
+ 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();
+ doIt();
+ 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})`);
+
+ 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();
+ doIt();
+ 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})`);
+
+ 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();
+ doIt();
+ 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})`);
+
+ // insertparagraph in existing paragraph.
+ // XXX Oddly, the white-space style of the ancestor block/inline is
+ // ignored.
+ test(() => {
+ editingHost.style.whiteSpace = style;
+ editingHost.style.display = display;
+ const utils = new EditorTestUtils(editingHost);
+ utils.setupEditingHost(`<${paragraph}>abc[]</${paragraph}>`);
+ editingHost.getBoundingClientRect();
+ doIt();
+ 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})`);
+
+ test(() => {
+ editingHost.style.whiteSpace = style;
+ editingHost.style.display = display;
+ const utils = new EditorTestUtils(editingHost);
+ utils.setupEditingHost(`<${paragraph}>[]abc</${paragraph}>`);
+ editingHost.getBoundingClientRect();
+ doIt();
+ 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})`);
+
+ test(() => {
+ editingHost.style.whiteSpace = style;
+ editingHost.style.display = display;
+ const utils = new EditorTestUtils(editingHost);
+ utils.setupEditingHost(`<${paragraph}>a[]bc</${paragraph}>`);
+ editingHost.getBoundingClientRect();
+ doIt();
+ 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})`);
+
+ // Styling the existing paragraph instead of editing host.
+ // XXX Oddly, new or right paragraph is wrapped by default paragraph
+ // separator, and even if the default paragraph separator is "p",
+ // the `<p>` element wraps the new block element!
+ 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();
+ doIt();
+ if (display != "block") {
+ assert_equals(
+ editingHost.innerHTML,
+ `<${paragraph} ${styleAttr}>abc</${paragraph}><${defaultParagraphSeparator}><${paragraph} ${styleAttr}><br></${paragraph}></${defaultParagraphSeparator}>`,
+ "New paragraph should be inserted at end of the paragraph which is wrapped by a default paragraph"
+ );
+ } else {
+ assert_equals(
+ editingHost.innerHTML,
+ `<${paragraph} ${styleAttr}>abc</${paragraph}><${paragraph} ${styleAttr}><br></${paragraph}>`,
+ "New paragraph 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();
+ doIt();
+ if (display != "block") {
+ assert_in_array(
+ editingHost.innerHTML,
+ [
+ `<${defaultParagraphSeparator}><${paragraph} ${styleAttr}><br></${paragraph}></${defaultParagraphSeparator}><${paragraph} ${styleAttr}>abc</${paragraph}>`,
+ `<${defaultParagraphSeparator}><${paragraph} ${styleAttr}><br></${paragraph}></${defaultParagraphSeparator}><${paragraph} ${styleAttr}>abc<br></${paragraph}>`,
+ ],
+ "New paragraph should be inserted at start of the paragraph which is wrapped by a default paragraph"
+ );
+ } else {
+ 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"
+ );
+ }
+ }, `<div contenteditable><${paragraph} style="display:${display}; white-space:${style}">[]abc</${paragraph}></div> (defaultparagraphseparator: ${defaultParagraphSeparator})`);
+
+ 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();
+ doIt();
+ if (display != "block") {
+ assert_in_array(
+ editingHost.innerHTML,
+ [
+ `<${paragraph} ${styleAttr}>a</${paragraph}><${defaultParagraphSeparator}><${paragraph} ${styleAttr}>bc</${paragraph}></${defaultParagraphSeparator}>`,
+ `<${paragraph} ${styleAttr}>a</${paragraph}><${defaultParagraphSeparator}><${paragraph} ${styleAttr}>bc<br></${paragraph}></${defaultParagraphSeparator}>`,
+ ],
+ "The paragraph should be split and the latter one should be wrapped by a default paragraph"
+ );
+ } else {
+ 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"
+ );
+ }
+ }, `<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/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&param=true">
+<meta name="variant" content="?command=insertBrOrReturn&param=false">
+<meta name="variant" content="?command=heading&param=h1">
+<meta name="variant" content="?command=heading&param=h2">
+<meta name="variant" content="?command=heading&param=h3">
+<meta name="variant" content="?command=heading&param=h4">
+<meta name="variant" content="?command=heading&param=h5">
+<meta name="variant" content="?command=heading&param=h6">
+<meta name="variant" content="?command=contentReadOnly&param=true">
+<meta name="variant" content="?command=contentReadOnly&param=false">
+<meta name="variant" content="?command=readonly&param=true">
+<meta name="variant" content="?command=readonly&param=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 &lt;html contenteditable&gt;</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-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/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(/&nbsp;/g, "\u00A0");
+ }
+ function escape(str) {
+ return str.replace(/\u00A0/ig, "&nbsp;");
+ }
+
+ // Test simple removing in a text node.
+ // - initialText: Set to data of text node (only &nbsp; 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&nbsp;", expectedText: "a", whiteSpaceRange: [1, 1] },
+ { initialText: "a&nbsp;&nbsp;", expectedText: "a&nbsp;", whiteSpaceRange: [1, 2] },
+ { initialText: "a &nbsp;", expectedText: "a&nbsp;", whiteSpaceRange: [1, 2] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp;&nbsp;", whiteSpaceRange: [1, 3] },
+ { initialText: "a&nbsp; &nbsp;", expectedText: "a&nbsp;&nbsp;", whiteSpaceRange: [1, 3] },
+ { initialText: "a &nbsp;&nbsp;", expectedText: "a&nbsp;&nbsp;", whiteSpaceRange: [1, 3] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp;&nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp;", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp; &nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;", whiteSpaceRange: [1, 4] },
+ { initialText: "a &nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp;", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp; &nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp; &nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp; &nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [1, 10] },
+ { initialText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [1, 10] },
+ { initialText: "a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [1, 10] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 11] },
+ { initialText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 11] },
+ { initialText: "a&nbsp;b", expectedText: "ab", whiteSpaceRange: [1, 1] },
+ { initialText: "a b", expectedText: "ab", whiteSpaceRange: [1, 1] },
+ { initialText: "a&nbsp;&nbsp;b", expectedText: "a b", whiteSpaceRange: [1, 2] },
+ { initialText: "a&nbsp; b", expectedText: "a b", whiteSpaceRange: [1, 2] },
+ { initialText: "a &nbsp;b", expectedText: "a b", whiteSpaceRange: [1, 2] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; b", whiteSpaceRange: [1, 3] },
+ { initialText: "a&nbsp; &nbsp;b", expectedText: "a&nbsp; b", whiteSpaceRange: [1, 3] },
+ { initialText: "a &nbsp; b", expectedText: "a&nbsp; b", whiteSpaceRange: [1, 3] },
+ { initialText: "a &nbsp;&nbsp;b", expectedText: "a&nbsp; b", whiteSpaceRange: [1, 3] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp; &nbsp; b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a &nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp;&nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp; &nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp; &nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a &nbsp; &nbsp; b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp; b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp; &nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp; &nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a &nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [1, 10] },
+ { initialText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [1, 10] },
+ { initialText: "a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [1, 10] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [1, 11] },
+ { initialText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [1, 11] },
+ { initialText: "a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [1, 11] },
+ { initialText: "&nbsp;b", expectedText: "b", whiteSpaceRange: [0, 1] },
+ { initialText: "&nbsp;&nbsp;b", expectedText: "&nbsp;b", whiteSpaceRange: [0, 2] },
+ { initialText: "&nbsp; b", expectedText: "&nbsp;b", whiteSpaceRange: [0, 2] },
+ { initialText: "&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; b", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp; &nbsp;b", expectedText: "&nbsp; b", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp;&nbsp; b", expectedText: "&nbsp; b", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp; &nbsp; b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp;&nbsp; b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp; &nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp; &nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp;&nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp; &nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp; &nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp;", expectedText: "", whiteSpaceRange: [0, 1] },
+ { initialText: "&nbsp;&nbsp;", expectedText: "&nbsp;", whiteSpaceRange: [0, 2] },
+ { initialText: "&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp;&nbsp;", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp; &nbsp;", expectedText: "&nbsp;&nbsp;", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp; &nbsp;", expectedText: "&nbsp; &nbsp;", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp; &nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp; &nbsp; &nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp;&nbsp; &nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp; &nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp; &nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", 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 &nbsp; is handled at setting.
+ // "[]" means that caret position.
+ // - expectedText: Set to data of all text nodes as an array.
+ // Same as above test, only &nbsp; is handled before comparing.
+ for (const currentTest of [
+ { initialText: "a&nbsp; &nbsp;|[]&nbsp; &nbsp;b", expectedText: ["a&nbsp; []", "&nbsp; &nbsp;b"] },
+ { initialText: "a&nbsp; &nbsp;[]|&nbsp; &nbsp;b", expectedText: ["a&nbsp; []", "&nbsp; &nbsp;b"] },
+ { initialText: "a&nbsp; &nbsp;|[] &nbsp; b", expectedText: ["a&nbsp; []", "&nbsp; &nbsp;b"] },
+ { initialText: "a&nbsp; &nbsp;[]| &nbsp; b", expectedText: ["a&nbsp; []", "&nbsp; &nbsp;b"] },
+ { initialText: "a &nbsp; |[]&nbsp; &nbsp;b", expectedText: ["a &nbsp;[]", "&nbsp; &nbsp;b"] },
+ { initialText: "a &nbsp; []|&nbsp; &nbsp;b", expectedText: ["a &nbsp;[]", "&nbsp; &nbsp;b"] },
+ { initialText: "a&nbsp; &nbsp;|&nbsp;[] &nbsp;b", expectedText: ["a&nbsp; &nbsp;[]", "&nbsp; b"] },
+ { initialText: "a&nbsp; &nbsp;| []&nbsp; b", expectedText: ["a&nbsp; &nbsp;[]", "&nbsp; b"] },
+ { initialText: "a &nbsp; |&nbsp;[] &nbsp;b", expectedText: ["a &nbsp; []", "&nbsp; b"] },
+ { initialText: "a &nbsp; | []&nbsp; b", expectedText: ["a &nbsp;[]", "&nbsp; b"] },
+
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;[]&nbsp;&nbsp;b", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;", "&nbsp;[] &nbsp;b"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;[]&nbsp;&nbsp;&nbsp;b", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp;b"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;|[]&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; b"] },
+ { initialText: "a&nbsp;b[]&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;[] &nbsp;", "&nbsp;&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;b[]&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; []&nbsp;&nbsp;", "&nbsp;&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;b[]|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;b|[]&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;b[]|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;b|[]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;|b[]&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]|b&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;[]", "b&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;|&nbsp;b[]&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;", "&nbsp;[] &nbsp; &nbsp;c"] },
+
+ { initialText: "a&nbsp;&nbsp;&nbsp;|&nbsp;|[]&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;| |[]&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;| []|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;[]| |&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp;", "&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;| |&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; []&nbsp;", " ", "&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;&nbsp;|&nbsp;|&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; []&nbsp;&nbsp;", "&nbsp;", "&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;&nbsp;| |&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; []&nbsp;&nbsp;", " ", "&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;||&nbsp;[]&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;||[]&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;|[]|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;[]||&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;||&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; []&nbsp;", "", "&nbsp;&nbsp;&nbsp;&nbsp;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>&nbsp;[]def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" },
+ { initialHTML: "<span>abc <span>[]&nbsp;def</span></span>", expectedHTML: "<span>abc<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc []<span>&nbsp;def</span></span>", expectedHTML: "<span>abc<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc&nbsp;<span>&nbsp;[]def</span></span>", expectedHTML: "<span>abc&nbsp;<span>def</span></span>" },
+ { initialHTML: "<span>abc&nbsp;<span>[]&nbsp;def</span></span>", expectedHTML: "<span>abc<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc&nbsp;[]<span>&nbsp;def</span></span>", expectedHTML: "<span>abc<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc&nbsp; <span>&nbsp;[]def</span></span>", expectedHTML: "<span>abc&nbsp; <span>def</span></span>" },
+ { initialHTML: "<span>abc&nbsp; <span>[]&nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc&nbsp; []<span>&nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc &nbsp;<span>&nbsp;[]def</span></span>", expectedHTML: "<span>abc &nbsp;<span>def</span></span>" },
+ { initialHTML: "<span>abc &nbsp;<span>[]&nbsp;def</span></span>", expectedHTML: "<span>abc <span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc &nbsp;[]<span>&nbsp;def</span></span>", expectedHTML: "<span>abc <span>&nbsp;def</span></span>" },
+
+ { initialHTML: "<span>abc </span><span>&nbsp;[]def</span>", expectedHTML: "<span>abc </span><span>def</span>" },
+ { initialHTML: "<span>abc </span><span>[]&nbsp;def</span>", expectedHTML: "<span>abc</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc []</span><span>&nbsp;def</span>", expectedHTML: "<span>abc</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span>&nbsp;[]def</span>", expectedHTML: "<span>abc&nbsp;</span><span>def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span>[]&nbsp;def</span>", expectedHTML: "<span>abc</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;[]</span><span>&nbsp;def</span>", expectedHTML: "<span>abc</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp; </span><span>&nbsp;[]def</span>", expectedHTML: "<span>abc&nbsp; </span><span>def</span>" },
+ { initialHTML: "<span>abc&nbsp; </span><span>[]&nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp; []</span><span>&nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc &nbsp;</span><span>&nbsp;[]def</span>", expectedHTML: "<span>abc &nbsp;</span><span>def</span>" },
+ { initialHTML: "<span>abc &nbsp;</span><span>[]&nbsp;def</span>", expectedHTML: "<span>abc </span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc &nbsp;[]</span><span>&nbsp;def</span>", expectedHTML: "<span>abc </span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span>&nbsp; []def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span>&nbsp; &nbsp;[]def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span>&nbsp; []&nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span>&nbsp;[] &nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span> &nbsp; []def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span> &nbsp;[] def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span> []&nbsp; def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc&nbsp;</span><span>[] &nbsp; def</span>", expectedHTML: "<span>abc</span><span>&nbsp; &nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;[]</span><span> &nbsp; def</span>", expectedHTML: "<span>abc</span><span>&nbsp; &nbsp;def</span>" },
+ { initialHTML: "<span>abc &nbsp;[]</span><span> &nbsp; def</span>", expectedHTML: "<span>abc </span><span>&nbsp; &nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;&nbsp;[]</span><span> &nbsp; def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; &nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;&nbsp;[]</span><span>&nbsp; &nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; &nbsp;def</span>" },
+
+ { initialHTML: "<span><span>abc </span>&nbsp;[]def</span>", expectedHTML: "<span><span>abc </span>def</span>" },
+ { initialHTML: "<span><span>abc </span>[]&nbsp;def</span>", expectedHTML: "<span><span>abc</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc []</span>&nbsp;def</span>", expectedHTML: "<span><span>abc</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc&nbsp;</span>&nbsp;[]def</span>", expectedHTML: "<span><span>abc&nbsp;</span>def</span>" },
+ { initialHTML: "<span><span>abc&nbsp;</span>[]&nbsp;def</span>", expectedHTML: "<span><span>abc</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc&nbsp;[]</span>&nbsp;def</span>", expectedHTML: "<span><span>abc</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc&nbsp; </span>&nbsp;[]def</span>", expectedHTML: "<span><span>abc&nbsp; </span>def</span>" },
+ { initialHTML: "<span><span>abc&nbsp; </span>[]&nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc&nbsp; []</span>&nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;</span>&nbsp;[]def</span>", expectedHTML: "<span><span>abc &nbsp;</span>def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;</span>[]&nbsp;def</span>", expectedHTML: "<span><span>abc </span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;[]</span>&nbsp;def</span>", expectedHTML: "<span><span>abc </span>&nbsp;def</span>" },
+
+ { initialHTML: "<span><span>abc &nbsp;</span></span><span>&nbsp;[]def</span>", expectedHTML: "<span><span>abc &nbsp;</span></span><span>def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;</span></span><span>[]&nbsp;def</span>", expectedHTML: "<span><span>abc </span></span><span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;[]</span></span><span>&nbsp;def</span>", expectedHTML: "<span><span>abc </span></span><span>&nbsp;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&nbsp;&nbsp;&nbsp;<span style=white-space:pre;>[] </span>", expectedHTML: "a&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;&nbsp;&nbsp;[]<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;&nbsp;[]&nbsp;<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;[]&nbsp;&nbsp;<span style=white-space:pre;>b </span>", expectedHTML: "a&nbsp;&nbsp;<span style=\"white-space:pre;\">b </span>" },
+ { initialHTML: "a&nbsp;&nbsp;&nbsp;&nbsp;<span style=white-space:pre;>[] </span>", expectedHTML: "a&nbsp;&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;&nbsp;&nbsp;&nbsp;[]<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp;&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;&nbsp;&nbsp;[]&nbsp;<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp; &nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;&nbsp;[]&nbsp;<span style=white-space:pre;>b </span>", expectedHTML: "a&nbsp;&nbsp;<span style=\"white-space:pre;\">b </span>" },
+ { initialHTML: "<span style=white-space:pre;> [] </span>&nbsp;&nbsp;&nbsp;a", expectedHTML: "<span style=\"white-space:pre;\"> </span>&nbsp;&nbsp;&nbsp;a" },
+ { initialHTML: "<span style=white-space:pre;> []</span>&nbsp;&nbsp;&nbsp;a", expectedHTML: "<span style=\"white-space:pre;\"> </span>&nbsp; &nbsp;a" },
+ { initialHTML: "<span style=white-space:pre;> </span>[]&nbsp;&nbsp;&nbsp;a", expectedHTML: "<span style=\"white-space:pre;\"> </span>&nbsp; &nbsp;a" },
+ { initialHTML: "<span style=white-space:pre;> </span>&nbsp;[]&nbsp;&nbsp;&nbsp;a", expectedHTML: "<span style=\"white-space:pre;\"> </span>&nbsp; &nbsp;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(/&nbsp;/g, "\u00A0");
+ }
+ function escape(str) {
+ return str.replace(/\u00A0/ig, "&nbsp;");
+ }
+
+ // Test simple removing in a text node.
+ // - initialText: Set to data of text node (only &nbsp; 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&nbsp;", expectedText: "a", whiteSpaceRange: [1, 1] },
+ { initialText: "a&nbsp;&nbsp;", expectedText: "a&nbsp;", whiteSpaceRange: [1, 2] },
+ { initialText: "a &nbsp;", expectedText: "a&nbsp;", whiteSpaceRange: [1, 2] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp;&nbsp;", whiteSpaceRange: [1, 3] },
+ { initialText: "a&nbsp; &nbsp;", expectedText: "a&nbsp;&nbsp;", whiteSpaceRange: [1, 3] },
+ { initialText: "a &nbsp;&nbsp;", expectedText: "a&nbsp;&nbsp;", whiteSpaceRange: [1, 3] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp;&nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp;", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp; &nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;", whiteSpaceRange: [1, 4] },
+ { initialText: "a &nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp;", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp; &nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp; &nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp; &nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [1, 10] },
+ { initialText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [1, 10] },
+ { initialText: "a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [1, 10] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 11] },
+ { initialText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", whiteSpaceRange: [1, 11] },
+ { initialText: "a&nbsp;b", expectedText: "ab", whiteSpaceRange: [1, 1] },
+ { initialText: "a b", expectedText: "ab", whiteSpaceRange: [1, 1] },
+ { initialText: "a&nbsp;&nbsp;b", expectedText: "a b", whiteSpaceRange: [1, 2] },
+ { initialText: "a&nbsp; b", expectedText: "a b", whiteSpaceRange: [1, 2] },
+ { initialText: "a &nbsp;b", expectedText: "a b", whiteSpaceRange: [1, 2] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; b", whiteSpaceRange: [1, 3] },
+ { initialText: "a&nbsp; &nbsp;b", expectedText: "a&nbsp; b", whiteSpaceRange: [1, 3] },
+ { initialText: "a &nbsp; b", expectedText: "a&nbsp; b", whiteSpaceRange: [1, 3] },
+ { initialText: "a &nbsp;&nbsp;b", expectedText: "a&nbsp; b", whiteSpaceRange: [1, 3] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp; &nbsp; b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a &nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp;&nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp; &nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp;b", whiteSpaceRange: [1, 4] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp; &nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a &nbsp; &nbsp; b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp; b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp; &nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp; &nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a &nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; b", whiteSpaceRange: [1, 5] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [1, 10] },
+ { initialText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [1, 10] },
+ { initialText: "a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [1, 10] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [1, 11] },
+ { initialText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [1, 11] },
+ { initialText: "a &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", expectedText: "a&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [1, 11] },
+ { initialText: "&nbsp;b", expectedText: "b", whiteSpaceRange: [0, 1] },
+ { initialText: "&nbsp;&nbsp;b", expectedText: "&nbsp;b", whiteSpaceRange: [0, 2] },
+ { initialText: "&nbsp; b", expectedText: "&nbsp;b", whiteSpaceRange: [0, 2] },
+ { initialText: "&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; b", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp; &nbsp;b", expectedText: "&nbsp; b", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp;&nbsp; b", expectedText: "&nbsp; b", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp; &nbsp; b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp;&nbsp; b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp; &nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp;b", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp; &nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp;&nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp; &nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp; &nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; b", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; b", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp;", expectedText: "", whiteSpaceRange: [0, 1] },
+ { initialText: "&nbsp;&nbsp;", expectedText: "&nbsp;", whiteSpaceRange: [0, 2] },
+ { initialText: "&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp;&nbsp;", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp; &nbsp;", expectedText: "&nbsp;&nbsp;", whiteSpaceRange: [0, 3] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp; &nbsp;", expectedText: "&nbsp; &nbsp;", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp; &nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;", whiteSpaceRange: [0, 4] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp; &nbsp; &nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp;&nbsp; &nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp; &nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp; &nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 5] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", whiteSpaceRange: [0, 10] },
+ { initialText: "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", whiteSpaceRange: [0, 11] },
+ { initialText: "&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", expectedText: "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;", 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 &nbsp; is handled at setting.
+ // "[]" means that caret position.
+ // - expectedText: Set to data of all text nodes as an array.
+ // Same as above test, only &nbsp; is handled before comparing.
+ for (const currentTest of [
+ { initialText: "a&nbsp; []&nbsp;|&nbsp; &nbsp;b", expectedText: ["a&nbsp; []", "&nbsp; &nbsp;b"] },
+ { initialText: "a&nbsp; []&nbsp;| &nbsp; b", expectedText: ["a&nbsp; []", "&nbsp; &nbsp;b"] },
+ { initialText: "a&nbsp; &nbsp;[]|&nbsp; &nbsp;b", expectedText: ["a&nbsp; &nbsp;[]", "&nbsp; b"] },
+ { initialText: "a&nbsp; &nbsp;[]| &nbsp; b", expectedText: ["a&nbsp; &nbsp;[]", "&nbsp; b"] },
+ { initialText: "a&nbsp; &nbsp;|[]&nbsp; &nbsp;b", expectedText: ["a&nbsp; &nbsp;[]", "&nbsp; b"] },
+ { initialText: "a&nbsp; &nbsp;|[] &nbsp; b", expectedText: ["a&nbsp; &nbsp;[]", "&nbsp; b"] },
+ { initialText: "a&nbsp; &nbsp;| []&nbsp; b", expectedText: ["a&nbsp; &nbsp;", "&nbsp;[] b"] },
+ { initialText: "a &nbsp; |[]&nbsp; &nbsp;b", expectedText: ["a &nbsp; []", "&nbsp; b"] },
+ { initialText: "a &nbsp; []|&nbsp; &nbsp;b", expectedText: ["a &nbsp; []", "&nbsp; b"] },
+ { initialText: "a &nbsp;[] |&nbsp; &nbsp;b", expectedText: ["a &nbsp;[]", "&nbsp; &nbsp;b"] },
+
+ { initialText: "a&nbsp;&nbsp;[]&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: ["a&nbsp; []&nbsp;", "&nbsp;&nbsp;&nbsp;&nbsp;b"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; b"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;[]|&nbsp;&nbsp;&nbsp;&nbsp;b", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp;b"] },
+ { initialText: "a&nbsp;[]b&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;[] &nbsp;", "&nbsp;&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;[]b&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; []&nbsp;&nbsp;", "&nbsp;&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]b|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]|b&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;|[]b&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;[]&nbsp;|b&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;[]", "b&nbsp;&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;|&nbsp;[]b&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;", "&nbsp;[] &nbsp; c"] },
+
+ { initialText: "a&nbsp;&nbsp;&nbsp;|&nbsp;|[]&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;", "&nbsp;[]", "&nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;| |[]&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;", " []", "&nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;| []|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;", " []", "&nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;|[] |&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;[]| |&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;| |&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp;", "&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;&nbsp;|&nbsp;|&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; &nbsp;[]&nbsp;", "&nbsp;", "&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;&nbsp;| |&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; &nbsp;[]&nbsp;", " ", "&nbsp;&nbsp;&nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;||&nbsp;[]&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;", "", "&nbsp;[] &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;||[]&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;|[]|&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;&nbsp;[]||&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp;c"] },
+ { initialText: "a&nbsp;&nbsp;&nbsp;[]&nbsp;||&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp;&nbsp;&nbsp;[]", "&nbsp; &nbsp; c"] },
+ { initialText: "a&nbsp;&nbsp;[]&nbsp;&nbsp;||&nbsp;&nbsp;&nbsp;&nbsp;c", expectedText: ["a&nbsp; []&nbsp;", "", "&nbsp;&nbsp;&nbsp;&nbsp;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>&nbsp;def</span></span>", expectedHTML: "<span>abc<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc[]&nbsp;<span>&nbsp;def</span></span>", expectedHTML: "<span>abc<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc[]&nbsp;<span> def</span></span>", expectedHTML: "<span>abc<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc []<span>&nbsp;def</span></span>", expectedHTML: "<span>abc <span>def</span></span>" },
+ { initialHTML: "<span>abc&nbsp;[]<span>&nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;<span>def</span></span>" },
+ { initialHTML: "<span>abc&nbsp;[]<span> def</span></span>", expectedHTML: "<span>abc&nbsp;<span>def</span></span>" },
+ { initialHTML: "<span>abc[]&nbsp;<span>&nbsp; def</span></span>", expectedHTML: "<span>abc<span>&nbsp; def</span></span>" },
+ { initialHTML: "<span>abc[]&nbsp;<span> &nbsp;def</span></span>", expectedHTML: "<span>abc<span>&nbsp; def</span></span>" },
+ { initialHTML: "<span>abc[]&nbsp; <span>&nbsp;&nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;<span>&nbsp;&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc[]&nbsp; <span>&nbsp; def</span></span>", expectedHTML: "<span>abc&nbsp;<span>&nbsp; def</span></span>" },
+ { initialHTML: "<span>abc[]&nbsp;&nbsp;<span>&nbsp;&nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;<span>&nbsp;&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc[] &nbsp;<span>&nbsp;&nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;<span>&nbsp;&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc[] &nbsp;<span> &nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;<span> &nbsp;def</span></span>" },
+ { initialHTML: "<span>abc[] &nbsp;<span>&nbsp; def</span></span>", expectedHTML: "<span>abc&nbsp;<span>&nbsp; def</span></span>" },
+ { initialHTML: "<span>abc&nbsp; []<span>&nbsp; def</span></span>", expectedHTML: "<span>abc&nbsp; <span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc&nbsp; <span>[]&nbsp; def</span></span>", expectedHTML: "<span>abc&nbsp; <span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc&nbsp;&nbsp;[]<span> &nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;&nbsp;<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc&nbsp;&nbsp;<span>[] &nbsp;def</span></span>", expectedHTML: "<span>abc&nbsp;&nbsp;<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc &nbsp;[]<span>&nbsp; def</span></span>", expectedHTML: "<span>abc &nbsp;<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc &nbsp;<span>[]&nbsp; def</span></span>", expectedHTML: "<span>abc &nbsp;<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc&nbsp; <span>&nbsp;[] def</span></span>", expectedHTML: "<span>abc&nbsp; <span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc &nbsp;<span>&nbsp;[] def</span></span>", expectedHTML: "<span>abc &nbsp;<span>&nbsp;def</span></span>" },
+ { initialHTML: "<span>abc &nbsp;<span> []&nbsp;def</span></span>", expectedHTML: "<span>abc &nbsp;<span>&nbsp;def</span></span>" },
+
+ { initialHTML: "<span><span>abc[] </span>&nbsp;def</span>", expectedHTML: "<span><span>abc</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc[]&nbsp;</span>&nbsp;def</span>", expectedHTML: "<span><span>abc</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc[]&nbsp;</span> def</span>", expectedHTML: "<span><span>abc</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc []</span>&nbsp;def</span>", expectedHTML: "<span><span>abc </span>def</span>" },
+ { initialHTML: "<span><span>abc&nbsp;[]</span>&nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;</span>def</span>" },
+ { initialHTML: "<span><span>abc&nbsp;[]</span> def</span>", expectedHTML: "<span><span>abc&nbsp;</span>def</span>" },
+ { initialHTML: "<span><span>abc[]&nbsp;</span>&nbsp; def</span>", expectedHTML: "<span><span>abc</span>&nbsp; def</span>" },
+ { initialHTML: "<span><span>abc[]&nbsp;</span> &nbsp;def</span>", expectedHTML: "<span><span>abc</span>&nbsp; def</span>" },
+ { initialHTML: "<span><span>abc[]&nbsp; </span>&nbsp;&nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;</span>&nbsp;&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc[]&nbsp; </span>&nbsp; def</span>", expectedHTML: "<span><span>abc&nbsp;</span>&nbsp; def</span>" },
+ { initialHTML: "<span><span>abc[]&nbsp;&nbsp;</span>&nbsp;&nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;</span>&nbsp;&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc[] &nbsp;</span>&nbsp;&nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;</span>&nbsp;&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc[] &nbsp;</span> &nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;</span> &nbsp;def</span>" },
+ { initialHTML: "<span><span>abc[] &nbsp;</span>&nbsp; def</span>", expectedHTML: "<span><span>abc&nbsp;</span>&nbsp; def</span>" },
+ { initialHTML: "<span><span>abc&nbsp; []</span>&nbsp; def</span>", expectedHTML: "<span><span>abc&nbsp; </span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc&nbsp; </span>[]&nbsp; def</span>", expectedHTML: "<span><span>abc&nbsp; </span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc&nbsp;&nbsp;[]</span> &nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;&nbsp;</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc&nbsp;&nbsp;</span>[] &nbsp;def</span>", expectedHTML: "<span><span>abc&nbsp;&nbsp;</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;[]</span>&nbsp; def</span>", expectedHTML: "<span><span>abc &nbsp;</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;</span>[]&nbsp; def</span>", expectedHTML: "<span><span>abc &nbsp;</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc&nbsp; </span>&nbsp;[] def</span>", expectedHTML: "<span><span>abc&nbsp; </span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;</span>&nbsp;[] def</span>", expectedHTML: "<span><span>abc &nbsp;</span>&nbsp;def</span>" },
+ { initialHTML: "<span><span>abc &nbsp;</span> []&nbsp;def</span>", expectedHTML: "<span><span>abc &nbsp;</span>&nbsp;def</span>" },
+
+ { initialHTML: "<span>abc[] </span><span>&nbsp;def</span>", expectedHTML: "<span>abc</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc[]&nbsp;</span><span>&nbsp;def</span>", expectedHTML: "<span>abc</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc[]&nbsp;</span><span> def</span>", expectedHTML: "<span>abc</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc []</span><span>&nbsp;def</span>", expectedHTML: "<span>abc </span><span>def</span>" },
+ { initialHTML: "<span>abc&nbsp;[]</span><span>&nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>def</span>" },
+ { initialHTML: "<span>abc&nbsp;[]</span><span> def</span>", expectedHTML: "<span>abc&nbsp;</span><span>def</span>" },
+ { initialHTML: "<span>abc[]&nbsp;</span><span>&nbsp; def</span>", expectedHTML: "<span>abc</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc[]&nbsp;</span><span> &nbsp;def</span>", expectedHTML: "<span>abc</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc[]&nbsp; </span><span>&nbsp;&nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp;&nbsp;def</span>" },
+ { initialHTML: "<span>abc[]&nbsp; </span><span>&nbsp; def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc[]&nbsp;&nbsp;</span><span>&nbsp;&nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp;&nbsp;def</span>" },
+ { initialHTML: "<span>abc[] &nbsp;</span><span>&nbsp;&nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp;&nbsp;def</span>" },
+ { initialHTML: "<span>abc[] &nbsp;</span><span> &nbsp;def</span>", expectedHTML: "<span>abc&nbsp;</span><span> &nbsp;def</span>" },
+ { initialHTML: "<span>abc[] &nbsp;</span><span>&nbsp; def</span>", expectedHTML: "<span>abc&nbsp;</span><span>&nbsp; def</span>" },
+ { initialHTML: "<span>abc&nbsp; []</span><span>&nbsp; def</span>", expectedHTML: "<span>abc&nbsp; </span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp; </span><span>[]&nbsp; def</span>", expectedHTML: "<span>abc&nbsp; </span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;&nbsp;[]</span><span> &nbsp;def</span>", expectedHTML: "<span>abc&nbsp;&nbsp;</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp;&nbsp;</span><span>[] &nbsp;def</span>", expectedHTML: "<span>abc&nbsp;&nbsp;</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc &nbsp;[]</span><span>&nbsp; def</span>", expectedHTML: "<span>abc &nbsp;</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc &nbsp;</span><span>[]&nbsp; def</span>", expectedHTML: "<span>abc &nbsp;</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc&nbsp; </span><span>&nbsp;[] def</span>", expectedHTML: "<span>abc&nbsp; </span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc &nbsp;</span><span>&nbsp;[] def</span>", expectedHTML: "<span>abc &nbsp;</span><span>&nbsp;def</span>" },
+ { initialHTML: "<span>abc &nbsp;</span><span> []&nbsp;def</span>", expectedHTML: "<span>abc &nbsp;</span><span>&nbsp;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&nbsp;&nbsp;&nbsp;[]<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp;&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;&nbsp;[]&nbsp;<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;[]&nbsp;&nbsp;<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;[]&nbsp;&nbsp;<span style=white-space:pre;>b </span>", expectedHTML: "a&nbsp;&nbsp;<span style=\"white-space:pre;\">b </span>" },
+ { initialHTML: "a&nbsp;&nbsp;&nbsp;[]&nbsp;<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp;&nbsp;&nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;&nbsp;[]&nbsp;&nbsp;<span style=white-space:pre;> </span>", expectedHTML: "a&nbsp; &nbsp;<span style=\"white-space:pre;\"> </span>" },
+ { initialHTML: "a&nbsp;&nbsp;[]&nbsp;&nbsp;<span style=white-space:pre;>b </span>", expectedHTML: "a&nbsp; &nbsp;<span style=\"white-space:pre;\">b </span>" },
+ { initialHTML: "<span style=white-space:pre;> [] </span>&nbsp;&nbsp;&nbsp;a", expectedHTML: "<span style=\"white-space:pre;\"> </span>&nbsp;&nbsp;&nbsp;a" },
+ { initialHTML: "<span style=white-space:pre;> [] </span>&nbsp;&nbsp;&nbsp;a", expectedHTML: "<span style=\"white-space:pre;\"> </span>&nbsp; &nbsp;a" },
+ { initialHTML: "<span style=white-space:pre;> []</span>&nbsp;&nbsp;&nbsp;&nbsp;a", expectedHTML: "<span style=\"white-space:pre;\"> </span>&nbsp; &nbsp;a" },
+ { initialHTML: "<span style=white-space:pre;> </span>[]&nbsp;&nbsp;&nbsp;&nbsp;a", expectedHTML: "<span style=\"white-space:pre;\"> </span>&nbsp; &nbsp;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, "&nbsp;") : "";
+ }
+
+ 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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 0);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `<br>a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 2);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;<br>${escape(generateWhiteSpaces(8, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 3);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;&nbsp;<br>${escape(generateWhiteSpaces(7, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 4);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;&nbsp;&nbsp;<br>${escape(generateWhiteSpaces(6, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 5);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;&nbsp;&nbsp;&nbsp;<br>${escape(generateWhiteSpaces(5, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 6);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>${escape(generateWhiteSpaces(4, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 7);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>${escape(generateWhiteSpaces(3, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 8);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>${escape(generateWhiteSpaces(2, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 9);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>${escape(generateWhiteSpaces(1, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("insertlinebreak", false, "") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ editor.innerHTML = "a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b";
+ selection.collapse(editor.firstChild, 10);
+ test(function () {
+ document.execCommand("insertlinebreak", false, "");
+ assert_equals(editor.innerHTML,
+ `a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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, "&nbsp;") : "";
+ }
+
+ 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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b</div>";
+ selection.collapse(editor.firstChild.firstChild, 0);
+ test(function () {
+ document.execCommand("insertparagraph", false, "");
+ assert_equals(editor.innerHTML,
+ `<div><br></div><div>a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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(/&nbsp;/g, "\u00A0");
+ }
+ function escape(str) {
+ return typeof(str) === "string" ? str.replace(/\u00A0/ig, "&nbsp;") : "";
+ }
+
+ 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 ? "&nbsp;" : 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 ? "&nbsp; " : escape(generateWhiteSpaces(i + 2, false))}b`,
+ "Modified text is wrong");
+ }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+ }
+
+ editor.innerHTML = "a&nbsp;b";
+ selection.collapse(editor.firstChild, 1);
+ test(function () {
+ document.execCommand("inserttext", false, " ");
+ assert_equals(escape(editor.firstChild.data), "a&nbsp; b", "Modified text is wrong");
+ }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}"`);
+
+ for (let i = 1; i <= 3; i++) {
+ editor.innerHTML = "a&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>&nbsp;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>&nbsp;c`,
+ "Modified text is wrong");
+ }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span>&nbsp;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>&nbsp;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>&nbsp;c</span>`,
+ "Modified text is wrong");
+ }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span><span>&nbsp;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>&nbsp;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>&nbsp;c</span></span>`,
+ "Modified text is wrong");
+ }, `execCommand("inserttext", false, " ") at "a<span>${getDescriptionForTextNode(editor.querySelector("span").firstChild)}</span><span><span>&nbsp;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>&nbsp;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>&nbsp;c</span>`,
+ "Modified text is wrong");
+ }, `execCommand("inserttext", false, " ") at "a<span><span>${getDescriptionForTextNode(editor.querySelector("span span").firstChild)}</span></span><span>&nbsp;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>&nbsp;</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\">&nbsp;</span>`,
+ "Modified text is wrong");
+ }, `execCommand("inserttext", false, " ") at "${getDescriptionForTextNode(editor.firstChild)}<span style=white-space:pre>&nbsp;</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>&nbsp;</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\">&nbsp;</span>`,
+ "Modified text is wrong");
+ }, `execCommand("inserttext", false, "\\u00A0") at "${getDescriptionForTextNode(editor.firstChild)}<span style=white-space:pre>&nbsp;</span>"`);
+ }
+
+ editor.innerHTML = "a&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;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&nbsp;&nbsp;&nbsp;&nbsp;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 &nbsp; is handled at setting.
+ // "[]" means that caret position.
+ // - expectedText: Set to data of all text nodes as an array.
+ // Same as above test, only &nbsp; 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[]&nbsp;|b", expectedText: ["a&nbsp;", "b"] },
+ { initialText: "a&nbsp;[]|b", expectedText: ["a&nbsp;", "b"] },
+ { initialText: "a&nbsp;|[]b", expectedText: ["a&nbsp;", "b"] },
+ { initialText: "a[]|&nbsp;b", expectedText: ["a", "&nbsp;b"] },
+ { initialText: "a|[]&nbsp;b", expectedText: ["a", "&nbsp;b"] },
+ { initialText: "a|&nbsp;[]b", expectedText: ["a", "&nbsp;b"] },
+ { initialText: "a[] |&nbsp;b", expectedText: ["a ", "&nbsp;b"] },
+ { initialText: "a []|&nbsp;b", expectedText: ["a ", "&nbsp;b"] },
+ { initialText: "a |[]&nbsp;b", expectedText: ["a ", "&nbsp;b"] },
+ { initialText: "a |&nbsp;[]b", expectedText: ["a ", "&nbsp;b"] },
+ { initialText: "a[]&nbsp;| b", expectedText: ["a&nbsp;", " b"] },
+ { initialText: "a&nbsp;[]| b", expectedText: ["a&nbsp;", " b"] },
+ { initialText: "a&nbsp;|[] b", expectedText: ["a&nbsp;", " b"] },
+ { initialText: "a&nbsp;| []b", expectedText: ["a&nbsp;", " b"] },
+ { initialText: "a[]&nbsp;|&nbsp;b", expectedText: ["a&nbsp;", "&nbsp;b"] },
+ { initialText: "a&nbsp;[]|&nbsp;b", expectedText: ["a&nbsp;", "&nbsp;b"] },
+ { initialText: "a&nbsp;|[]&nbsp;b", expectedText: ["a&nbsp;", "&nbsp;b"] },
+ { initialText: "a&nbsp;|&nbsp;[]b", expectedText: ["a&nbsp;", "&nbsp;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>