summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/selection
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/selection')
-rw-r--r--testing/web-platform/tests/selection/Document-open.html28
-rw-r--r--testing/web-platform/tests/selection/META.yml3
-rw-r--r--testing/web-platform/tests/selection/addRange-00.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-04.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-08.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-12.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-16.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-20.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-24.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-28.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-32.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-36.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-40.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-44.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-48.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-52.html14
-rw-r--r--testing/web-platform/tests/selection/addRange-56.html14
-rw-r--r--testing/web-platform/tests/selection/addRange.htm29
-rw-r--r--testing/web-platform/tests/selection/addRange.js205
-rw-r--r--testing/web-platform/tests/selection/addRange.tentative.html29
-rw-r--r--testing/web-platform/tests/selection/anonymous/details-ancestor.html40
-rw-r--r--testing/web-platform/tests/selection/anonymous/details-mutate.html38
-rw-r--r--testing/web-platform/tests/selection/bidi/modify.tentative.html60
-rw-r--r--testing/web-platform/tests/selection/caret/collapse-pre-linestart-1.html12
-rw-r--r--testing/web-platform/tests/selection/caret/collapse-pre-linestart-2.html13
-rw-r--r--testing/web-platform/tests/selection/caret/collapse-pre-linestart-ref.html8
-rw-r--r--testing/web-platform/tests/selection/caret/empty-elements.html25
-rw-r--r--testing/web-platform/tests/selection/collapse-00.html14
-rw-r--r--testing/web-platform/tests/selection/collapse-15.html14
-rw-r--r--testing/web-platform/tests/selection/collapse-30.html14
-rw-r--r--testing/web-platform/tests/selection/collapse-45.html14
-rw-r--r--testing/web-platform/tests/selection/collapse.htm30
-rw-r--r--testing/web-platform/tests/selection/collapse.js103
-rw-r--r--testing/web-platform/tests/selection/collapseToStartEnd.html114
-rw-r--r--testing/web-platform/tests/selection/common.js1002
-rw-r--r--testing/web-platform/tests/selection/contenteditable/cefalse-on-boundaries.html72
-rw-r--r--testing/web-platform/tests/selection/contenteditable/collapse.html43
-rw-r--r--testing/web-platform/tests/selection/contenteditable/initial-selection-on-focus.tentative.html473
-rw-r--r--testing/web-platform/tests/selection/contenteditable/modify.tentative.html60
-rw-r--r--testing/web-platform/tests/selection/contenteditable/modifying-selection-with-middle-mouse-button.tentative.html213
-rw-r--r--testing/web-platform/tests/selection/contenteditable/modifying-selection-with-primary-mouse-button.tentative.html212
-rw-r--r--testing/web-platform/tests/selection/crashtests/table.html14
-rw-r--r--testing/web-platform/tests/selection/deleteFromDocument.html97
-rw-r--r--testing/web-platform/tests/selection/dir-manual.html106
-rw-r--r--testing/web-platform/tests/selection/drag-disabled-textarea-shadow-dom.html39
-rw-r--r--testing/web-platform/tests/selection/extend-00.html16
-rw-r--r--testing/web-platform/tests/selection/extend-20.html16
-rw-r--r--testing/web-platform/tests/selection/extend-40.html16
-rw-r--r--testing/web-platform/tests/selection/extend-exception.html21
-rw-r--r--testing/web-platform/tests/selection/extend.js161
-rw-r--r--testing/web-platform/tests/selection/getRangeAt.html32
-rw-r--r--testing/web-platform/tests/selection/getSelection.html160
-rw-r--r--testing/web-platform/tests/selection/idlharness.window.js18
-rw-r--r--testing/web-platform/tests/selection/isCollapsed.html33
-rw-r--r--testing/web-platform/tests/selection/modify-extend-word-trailing-inline-block.tentative.html23
-rw-r--r--testing/web-platform/tests/selection/modify-line-flex-column.tentative.html47
-rw-r--r--testing/web-platform/tests/selection/modify-line-flex-row.tentative.html50
-rw-r--r--testing/web-platform/tests/selection/modify-line-grid-basic.tentative.html47
-rw-r--r--testing/web-platform/tests/selection/modify.tentative.html90
-rw-r--r--testing/web-platform/tests/selection/removeAllRanges.html55
-rw-r--r--testing/web-platform/tests/selection/removeRange.html47
-rw-r--r--testing/web-platform/tests/selection/script-and-style-elements.html37
-rw-r--r--testing/web-platform/tests/selection/select-end-of-line-image.tentative.html49
-rw-r--r--testing/web-platform/tests/selection/selectAllChildren.html64
-rw-r--r--testing/web-platform/tests/selection/selection-select-all-move-input-crash.html10
-rw-r--r--testing/web-platform/tests/selection/selection-shadow-dom-crash-print.html18
-rw-r--r--testing/web-platform/tests/selection/setBaseAndExtent.html130
-rw-r--r--testing/web-platform/tests/selection/stringifier.tentative.html28
-rw-r--r--testing/web-platform/tests/selection/test-iframe.html33
-rw-r--r--testing/web-platform/tests/selection/textcontrols/focus.html43
-rw-r--r--testing/web-platform/tests/selection/textcontrols/onselectionchange-content-attribute.html29
-rw-r--r--testing/web-platform/tests/selection/textcontrols/selectionchange-bubble.html33
-rw-r--r--testing/web-platform/tests/selection/textcontrols/selectionchange.html212
-rw-r--r--testing/web-platform/tests/selection/toString-ff-bug-001.html23
-rw-r--r--testing/web-platform/tests/selection/type.html31
-rw-r--r--testing/web-platform/tests/selection/user-select-on-input-and-contenteditable.html41
76 files changed, 4947 insertions, 0 deletions
diff --git a/testing/web-platform/tests/selection/Document-open.html b/testing/web-platform/tests/selection/Document-open.html
new file mode 100644
index 0000000000..9e3cb28124
--- /dev/null
+++ b/testing/web-platform/tests/selection/Document-open.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<title>Selection Document.open() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+const iframe = document.createElement("iframe");
+async_test(t => {
+ iframe.onload = t.step_func_done(() => {
+ const originalSelection = iframe.contentWindow.getSelection();
+ assert_equals(originalSelection.rangeCount, 0, "rangeCount must initially be 0");
+ iframe.contentDocument.body.appendChild(iframe.contentDocument.createTextNode("foo"));
+ const range = iframe.contentDocument.createRange();
+ range.selectNodeContents(iframe.contentDocument.body);
+ iframe.contentWindow.getSelection().addRange(range);
+ assert_equals(originalSelection.rangeCount, 1, "rangeCount must be 1 after adding a range");
+
+ iframe.contentDocument.open();
+
+ assert_equals(iframe.contentWindow.getSelection(), originalSelection, "After document.open(), the Selection object must be the same");
+ assert_equals(iframe.contentWindow.getSelection().rangeCount, 1, "After document.open(), rangeCount must still be 1");
+ document.body.removeChild(iframe);
+ });
+ document.body.appendChild(iframe);
+}, "Selection must not be replaced with a new object after document.open()");
+</script>
diff --git a/testing/web-platform/tests/selection/META.yml b/testing/web-platform/tests/selection/META.yml
new file mode 100644
index 0000000000..efe4ede48f
--- /dev/null
+++ b/testing/web-platform/tests/selection/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/selection-api/
+suggested_reviewers:
+ - rniwa
diff --git a/testing/web-platform/tests/selection/addRange-00.html b/testing/web-platform/tests/selection/addRange-00.html
new file mode 100644
index 0000000000..7f73f2a3a3
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-00.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(0, 4);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-04.html b/testing/web-platform/tests/selection/addRange-04.html
new file mode 100644
index 0000000000..b9598a4b85
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-04.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(4, 8);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-08.html b/testing/web-platform/tests/selection/addRange-08.html
new file mode 100644
index 0000000000..79e8ea6451
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-08.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(8, 12);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-12.html b/testing/web-platform/tests/selection/addRange-12.html
new file mode 100644
index 0000000000..02d4f6d1f5
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-12.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(12, 16);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-16.html b/testing/web-platform/tests/selection/addRange-16.html
new file mode 100644
index 0000000000..295b35f01b
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-16.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(16, 20);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-20.html b/testing/web-platform/tests/selection/addRange-20.html
new file mode 100644
index 0000000000..66ac5aec0d
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-20.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(20, 24);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-24.html b/testing/web-platform/tests/selection/addRange-24.html
new file mode 100644
index 0000000000..9804e1d908
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-24.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(24, 28);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-28.html b/testing/web-platform/tests/selection/addRange-28.html
new file mode 100644
index 0000000000..784002a336
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-28.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(28, 32);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-32.html b/testing/web-platform/tests/selection/addRange-32.html
new file mode 100644
index 0000000000..d599ec885f
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-32.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(32, 36);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-36.html b/testing/web-platform/tests/selection/addRange-36.html
new file mode 100644
index 0000000000..0dfe1b7a58
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-36.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(36, 40);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-40.html b/testing/web-platform/tests/selection/addRange-40.html
new file mode 100644
index 0000000000..8df437c84c
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-40.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(40, 44);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-44.html b/testing/web-platform/tests/selection/addRange-44.html
new file mode 100644
index 0000000000..37c1a83df3
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-44.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(44, 48);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-48.html b/testing/web-platform/tests/selection/addRange-48.html
new file mode 100644
index 0000000000..e219bc66e1
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-48.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(48, 52);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-52.html b/testing/web-platform/tests/selection/addRange-52.html
new file mode 100644
index 0000000000..0f687a6e3a
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-52.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(52, 56);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange-56.html b/testing/web-platform/tests/selection/addRange-56.html
new file mode 100644
index 0000000000..6313f8969e
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange-56.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.addRange() tests</title>
+<meta name="timeout" content="long">
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script src="addRange.js"></script>
+<script>
+"use strict";
+
+testAddRangeSubSet(56);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/addRange.htm b/testing/web-platform/tests/selection/addRange.htm
new file mode 100644
index 0000000000..4feb69f848
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Selection: Add a range to the selection</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+window.onload = function() {
+ test(function() {
+ var selection = window.getSelection();
+ var p1 = document.getElementById("p1");
+
+ var range = document.createRange();
+ range.selectNode(p1);
+ selection.addRange(range);
+
+ assert_equals(selection.anchorNode, range.startContainer);
+ assert_equals(selection.anchorOffset, range.startOffset);
+ assert_equals(selection.focusNode, range.endContainer);
+ assert_equals(selection.focusOffset, range.endOffset);
+ assert_equals(selection.isCollapsed, range.collapsed);
+ assert_equals(selection.rangeCount, 1);
+ assert_equals(selection.toString(), p1.firstChild.nodeValue);
+ });
+};
+</script>
+<div id=log></div>
+<p id="p1">Add a range to the selection</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/selection/addRange.js b/testing/web-platform/tests/selection/addRange.js
new file mode 100644
index 0000000000..7c443f7dad
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange.js
@@ -0,0 +1,205 @@
+"use strict";
+
+function testAddRange(exception, range, endpoints, qualifier, testName) {
+ if (!isSelectableNode(endpoints[0]) || !isSelectableNode(endpoints[2])) {
+ testAddRangeDoesNothing(exception, range, endpoints, qualifier, testName);
+ return;
+ }
+
+ test(function() {
+ assert_equals(exception, null, "Test setup must not throw exceptions");
+
+ selection.addRange(range);
+
+ assert_equals(range.startContainer, endpoints[0],
+ "addRange() must not modify the startContainer of the Range it's given");
+ assert_equals(range.startOffset, endpoints[1],
+ "addRange() must not modify the startOffset of the Range it's given");
+ assert_equals(range.endContainer, endpoints[2],
+ "addRange() must not modify the endContainer of the Range it's given");
+ assert_equals(range.endOffset, endpoints[3],
+ "addRange() must not modify the endOffset of the Range it's given");
+ }, testName + ": " + qualifier + " addRange() must not throw exceptions or modify the range it's given");
+
+ test(function() {
+ assert_equals(exception, null, "Test setup must not throw exceptions");
+
+ assert_equals(selection.rangeCount, 1, "rangeCount must be 1");
+ }, testName + ": " + qualifier + " addRange() must result in rangeCount being 1");
+
+ // From here on out we check selection.getRangeAt(selection.rangeCount - 1)
+ // so as not to double-fail Gecko.
+
+ test(function() {
+ assert_equals(exception, null, "Test setup must not throw exceptions");
+ assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0");
+
+ var newRange = selection.getRangeAt(selection.rangeCount - 1);
+
+ assert_not_equals(newRange, null,
+ "getRangeAt(rangeCount - 1) must not return null");
+ assert_equals(typeof newRange, "object",
+ "getRangeAt(rangeCount - 1) must return an object");
+
+ assert_equals(newRange.startContainer, range.startContainer,
+ "startContainer of the Selection's last Range must match the added Range");
+ assert_equals(newRange.startOffset, range.startOffset,
+ "startOffset of the Selection's last Range must match the added Range");
+ assert_equals(newRange.endContainer, range.endContainer,
+ "endContainer of the Selection's last Range must match the added Range");
+ assert_equals(newRange.endOffset, range.endOffset,
+ "endOffset of the Selection's last Range must match the added Range");
+ }, testName + ": " + qualifier + " addRange() must result in the selection's last range having the specified endpoints");
+
+ test(function() {
+ assert_equals(exception, null, "Test setup must not throw exceptions");
+ assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0");
+
+ assert_equals(selection.getRangeAt(selection.rangeCount - 1), range,
+ "getRangeAt(rangeCount - 1) must return the same object we added");
+ }, testName + ": " + qualifier + " addRange() must result in the selection's last range being the same object we added");
+
+ // Let's not test many different modifications -- one should be enough.
+ test(function() {
+ assert_equals(exception, null, "Test setup must not throw exceptions");
+ assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0");
+
+ if (range.startContainer == paras[0].firstChild
+ && range.startOffset == 0
+ && range.endContainer == paras[0].firstChild
+ && range.endOffset == 2) {
+ // Just in case . . .
+ range.setStart(paras[0].firstChild, 1);
+ } else {
+ range.setStart(paras[0].firstChild, 0);
+ range.setEnd(paras[0].firstChild, 2);
+ }
+
+ var newRange = selection.getRangeAt(selection.rangeCount - 1);
+
+ assert_equals(newRange.startContainer, range.startContainer,
+ "After mutating the " + qualifier + " added Range, startContainer of the Selection's last Range must match the added Range");
+ assert_equals(newRange.startOffset, range.startOffset,
+ "After mutating the " + qualifier + " added Range, startOffset of the Selection's last Range must match the added Range");
+ assert_equals(newRange.endContainer, range.endContainer,
+ "After mutating the " + qualifier + " added Range, endContainer of the Selection's last Range must match the added Range");
+ assert_equals(newRange.endOffset, range.endOffset,
+ "After mutating the " + qualifier + " added Range, endOffset of the Selection's last Range must match the added Range");
+ }, testName + ": modifying the " + qualifier + " added range must modify the Selection's last Range");
+
+ // Now test the other way too.
+ test(function() {
+ assert_equals(exception, null, "Test setup must not throw exceptions");
+ assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0");
+
+ var newRange = selection.getRangeAt(selection.rangeCount - 1);
+
+ if (newRange.startContainer == paras[0].firstChild
+ && newRange.startOffset == 4
+ && newRange.endContainer == paras[0].firstChild
+ && newRange.endOffset == 6) {
+ newRange.setStart(paras[0].firstChild, 5);
+ } else {
+ newRange.setStart(paras[0].firstChild, 4);
+ newRange.setStart(paras[0].firstChild, 6);
+ }
+
+ assert_equals(newRange.startContainer, range.startContainer,
+ "After " + qualifier + " addRange(), after mutating the Selection's last Range, startContainer of the Selection's last Range must match the added Range");
+ assert_equals(newRange.startOffset, range.startOffset,
+ "After " + qualifier + " addRange(), after mutating the Selection's last Range, startOffset of the Selection's last Range must match the added Range");
+ assert_equals(newRange.endContainer, range.endContainer,
+ "After " + qualifier + " addRange(), after mutating the Selection's last Range, endContainer of the Selection's last Range must match the added Range");
+ assert_equals(newRange.endOffset, range.endOffset,
+ "After " + qualifier + " addRange(), after mutating the Selection's last Range, endOffset of the Selection's last Range must match the added Range");
+ }, testName + ": modifying the Selection's last Range must modify the " + qualifier + " added Range");
+}
+
+function testAddRangeDoesNothing(exception, range, endpoints, qualifier, testName) {
+ test(function() {
+ assert_equals(exception, null, "Test setup must not throw exceptions");
+
+ assertSelectionNoChange(function() { selection.addRange(range); });
+ assert_equals(range.startContainer, endpoints[0],
+ "addRange() must not modify the startContainer of the Range it's given");
+ assert_equals(range.startOffset, endpoints[1],
+ "addRange() must not modify the startOffset of the Range it's given");
+ assert_equals(range.endContainer, endpoints[2],
+ "addRange() must not modify the endContainer of the Range it's given");
+ assert_equals(range.endOffset, endpoints[3],
+ "addRange() must not modify the endOffset of the Range it's given");
+ }, testName + ": " + qualifier + " addRange() must do nothing");
+}
+
+// Do only n evals, not n^2
+var testRangesEvaled = testRanges.map(eval);
+
+// Run a subset of all of addRange tests.
+// Huge number of tests in a single file causes problems. Each of
+// addRange-NN.html runs a part of them.
+//
+// startIndex - Start index in testRanges array
+// optionalEndIndex - End index in testRanges array + 1. If this argument is
+// omitted, testRanges.length is applied.
+function testAddRangeSubSet(startIndex, optionalEndIndex) {
+ var endIndex = optionalEndIndex === undefined ? testRanges.length : optionalEndIndex;
+ if (startIndex < 0 || startIndex >= testRanges.length)
+ throw "Sanity check: Specified index is invalid.";
+ if (endIndex < 0 || endIndex > testRanges.length)
+ throw "Sanity check: Specified index is invalid.";
+
+ for (var i = startIndex; i < endIndex; i++) {
+ for (var j = 0; j < testRanges.length; j++) {
+ var testName = "Range " + i + " " + testRanges[i]
+ + " followed by Range " + j + " " + testRanges[j];
+
+ var exception = null;
+ try {
+ selection.removeAllRanges();
+
+ var endpoints1 = testRangesEvaled[i];
+ var range1 = ownerDocument(endpoints1[0]).createRange();
+ range1.setStart(endpoints1[0], endpoints1[1]);
+ range1.setEnd(endpoints1[2], endpoints1[3]);
+
+ if (range1.startContainer !== endpoints1[0]) {
+ throw "Sanity check: the first Range we created must have the desired startContainer";
+ }
+ if (range1.startOffset !== endpoints1[1]) {
+ throw "Sanity check: the first Range we created must have the desired startOffset";
+ }
+ if (range1.endContainer !== endpoints1[2]) {
+ throw "Sanity check: the first Range we created must have the desired endContainer";
+ }
+ if (range1.endOffset !== endpoints1[3]) {
+ throw "Sanity check: the first Range we created must have the desired endOffset";
+ }
+
+ var endpoints2 = testRangesEvaled[j];
+ var range2 = ownerDocument(endpoints2[0]).createRange();
+ range2.setStart(endpoints2[0], endpoints2[1]);
+ range2.setEnd(endpoints2[2], endpoints2[3]);
+
+ if (range2.startContainer !== endpoints2[0]) {
+ throw "Sanity check: the second Range we created must have the desired startContainer";
+ }
+ if (range2.startOffset !== endpoints2[1]) {
+ throw "Sanity check: the second Range we created must have the desired startOffset";
+ }
+ if (range2.endContainer !== endpoints2[2]) {
+ throw "Sanity check: the second Range we created must have the desired endContainer";
+ }
+ if (range2.endOffset !== endpoints2[3]) {
+ throw "Sanity check: the second Range we created must have the desired endOffset";
+ }
+ } catch (e) {
+ exception = e;
+ }
+
+ testAddRange(exception, range1, endpoints1, "first", testName);
+ if (selection.rangeCount > 0)
+ testAddRangeDoesNothing(exception, range2, endpoints2, "second", testName);
+ }
+ }
+}
+
diff --git a/testing/web-platform/tests/selection/addRange.tentative.html b/testing/web-platform/tests/selection/addRange.tentative.html
new file mode 100644
index 0000000000..28e3b675d3
--- /dev/null
+++ b/testing/web-platform/tests/selection/addRange.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Selection: Add a range to the selection</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+window.onload = function() {
+ test(function() {
+ var selection = window.getSelection();
+ var p = document.querySelector("p");
+
+ var range = document.createRange();
+ range.selectNode(p);
+ selection.addRange(range);
+
+ // In Safari the anchorNode is the Text node, elsewhere it's the Element.
+ assert_equals(selection.anchorNode, document.body);
+ assert_equals(selection.anchorOffset, 1);
+ assert_equals(selection.focusNode, document.body);
+ assert_equals(selection.focusOffset, 2);
+ assert_equals(selection.isCollapsed, range.collapsed);
+ assert_equals(selection.rangeCount, 1);
+ });
+};
+</script>
+<body>
+ <p>Add a range to the selection</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/selection/anonymous/details-ancestor.html b/testing/web-platform/tests/selection/anonymous/details-ancestor.html
new file mode 100644
index 0000000000..b0adfa6890
--- /dev/null
+++ b/testing/web-platform/tests/selection/anonymous/details-ancestor.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Selecting internal node</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>
+<link rel="stylesheet" href="/fonts/ahem.css">
+<style>
+ details {
+ font: 16px/1 Ahem;
+ }
+</style>
+<details id="details"></details>
+<script>
+promise_test(async () => {
+ await new test_driver.Actions()
+ .pointerMove(5, 5, {origin: details})
+ .pointerDown()
+ .pointerMove(50, 50)
+ .pointerUp()
+ .send();
+ const selection = getSelection();
+
+ if (selection.anchorNode === null) {
+ // <details> is not selectable, which is acceptable
+ return;
+ }
+
+ // Gecko throws when accessing any property from DOM-invisible node
+ // so check we can access something
+ assert_equals(selection.anchorNode.constructor.name, "HTMLDetailsElement");
+ assert_equals(selection.anchorOffset, 0);
+ // Gecko limits the selection inside <details> while Blink does not
+ // so check something general
+ assert_equals(selection.focusNode.nodeType, Node.ELEMENT_NODE);
+ assert_equals(selection.focusOffset, 0);
+}, "Selecting the default summary of <details> should report a DOM-visible ancestor");
+</script>
diff --git a/testing/web-platform/tests/selection/anonymous/details-mutate.html b/testing/web-platform/tests/selection/anonymous/details-mutate.html
new file mode 100644
index 0000000000..3cd5475112
--- /dev/null
+++ b/testing/web-platform/tests/selection/anonymous/details-mutate.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test selecting anonymous summary element inside details</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<link rel="stylesheet" href="/fonts/ahem.css">
+<style>
+ details {
+ font: 16px/1 Ahem;
+ }
+</style>
+<details id="d"></details>
+<script>
+ const selection = getSelection();
+ d.addEventListener("DOMSubtreeModified", ev => {
+ window.find("cthulu", true, false);
+ selection.extend(document.body, document.body.childNodes.length);
+ });
+
+ window.onload = () => {
+ promise_test(async () => {
+ // This enables `.extend()`
+ selection.collapse(document.body);
+ // Clicking implicitly selects an anonymous summary element inside <details>
+ await new test_driver.Actions()
+ .pointerMove(0, 0, { origin: d })
+ .pointerDown()
+ .pointerUp()
+ .send();
+ // FIXME: Convert this to a crash test. Currently test_driver actions
+ // do not work within crash tests.
+ assert_true(true, "No crash");
+ }, "Manipulating selection after clicking <details> shouldn't cause a crash");
+ };
+</script>
diff --git a/testing/web-platform/tests/selection/bidi/modify.tentative.html b/testing/web-platform/tests/selection/bidi/modify.tentative.html
new file mode 100644
index 0000000000..6029cf423a
--- /dev/null
+++ b/testing/web-platform/tests/selection/bidi/modify.tentative.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Selection#modify bidi tests</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<div id="container">
+ <div title="LTR text">Hello World</div>
+ <div title="RTL text">مرحبا عالم</div>
+ <div title="RTL+LTR text">مرحبا عالم Hello World</div>
+ <div title="LTR+RTL text">Hello World مرحبا عالم</div>
+ <div title="LTR+RTL+LTR text">Hello World مرحبا عالم Hello World</div>
+ <div title="RTL+LTR+RTL text">مرحبا عالم Hello World مرحبا عالم</div>
+</div>
+
+<script>
+/**
+ * @param {"ltr" | "rtl"} bidi
+ * @param {"left" | "right"} direction
+ * @param {number} length
+ */
+function getExpectedOffset(bidi, direction, length) {
+ const isLtr = bidi === "ltr";
+ const toLeft = direction === "left";
+ return isLtr === toLeft ? 0 : length;
+}
+
+function runTest(div, direction, bidi, postfix = "") {
+ test(() => {
+ div.dir = bidi;
+
+ selection.collapse(div);
+ selection.modify("extend", direction, "lineboundary");
+
+ const offset = getExpectedOffset(
+ bidi, direction,
+ div.childNodes[0].textContent.length
+ );
+
+ assert_equals(selection.focusOffset, offset);
+ }, `${div.title} with ${direction} direction in ${bidi} context${postfix}`);
+}
+
+const selection = getSelection();
+for (const bidi of ["ltr", "rtl"]) {
+ for (const direction of ["left", "right"]) {
+ for (const div of container.children) {
+ runTest(div, direction, bidi)
+ }
+ }
+}
+
+// Gecko treats morphed LTR contexts differently from native LTR context
+// @see https://searchfox.org/mozilla-central/rev/35d927df97900a57ecb562ad13909e392440b0fb/dom/base/Document.h#981-987
+for (const direction of ["left", "right"]) {
+ for (const div of container.children) {
+ runTest(div, direction, "ltr", " (which was previously rtl)")
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/selection/caret/collapse-pre-linestart-1.html b/testing/web-platform/tests/selection/caret/collapse-pre-linestart-1.html
new file mode 100644
index 0000000000..6863456f06
--- /dev/null
+++ b/testing/web-platform/tests/selection/caret/collapse-pre-linestart-1.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>selection.collapse() to line start</title>
+<link rel="author" title="Kagami Sascha Rosylight" href="mailto:krosylight@mozilla.com">
+<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-collapse">
+<link rel="match" href="collapse-pre-linestart-ref.html">
+<meta name="assert" content="The caret must appear at the start of the second line.">
+<pre id=target contenteditable>ABC
+<br></pre>
+<script>
+ getSelection().collapse(target.childNodes[0], 4);
+</script>
diff --git a/testing/web-platform/tests/selection/caret/collapse-pre-linestart-2.html b/testing/web-platform/tests/selection/caret/collapse-pre-linestart-2.html
new file mode 100644
index 0000000000..ac119cbb31
--- /dev/null
+++ b/testing/web-platform/tests/selection/caret/collapse-pre-linestart-2.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>selection.collapse() to line start</title>
+<link rel="author" title="Kagami Sascha Rosylight" href="mailto:krosylight@mozilla.com">
+<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-collapse">
+<link rel="match" href="collapse-pre-linestart-ref.html">
+<meta name="assert" content="The caret must appear at the start of the second line.">
+<pre id=target contenteditable>ABC
+
+</pre>
+<script>
+ getSelection().collapse(target.childNodes[0], 4);
+</script>
diff --git a/testing/web-platform/tests/selection/caret/collapse-pre-linestart-ref.html b/testing/web-platform/tests/selection/caret/collapse-pre-linestart-ref.html
new file mode 100644
index 0000000000..2b25941ded
--- /dev/null
+++ b/testing/web-platform/tests/selection/caret/collapse-pre-linestart-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>selection.collapse() to line start</title>
+<pre id=target contenteditable>ABC<br><br></pre>
+<script>
+ target.focus();
+ getSelection().collapse(target, 3);
+</script>
diff --git a/testing/web-platform/tests/selection/caret/empty-elements.html b/testing/web-platform/tests/selection/caret/empty-elements.html
new file mode 100644
index 0000000000..328188c957
--- /dev/null
+++ b/testing/web-platform/tests/selection/caret/empty-elements.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Placing selection inside empty elements</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div contenteditable id="host">
+ <p><strong id="strong"></strong></p>
+</div>
+<script>
+test( () => {
+ const range = document.createRange();
+
+ range.setStart( strong, 0 );
+ range.collapse()
+ getSelection().removeAllRanges();
+ getSelection().addRange( range );
+
+ const selectedRange = getSelection().getRangeAt( 0 );
+
+ assert_equals( selectedRange.startContainer, strong );
+ assert_equals( selectedRange.startOffset, 0 );
+ assert_equals( selectedRange.endContainer, strong );
+ assert_equals( selectedRange.endOffset, 0 );
+}, 'Selection can be placed inside the empty element' );
+</script>
diff --git a/testing/web-platform/tests/selection/collapse-00.html b/testing/web-platform/tests/selection/collapse-00.html
new file mode 100644
index 0000000000..6adaca4002
--- /dev/null
+++ b/testing/web-platform/tests/selection/collapse-00.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.collapse()/setPosition() tests</title>
+<meta name=timeout content=long>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script src=collapse.js></script>
+<script>
+"use strict";
+
+testCollapseSubSet(0, 15);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/collapse-15.html b/testing/web-platform/tests/selection/collapse-15.html
new file mode 100644
index 0000000000..377e504434
--- /dev/null
+++ b/testing/web-platform/tests/selection/collapse-15.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.collapse()/setPosition() tests</title>
+<meta name=timeout content=long>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script src=collapse.js></script>
+<script>
+"use strict";
+
+testCollapseSubSet(15, 30);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/collapse-30.html b/testing/web-platform/tests/selection/collapse-30.html
new file mode 100644
index 0000000000..376633d910
--- /dev/null
+++ b/testing/web-platform/tests/selection/collapse-30.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.collapse()/setPosition() tests</title>
+<meta name=timeout content=long>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script src=collapse.js></script>
+<script>
+"use strict";
+
+testCollapseSubSet(30);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/collapse-45.html b/testing/web-platform/tests/selection/collapse-45.html
new file mode 100644
index 0000000000..28eee4eb79
--- /dev/null
+++ b/testing/web-platform/tests/selection/collapse-45.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Selection.collapse()/setPosition() tests</title>
+<meta name=timeout content=long>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script src=collapse.js></script>
+<script>
+"use strict";
+
+testCollapseSubSet(30, 45);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/collapse.htm b/testing/web-platform/tests/selection/collapse.htm
new file mode 100644
index 0000000000..78fb779c4f
--- /dev/null
+++ b/testing/web-platform/tests/selection/collapse.htm
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Selection: Collapse the selection using collapse()</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+window.onload = function() {
+ test(function() {
+ var selection = window.getSelection();
+ var p1 = document.getElementById("p1");
+
+ var range = document.createRange();
+ range.selectNode(p1);
+ selection.addRange(range);
+ selection.collapse(p1, 0);
+
+ assert_equals(selection.anchorNode, p1);
+ assert_equals(selection.anchorOffset, 0);
+ assert_equals(selection.focusNode, p1);
+ assert_equals(selection.focusOffset, 0);
+ assert_equals(selection.isCollapsed, true);
+ assert_equals(selection.rangeCount, 1);
+ assert_equals(selection.toString(), "");
+ });
+};
+</script>
+<div id=log></div>
+<p id="p1">Add a range to the selection</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/selection/collapse.js b/testing/web-platform/tests/selection/collapse.js
new file mode 100644
index 0000000000..4a816f4956
--- /dev/null
+++ b/testing/web-platform/tests/selection/collapse.js
@@ -0,0 +1,103 @@
+"use strict";
+
+function testCollapse(range, point, method) {
+ selection.removeAllRanges();
+ var addedRange;
+ if (range) {
+ addedRange = range.cloneRange();
+ selection.addRange(addedRange);
+ }
+
+ if (point[0].nodeType == Node.DOCUMENT_TYPE_NODE) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function() {
+ selection[method](point[0], point[1]);
+ }, "Must throw INVALID_NODE_TYPE_ERR when " + method + "()ing if the node is a DocumentType");
+ return;
+ }
+
+ if (point[1] < 0 || point[1] > getNodeLength(point[0])) {
+ assert_throws_dom("INDEX_SIZE_ERR", function() {
+ selection[method](point[0], point[1]);
+ }, "Must throw INDEX_SIZE_ERR when " + method + "()ing if the offset is negative or greater than the node's length");
+ return;
+ }
+
+ if (!document.contains(point[0])) {
+ assertSelectionNoChange(function() {
+ selection[method](point[0], point[1]);
+ });
+ return;
+ }
+
+ selection[method](point[0], point[1]);
+
+ assert_equals(selection.rangeCount, 1,
+ "selection.rangeCount must equal 1 after " + method + "()");
+ assert_equals(selection.focusNode, point[0],
+ "focusNode must equal the node we " + method + "()d to");
+ assert_equals(selection.focusOffset, point[1],
+ "focusOffset must equal the offset we " + method + "()d to");
+ assert_equals(selection.focusNode, selection.anchorNode,
+ "focusNode and anchorNode must be equal after " + method + "()");
+ assert_equals(selection.focusOffset, selection.anchorOffset,
+ "focusOffset and anchorOffset must be equal after " + method + "()");
+ if (range) {
+ assert_equals(addedRange.startContainer, range.startContainer,
+ method + "() must not change the startContainer of a preexisting Range");
+ assert_equals(addedRange.endContainer, range.endContainer,
+ method + "() must not change the endContainer of a preexisting Range");
+ assert_equals(addedRange.startOffset, range.startOffset,
+ method + "() must not change the startOffset of a preexisting Range");
+ assert_equals(addedRange.endOffset, range.endOffset,
+ method + "() must not change the endOffset of a preexisting Range");
+ }
+}
+
+// Also test a selection with no ranges
+testRanges.unshift("[]");
+
+// Don't want to eval() each point a bazillion times
+var testPointsCached = [];
+for (var i = 0; i < testPoints.length; i++) {
+ testPointsCached.push(eval(testPoints[i]));
+}
+
+// Run a subset of all of collapse tests.
+// Huge number of tests in a single file causes problems. Each of
+// collapse-NN.html runs a part of them.
+//
+// startIndex - Start index in testRanges array
+// optionalEndIndex - End index in testRanges array + 1. If this argument is
+// omitted, testRanges.length is applied.
+function testCollapseSubSet(startIndex, optionalEndIndex) {
+ var endIndex = optionalEndIndex === undefined ? testRanges.length : optionalEndIndex;
+ if (startIndex < 0 || startIndex >= testRanges.length)
+ throw "Sanity check: Specified index is invalid.";
+ if (endIndex < 0 || endIndex > testRanges.length)
+ throw "Sanity check: Specified index is invalid.";
+
+ var tests = [];
+ for (var i = startIndex; i < endIndex; i++) {
+ var endpoints = eval(testRanges[i]);
+ var range;
+ test(function() {
+ if (endpoints.length) {
+ range = ownerDocument(endpoints[0]).createRange();
+ range.setStart(endpoints[0], endpoints[1]);
+ range.setEnd(endpoints[2], endpoints[3]);
+ } else {
+ // Empty selection
+ range = null;
+ }
+ }, "Set up range " + i + " " + testRanges[i]);
+ for (var j = 0; j < testPoints.length; j++) {
+ tests.push(["collapse() on " + testRanges[i] + " to " + testPoints[j],
+ range, testPointsCached[j], "collapse"]);
+ tests.push(["setPosition() on " + testRanges[i] + " to " + testPoints[j],
+ range, testPointsCached[j], "setPosition"]);
+ }
+ }
+
+ generate_tests(testCollapse, tests);
+}
+
diff --git a/testing/web-platform/tests/selection/collapseToStartEnd.html b/testing/web-platform/tests/selection/collapseToStartEnd.html
new file mode 100644
index 0000000000..c2bba3af18
--- /dev/null
+++ b/testing/web-platform/tests/selection/collapseToStartEnd.html
@@ -0,0 +1,114 @@
+<!doctype html>
+<title>Selection.collapseTo(Start|End)() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+test(function() {
+ selection.removeAllRanges();
+ assert_throws_dom("INVALID_STATE_ERR", function() {
+ selection.collapseToStart();
+ });
+}, "Must throw InvalidStateErr if the selection's range is null");
+
+for (var i = 0; i < testRanges.length; i++) {
+ var endpoints = eval(testRanges[i]);
+ if (!isSelectableNode(endpoints[0]) || !isSelectableNode(endpoints[2]))
+ continue;
+ test(function() {
+ selection.removeAllRanges();
+
+ var addedRange = ownerDocument(endpoints[0]).createRange();
+ addedRange.setStart(endpoints[0], endpoints[1]);
+ addedRange.setEnd(endpoints[2], endpoints[3]);
+ selection.addRange(addedRange);
+
+ // We don't penalize browsers here for mishandling addRange() and
+ // adding a different range than we specified. They fail addRange()
+ // tests for that, and don't have to fail collapseToStart/End() tests
+ // too. They do fail if they throw unexpectedly, though. I also fail
+ // them if there's no range at all, because otherwise they could pass
+ // all tests if addRange() always does nothing and collapseToStart()
+ // always throws.
+ assert_equals(selection.rangeCount, 1,
+ "Sanity check: rangeCount must equal 1 after addRange()");
+
+ var expectedEndpoint = [
+ selection.getRangeAt(0).startContainer,
+ selection.getRangeAt(0).startOffset
+ ];
+
+ selection.collapseToStart();
+
+ assert_equals(selection.rangeCount, 1,
+ "selection.rangeCount must equal 1");
+ assert_equals(selection.focusNode, expectedEndpoint[0],
+ "focusNode must equal the original start node");
+ assert_equals(selection.focusOffset, expectedEndpoint[1],
+ "focusOffset must equal the original start offset");
+ assert_equals(selection.anchorNode, expectedEndpoint[0],
+ "anchorNode must equal the original start node");
+ assert_equals(selection.anchorOffset, expectedEndpoint[1],
+ "anchorOffset must equal the original start offset");
+ assert_equals(addedRange.startContainer, endpoints[0],
+ "collapseToStart() must not change the startContainer of the selection's original range");
+ assert_equals(addedRange.startOffset, endpoints[1],
+ "collapseToStart() must not change the startOffset of the selection's original range");
+ assert_equals(addedRange.endContainer, endpoints[2],
+ "collapseToStart() must not change the endContainer of the selection's original range");
+ assert_equals(addedRange.endOffset, endpoints[3],
+ "collapseToStart() must not change the endOffset of the selection's original range");
+ }, "Range " + i + " " + testRanges[i] + " collapseToStart()");
+
+ // Copy-paste of above
+ test(function() {
+ selection.removeAllRanges();
+
+ var addedRange = ownerDocument(endpoints[0]).createRange();
+ addedRange.setStart(endpoints[0], endpoints[1]);
+ addedRange.setEnd(endpoints[2], endpoints[3]);
+ selection.addRange(addedRange);
+
+ // We don't penalize browsers here for mishandling addRange() and
+ // adding a different range than we specified. They fail addRange()
+ // tests for that, and don't have to fail collapseToStart/End() tests
+ // too. They do fail if they throw unexpectedly, though. I also fail
+ // them if there's no range at all, because otherwise they could pass
+ // all tests if addRange() always does nothing and collapseToStart()
+ // always throws.
+ assert_equals(selection.rangeCount, 1,
+ "Sanity check: rangeCount must equal 1 after addRange()");
+
+ var expectedEndpoint = [
+ selection.getRangeAt(0).endContainer,
+ selection.getRangeAt(0).endOffset
+ ];
+
+ selection.collapseToEnd();
+
+ assert_equals(selection.rangeCount, 1,
+ "selection.rangeCount must equal 1");
+ assert_equals(selection.focusNode, expectedEndpoint[0],
+ "focusNode must equal the original end node");
+ assert_equals(selection.focusOffset, expectedEndpoint[1],
+ "focusOffset must equal the original end offset");
+ assert_equals(selection.anchorNode, expectedEndpoint[0],
+ "anchorNode must equal the original end node");
+ assert_equals(selection.anchorOffset, expectedEndpoint[1],
+ "anchorOffset must equal the original end offset");
+ assert_equals(addedRange.startContainer, endpoints[0],
+ "collapseToEnd() must not change the startContainer of the selection's original range");
+ assert_equals(addedRange.startOffset, endpoints[1],
+ "collapseToEnd() must not change the startOffset of the selection's original range");
+ assert_equals(addedRange.endContainer, endpoints[2],
+ "collapseToEnd() must not change the endContainer of the selection's original range");
+ assert_equals(addedRange.endOffset, endpoints[3],
+ "collapseToEnd() must not change the endOffset of the selection's original range");
+ }, "Range " + i + " " + testRanges[i] + " collapseToEnd()");
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/common.js b/testing/web-platform/tests/selection/common.js
new file mode 100644
index 0000000000..3bdc509462
--- /dev/null
+++ b/testing/web-platform/tests/selection/common.js
@@ -0,0 +1,1002 @@
+"use strict";
+// TODO: iframes, contenteditable/designMode
+
+// Everything is done in functions in this test harness, so we have to declare
+// all the variables before use to make sure they can be reused.
+var selection;
+var testDiv, paras, detachedDiv, detachedPara1, detachedPara2,
+ foreignDoc, foreignPara1, foreignPara2, xmlDoc, xmlElement,
+ detachedXmlElement, detachedTextNode, foreignTextNode,
+ detachedForeignTextNode, xmlTextNode, detachedXmlTextNode,
+ processingInstruction, detachedProcessingInstruction, comment,
+ detachedComment, foreignComment, detachedForeignComment, xmlComment,
+ detachedXmlComment, docfrag, foreignDocfrag, xmlDocfrag, doctype,
+ foreignDoctype, xmlDoctype;
+var testRanges, testPoints, testNodes;
+
+function setupRangeTests() {
+ selection = getSelection();
+ testDiv = document.querySelector("#test");
+ if (testDiv) {
+ testDiv.parentNode.removeChild(testDiv);
+ }
+ testDiv = document.createElement("div");
+ testDiv.id = "test";
+ document.body.insertBefore(testDiv, document.body.firstChild);
+ // Test some diacritics, to make sure browsers are using code units here
+ // and not something like grapheme clusters.
+ testDiv.innerHTML = "<p id=a>A&#x308;b&#x308;c&#x308;d&#x308;e&#x308;f&#x308;g&#x308;h&#x308;\n"
+ + "<p id=b style=display:none>Ijklmnop\n"
+ + "<p id=c>Qrstuvwx"
+ + "<p id=d style=display:none>Yzabcdef"
+ + "<p id=e style=display:none>Ghijklmn";
+ paras = testDiv.querySelectorAll("p");
+
+ detachedDiv = document.createElement("div");
+ detachedPara1 = document.createElement("p");
+ detachedPara1.appendChild(document.createTextNode("Opqrstuv"));
+ detachedPara2 = document.createElement("p");
+ detachedPara2.appendChild(document.createTextNode("Wxyzabcd"));
+ detachedDiv.appendChild(detachedPara1);
+ detachedDiv.appendChild(detachedPara2);
+
+ // Opera doesn't automatically create a doctype for a new HTML document,
+ // contrary to spec. It also doesn't let you add doctypes to documents
+ // after the fact through any means I've tried. So foreignDoc in Opera
+ // will have no doctype, foreignDoctype will be null, and Opera will fail
+ // some tests somewhat mysteriously as a result.
+ foreignDoc = document.implementation.createHTMLDocument("");
+ foreignPara1 = foreignDoc.createElement("p");
+ foreignPara1.appendChild(foreignDoc.createTextNode("Efghijkl"));
+ foreignPara2 = foreignDoc.createElement("p");
+ foreignPara2.appendChild(foreignDoc.createTextNode("Mnopqrst"));
+ foreignDoc.body.appendChild(foreignPara1);
+ foreignDoc.body.appendChild(foreignPara2);
+
+ // Now we get to do really silly stuff, which nobody in the universe is
+ // ever going to actually do, but the spec defines behavior, so too bad.
+ // Testing is fun!
+ xmlDoctype = document.implementation.createDocumentType("qorflesnorf", "abcde", "x\"'y");
+ xmlDoc = document.implementation.createDocument(null, null, xmlDoctype);
+ detachedXmlElement = xmlDoc.createElement("everyone-hates-hyphenated-element-names");
+ detachedTextNode = document.createTextNode("Uvwxyzab");
+ detachedForeignTextNode = foreignDoc.createTextNode("Cdefghij");
+ detachedXmlTextNode = xmlDoc.createTextNode("Klmnopqr");
+ // PIs only exist in XML documents, so don't bother with document or
+ // foreignDoc.
+ detachedProcessingInstruction = xmlDoc.createProcessingInstruction("whippoorwill", "chirp chirp chirp");
+ detachedComment = document.createComment("Stuvwxyz");
+ // Hurrah, we finally got to "z" at the end!
+ detachedForeignComment = foreignDoc.createComment("אריה יהודה");
+ detachedXmlComment = xmlDoc.createComment("בן חיים אליעזר");
+
+ // We should also test with document fragments that actually contain stuff
+ // . . . but, maybe later.
+ docfrag = document.createDocumentFragment();
+ foreignDocfrag = foreignDoc.createDocumentFragment();
+ xmlDocfrag = xmlDoc.createDocumentFragment();
+
+ xmlElement = xmlDoc.createElement("igiveuponcreativenames");
+ xmlTextNode = xmlDoc.createTextNode("do re mi fa so la ti");
+ xmlElement.appendChild(xmlTextNode);
+ processingInstruction = xmlDoc.createProcessingInstruction("somePI", 'Did you know that ":syn sync fromstart" is very useful when using vim to edit large amounts of JavaScript embedded in HTML?');
+ xmlDoc.appendChild(xmlElement);
+ xmlDoc.appendChild(processingInstruction);
+ xmlComment = xmlDoc.createComment("I maliciously created a comment that will break incautious XML serializers, but Firefox threw an exception, so all I got was this lousy T-shirt");
+ xmlDoc.appendChild(xmlComment);
+
+ comment = document.createComment("Alphabet soup?");
+ testDiv.appendChild(comment);
+
+ foreignComment = foreignDoc.createComment('"Commenter" and "commentator" mean different things. I\'ve seen non-native speakers trip up on this.');
+ foreignDoc.appendChild(foreignComment);
+ foreignTextNode = foreignDoc.createTextNode("I admit that I harbor doubts about whether we really need so many things to test, but it's too late to stop now.");
+ foreignDoc.body.appendChild(foreignTextNode);
+
+ doctype = document.doctype;
+ foreignDoctype = foreignDoc.doctype;
+
+ // Chromium project has a limitation of text file size, and it is applied to
+ // test result documents too. Generating tests with testRanges or
+ // testPoints can exceed the limitation easily. Some tests were split into
+ // multiple files such as addRange-NN.html. If you add more ranges, points,
+ // or tests, a Chromium project member might split affected tests.
+ //
+ // In selection/, a rough estimation of the limit is 4,000 test() functions
+ // per a file.
+ testRanges = [
+ // Various ranges within the text node children of different
+ // paragraphs. All should be valid.
+ "[paras[0].firstChild, 0, paras[0].firstChild, 0]",
+ "[paras[0].firstChild, 0, paras[0].firstChild, 1]",
+ "[paras[0].firstChild, 2, paras[0].firstChild, 8]",
+ "[paras[0].firstChild, 2, paras[0].firstChild, 9]",
+ "[paras[1].firstChild, 0, paras[1].firstChild, 0]",
+ "[paras[1].firstChild, 0, paras[1].firstChild, 1]",
+ "[paras[1].firstChild, 2, paras[1].firstChild, 8]",
+ "[paras[1].firstChild, 2, paras[1].firstChild, 9]",
+ "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]",
+ "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]",
+ "[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]",
+ "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]",
+ "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]",
+ "[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]",
+ // Now try testing some elements, not just text nodes.
+ "[document.documentElement, 0, document.documentElement, 1]",
+ "[document.documentElement, 0, document.documentElement, 2]",
+ "[document.documentElement, 1, document.documentElement, 2]",
+ "[document.head, 1, document.head, 1]",
+ "[document.body, 0, document.body, 1]",
+ "[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]",
+ "[foreignDoc.head, 1, foreignDoc.head, 1]",
+ "[foreignDoc.body, 0, foreignDoc.body, 0]",
+ "[paras[0], 0, paras[0], 0]",
+ "[paras[0], 0, paras[0], 1]",
+ "[detachedPara1, 0, detachedPara1, 0]",
+ "[detachedPara1, 0, detachedPara1, 1]",
+ // Now try some ranges that span elements.
+ "[paras[0].firstChild, 0, paras[1].firstChild, 0]",
+ "[paras[0].firstChild, 0, paras[1].firstChild, 8]",
+ "[paras[0].firstChild, 3, paras[3], 1]",
+ // How about something that spans a node and its descendant?
+ "[paras[0], 0, paras[0].firstChild, 7]",
+ "[testDiv, 2, paras[4], 1]",
+ "[testDiv, 1, paras[2].firstChild, 5]",
+ "[document.documentElement, 1, document.body, 0]",
+ "[foreignDoc.documentElement, 1, foreignDoc.body, 0]",
+ // Then a few more interesting things just for good measure.
+ "[document, 0, document, 1]",
+ "[document, 0, document, 2]",
+ "[document, 1, document, 2]",
+ "[testDiv, 0, comment, 5]",
+ "[paras[2].firstChild, 4, comment, 2]",
+ "[paras[3], 1, comment, 8]",
+ "[foreignDoc, 0, foreignDoc, 0]",
+ "[foreignDoc, 1, foreignComment, 2]",
+ "[foreignDoc.body, 0, foreignTextNode, 36]",
+ "[xmlDoc, 0, xmlDoc, 0]",
+ // Opera 11 crashes if you extractContents() a range that ends at offset
+ // zero in a comment. Comment out this line to run the tests successfully.
+ "[xmlDoc, 1, xmlComment, 0]",
+ "[detachedTextNode, 0, detachedTextNode, 8]",
+ "[detachedForeignTextNode, 7, detachedForeignTextNode, 7]",
+ "[detachedForeignTextNode, 0, detachedForeignTextNode, 8]",
+ "[detachedXmlTextNode, 7, detachedXmlTextNode, 7]",
+ "[detachedXmlTextNode, 0, detachedXmlTextNode, 8]",
+ "[detachedComment, 3, detachedComment, 4]",
+ "[detachedComment, 5, detachedComment, 5]",
+ "[detachedForeignComment, 0, detachedForeignComment, 1]",
+ "[detachedForeignComment, 4, detachedForeignComment, 4]",
+ "[detachedXmlComment, 2, detachedXmlComment, 6]",
+ "[docfrag, 0, docfrag, 0]",
+ "[foreignDocfrag, 0, foreignDocfrag, 0]",
+ "[xmlDocfrag, 0, xmlDocfrag, 0]",
+ ];
+
+ testPoints = [
+ // Various positions within the page, some invalid. Remember that
+ // paras[0] is visible, and paras[1] is display: none.
+ "[paras[0].firstChild, -1]",
+ "[paras[0].firstChild, 0]",
+ "[paras[0].firstChild, 1]",
+ "[paras[0].firstChild, 2]",
+ "[paras[0].firstChild, 8]",
+ "[paras[0].firstChild, 9]",
+ "[paras[0].firstChild, 10]",
+ "[paras[0].firstChild, 65535]",
+ "[paras[1].firstChild, -1]",
+ "[paras[1].firstChild, 0]",
+ "[paras[1].firstChild, 1]",
+ "[paras[1].firstChild, 2]",
+ "[paras[1].firstChild, 8]",
+ "[paras[1].firstChild, 9]",
+ "[paras[1].firstChild, 10]",
+ "[paras[1].firstChild, 65535]",
+ "[detachedPara1.firstChild, 0]",
+ "[detachedPara1.firstChild, 1]",
+ "[detachedPara1.firstChild, 8]",
+ "[detachedPara1.firstChild, 9]",
+ "[foreignPara1.firstChild, 0]",
+ "[foreignPara1.firstChild, 1]",
+ "[foreignPara1.firstChild, 8]",
+ "[foreignPara1.firstChild, 9]",
+ // Now try testing some elements, not just text nodes.
+ "[document.documentElement, -1]",
+ "[document.documentElement, 0]",
+ "[document.documentElement, 1]",
+ "[document.documentElement, 2]",
+ "[document.documentElement, 7]",
+ "[document.head, 1]",
+ "[document.body, 3]",
+ "[foreignDoc.documentElement, 0]",
+ "[foreignDoc.documentElement, 1]",
+ "[foreignDoc.head, 0]",
+ "[foreignDoc.body, 1]",
+ "[paras[0], 0]",
+ "[paras[0], 1]",
+ "[paras[0], 2]",
+ "[paras[1], 0]",
+ "[paras[1], 1]",
+ "[paras[1], 2]",
+ "[detachedPara1, 0]",
+ "[detachedPara1, 1]",
+ "[testDiv, 0]",
+ "[testDiv, 3]",
+ // Then a few more interesting things just for good measure.
+ "[document, -1]",
+ "[document, 0]",
+ "[document, 1]",
+ "[document, 2]",
+ "[document, 3]",
+ "[comment, -1]",
+ "[comment, 0]",
+ "[comment, 4]",
+ "[comment, 96]",
+ "[foreignDoc, 0]",
+ "[foreignDoc, 1]",
+ "[foreignComment, 2]",
+ "[foreignTextNode, 0]",
+ "[foreignTextNode, 36]",
+ "[xmlDoc, -1]",
+ "[xmlDoc, 0]",
+ "[xmlDoc, 1]",
+ "[xmlDoc, 5]",
+ "[xmlComment, 0]",
+ "[xmlComment, 4]",
+ "[processingInstruction, 0]",
+ "[processingInstruction, 5]",
+ "[processingInstruction, 9]",
+ "[detachedTextNode, 0]",
+ "[detachedTextNode, 8]",
+ "[detachedForeignTextNode, 0]",
+ "[detachedForeignTextNode, 8]",
+ "[detachedXmlTextNode, 0]",
+ "[detachedXmlTextNode, 8]",
+ "[detachedProcessingInstruction, 12]",
+ "[detachedComment, 3]",
+ "[detachedComment, 5]",
+ "[detachedForeignComment, 0]",
+ "[detachedForeignComment, 4]",
+ "[detachedXmlComment, 2]",
+ "[docfrag, 0]",
+ "[foreignDocfrag, 0]",
+ "[xmlDocfrag, 0]",
+ "[doctype, 0]",
+ "[doctype, -17]",
+ "[doctype, 1]",
+ "[foreignDoctype, 0]",
+ "[xmlDoctype, 0]",
+ ];
+
+ testNodes = [
+ "paras[0]",
+ "paras[0].firstChild",
+ "paras[1]",
+ "paras[1].firstChild",
+ "foreignPara1",
+ "foreignPara1.firstChild",
+ "detachedPara1",
+ "detachedPara1.firstChild",
+ "detachedPara1",
+ "detachedPara1.firstChild",
+ "testDiv",
+ "document",
+ "detachedDiv",
+ "detachedPara2",
+ "foreignDoc",
+ "foreignPara2",
+ "xmlDoc",
+ "xmlElement",
+ "detachedXmlElement",
+ "detachedTextNode",
+ "foreignTextNode",
+ "detachedForeignTextNode",
+ "xmlTextNode",
+ "detachedXmlTextNode",
+ "processingInstruction",
+ "detachedProcessingInstruction",
+ "comment",
+ "detachedComment",
+ "foreignComment",
+ "detachedForeignComment",
+ "xmlComment",
+ "detachedXmlComment",
+ "docfrag",
+ "foreignDocfrag",
+ "xmlDocfrag",
+ "doctype",
+ "foreignDoctype",
+ "xmlDoctype",
+ ];
+}
+if ("setup" in window) {
+ setup(setupRangeTests);
+} else {
+ // Presumably we're running from within an iframe or something
+ setupRangeTests();
+}
+
+/**
+ * Return the length of a node as specified in DOM Range.
+ */
+function getNodeLength(node) {
+ if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ return 0;
+ }
+ if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INSTRUCTION_NODE || node.nodeType == Node.COMMENT_NODE) {
+ return node.length;
+ }
+ return node.childNodes.length;
+}
+
+/**
+ * Returns the furthest ancestor of a Node as defined by the spec.
+ */
+function furthestAncestor(node) {
+ var root = node;
+ while (root.parentNode != null) {
+ root = root.parentNode;
+ }
+ return root;
+}
+
+/**
+ * "The ancestor containers of a Node are the Node itself and all its
+ * ancestors."
+ *
+ * Is node1 an ancestor container of node2?
+ */
+function isAncestorContainer(node1, node2) {
+ return node1 == node2 ||
+ (node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS);
+}
+
+/**
+ * Returns the first Node that's after node in tree order, or null if node is
+ * the last Node.
+ */
+function nextNode(node) {
+ if (node.hasChildNodes()) {
+ return node.firstChild;
+ }
+ return nextNodeDescendants(node);
+}
+
+/**
+ * Returns the last Node that's before node in tree order, or null if node is
+ * the first Node.
+ */
+function previousNode(node) {
+ if (node.previousSibling) {
+ node = node.previousSibling;
+ while (node.hasChildNodes()) {
+ node = node.lastChild;
+ }
+ return node;
+ }
+ return node.parentNode;
+}
+
+/**
+ * Returns the next Node that's after node and all its descendants in tree
+ * order, or null if node is the last Node or an ancestor of it.
+ */
+function nextNodeDescendants(node) {
+ while (node && !node.nextSibling) {
+ node = node.parentNode;
+ }
+ if (!node) {
+ return null;
+ }
+ return node.nextSibling;
+}
+
+/**
+ * Returns the ownerDocument of the Node, or the Node itself if it's a
+ * Document.
+ */
+function ownerDocument(node) {
+ return node.nodeType == Node.DOCUMENT_NODE
+ ? node
+ : node.ownerDocument;
+}
+
+/**
+ * Returns true if ancestor is an ancestor of descendant, false otherwise.
+ */
+function isAncestor(ancestor, descendant) {
+ if (!ancestor || !descendant) {
+ return false;
+ }
+ while (descendant && descendant != ancestor) {
+ descendant = descendant.parentNode;
+ }
+ return descendant == ancestor;
+}
+
+/**
+ * Returns true if descendant is a descendant of ancestor, false otherwise.
+ */
+function isDescendant(descendant, ancestor) {
+ return isAncestor(ancestor, descendant);
+}
+
+/**
+ * The position of two boundary points relative to one another, as defined by
+ * the spec.
+ */
+function getPosition(nodeA, offsetA, nodeB, offsetB) {
+ // "If node A is the same as node B, return equal if offset A equals offset
+ // B, before if offset A is less than offset B, and after if offset A is
+ // greater than offset B."
+ if (nodeA == nodeB) {
+ if (offsetA == offsetB) {
+ return "equal";
+ }
+ if (offsetA < offsetB) {
+ return "before";
+ }
+ if (offsetA > offsetB) {
+ return "after";
+ }
+ }
+
+ // "If node A is after node B in tree order, compute the position of (node
+ // B, offset B) relative to (node A, offset A). If it is before, return
+ // after. If it is after, return before."
+ if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
+ var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
+ if (pos == "before") {
+ return "after";
+ }
+ if (pos == "after") {
+ return "before";
+ }
+ }
+
+ // "If node A is an ancestor of node B:"
+ if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
+ // "Let child equal node B."
+ var child = nodeB;
+
+ // "While child is not a child of node A, set child to its parent."
+ while (child.parentNode != nodeA) {
+ child = child.parentNode;
+ }
+
+ // "If the index of child is less than offset A, return after."
+ if (indexOf(child) < offsetA) {
+ return "after";
+ }
+ }
+
+ // "Return before."
+ return "before";
+}
+
+/**
+ * "contained" as defined by DOM Range: "A Node node is contained in a range
+ * range if node's furthest ancestor is the same as range's root, and (node, 0)
+ * is after range's start, and (node, length of node) is before range's end."
+ */
+function isContained(node, range) {
+ var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
+ var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset);
+
+ return furthestAncestor(node) == furthestAncestor(range.startContainer)
+ && pos1 == "after"
+ && pos2 == "before";
+}
+
+/**
+ * "partially contained" as defined by DOM Range: "A Node is partially
+ * contained in a range if it is an ancestor container of the range's start but
+ * not its end, or vice versa."
+ */
+function isPartiallyContained(node, range) {
+ var cond1 = isAncestorContainer(node, range.startContainer);
+ var cond2 = isAncestorContainer(node, range.endContainer);
+ return (cond1 && !cond2) || (cond2 && !cond1);
+}
+
+/**
+ * Index of a node as defined by the spec.
+ */
+function indexOf(node) {
+ if (!node.parentNode) {
+ // No preceding sibling nodes, right?
+ return 0;
+ }
+ var i = 0;
+ while (node != node.parentNode.childNodes[i]) {
+ i++;
+ }
+ return i;
+}
+
+/**
+ * extractContents() implementation, following the spec. If an exception is
+ * supposed to be thrown, will return a string with the name (e.g.,
+ * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also
+ * return an arbitrary human-readable string if a condition is hit that implies
+ * a spec bug.
+ */
+function myExtractContents(range) {
+ // "If the context object's detached flag is set, raise an
+ // INVALID_STATE_ERR exception and abort these steps."
+ try {
+ range.collapsed;
+ } catch (e) {
+ return "INVALID_STATE_ERR";
+ }
+
+ // "Let frag be a new DocumentFragment whose ownerDocument is the same as
+ // the ownerDocument of the context object's start node."
+ var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE
+ ? range.startContainer
+ : range.startContainer.ownerDocument;
+ var frag = ownerDoc.createDocumentFragment();
+
+ // "If the context object's start and end are the same, abort this method,
+ // returning frag."
+ if (range.startContainer == range.endContainer
+ && range.startOffset == range.endOffset) {
+ return frag;
+ }
+
+ // "Let original start node, original start offset, original end node, and
+ // original end offset be the context object's start and end nodes and
+ // offsets, respectively."
+ var originalStartNode = range.startContainer;
+ var originalStartOffset = range.startOffset;
+ var originalEndNode = range.endContainer;
+ var originalEndOffset = range.endOffset;
+
+ // "If original start node and original end node are the same, and they are
+ // a Text or Comment node:"
+ if (range.startContainer == range.endContainer
+ && (range.startContainer.nodeType == Node.TEXT_NODE
+ || range.startContainer.nodeType == Node.COMMENT_NODE)) {
+ // "Let clone be the result of calling cloneNode(false) on original
+ // start node."
+ var clone = originalStartNode.cloneNode(false);
+
+ // "Set the data of clone to the result of calling
+ // substringData(original start offset, original end offset − original
+ // start offset) on original start node."
+ clone.data = originalStartNode.substringData(originalStartOffset,
+ originalEndOffset - originalStartOffset);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Call deleteData(original start offset, original end offset −
+ // original start offset) on original start node."
+ originalStartNode.deleteData(originalStartOffset,
+ originalEndOffset - originalStartOffset);
+
+ // "Abort this method, returning frag."
+ return frag;
+ }
+
+ // "Let common ancestor equal original start node."
+ var commonAncestor = originalStartNode;
+
+ // "While common ancestor is not an ancestor container of original end
+ // node, set common ancestor to its own parent."
+ while (!isAncestorContainer(commonAncestor, originalEndNode)) {
+ commonAncestor = commonAncestor.parentNode;
+ }
+
+ // "If original start node is an ancestor container of original end node,
+ // let first partially contained child be null."
+ var firstPartiallyContainedChild;
+ if (isAncestorContainer(originalStartNode, originalEndNode)) {
+ firstPartiallyContainedChild = null;
+ // "Otherwise, let first partially contained child be the first child of
+ // common ancestor that is partially contained in the context object."
+ } else {
+ for (var i = 0; i < commonAncestor.childNodes.length; i++) {
+ if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
+ firstPartiallyContainedChild = commonAncestor.childNodes[i];
+ break;
+ }
+ }
+ if (!firstPartiallyContainedChild) {
+ throw "Spec bug: no first partially contained child!";
+ }
+ }
+
+ // "If original end node is an ancestor container of original start node,
+ // let last partially contained child be null."
+ var lastPartiallyContainedChild;
+ if (isAncestorContainer(originalEndNode, originalStartNode)) {
+ lastPartiallyContainedChild = null;
+ // "Otherwise, let last partially contained child be the last child of
+ // common ancestor that is partially contained in the context object."
+ } else {
+ for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) {
+ if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
+ lastPartiallyContainedChild = commonAncestor.childNodes[i];
+ break;
+ }
+ }
+ if (!lastPartiallyContainedChild) {
+ throw "Spec bug: no last partially contained child!";
+ }
+ }
+
+ // "Let contained children be a list of all children of common ancestor
+ // that are contained in the context object, in tree order."
+ //
+ // "If any member of contained children is a DocumentType, raise a
+ // HIERARCHY_REQUEST_ERR exception and abort these steps."
+ var containedChildren = [];
+ for (var i = 0; i < commonAncestor.childNodes.length; i++) {
+ if (isContained(commonAncestor.childNodes[i], range)) {
+ if (commonAncestor.childNodes[i].nodeType
+ == Node.DOCUMENT_TYPE_NODE) {
+ return "HIERARCHY_REQUEST_ERR";
+ }
+ containedChildren.push(commonAncestor.childNodes[i]);
+ }
+ }
+
+ // "If original start node is an ancestor container of original end node,
+ // set new node to original start node and new offset to original start
+ // offset."
+ var newNode, newOffset;
+ if (isAncestorContainer(originalStartNode, originalEndNode)) {
+ newNode = originalStartNode;
+ newOffset = originalStartOffset;
+ // "Otherwise:"
+ } else {
+ // "Let reference node equal original start node."
+ var referenceNode = originalStartNode;
+
+ // "While reference node's parent is not null and is not an ancestor
+ // container of original end node, set reference node to its parent."
+ while (referenceNode.parentNode
+ && !isAncestorContainer(referenceNode.parentNode, originalEndNode)) {
+ referenceNode = referenceNode.parentNode;
+ }
+
+ // "Set new node to the parent of reference node, and new offset to one
+ // plus the index of reference node."
+ newNode = referenceNode.parentNode;
+ newOffset = 1 + indexOf(referenceNode);
+ }
+
+ // "If first partially contained child is a Text or Comment node:"
+ if (firstPartiallyContainedChild
+ && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE
+ || firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
+ // "Let clone be the result of calling cloneNode(false) on original
+ // start node."
+ var clone = originalStartNode.cloneNode(false);
+
+ // "Set the data of clone to the result of calling substringData() on
+ // original start node, with original start offset as the first
+ // argument and (length of original start node − original start offset)
+ // as the second."
+ clone.data = originalStartNode.substringData(originalStartOffset,
+ getNodeLength(originalStartNode) - originalStartOffset);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Call deleteData() on original start node, with original start
+ // offset as the first argument and (length of original start node −
+ // original start offset) as the second."
+ originalStartNode.deleteData(originalStartOffset,
+ getNodeLength(originalStartNode) - originalStartOffset);
+ // "Otherwise, if first partially contained child is not null:"
+ } else if (firstPartiallyContainedChild) {
+ // "Let clone be the result of calling cloneNode(false) on first
+ // partially contained child."
+ var clone = firstPartiallyContainedChild.cloneNode(false);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Let subrange be a new Range whose start is (original start node,
+ // original start offset) and whose end is (first partially contained
+ // child, length of first partially contained child)."
+ var subrange = ownerDoc.createRange();
+ subrange.setStart(originalStartNode, originalStartOffset);
+ subrange.setEnd(firstPartiallyContainedChild,
+ getNodeLength(firstPartiallyContainedChild));
+
+ // "Let subfrag be the result of calling extractContents() on
+ // subrange."
+ var subfrag = myExtractContents(subrange);
+
+ // "For each child of subfrag, in order, append that child to clone as
+ // its last child."
+ for (var i = 0; i < subfrag.childNodes.length; i++) {
+ clone.appendChild(subfrag.childNodes[i]);
+ }
+ }
+
+ // "For each contained child in contained children, append contained child
+ // as the last child of frag."
+ for (var i = 0; i < containedChildren.length; i++) {
+ frag.appendChild(containedChildren[i]);
+ }
+
+ // "If last partially contained child is a Text or Comment node:"
+ if (lastPartiallyContainedChild
+ && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE
+ || lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE)) {
+ // "Let clone be the result of calling cloneNode(false) on original
+ // end node."
+ var clone = originalEndNode.cloneNode(false);
+
+ // "Set the data of clone to the result of calling substringData(0,
+ // original end offset) on original end node."
+ clone.data = originalEndNode.substringData(0, originalEndOffset);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Call deleteData(0, original end offset) on original end node."
+ originalEndNode.deleteData(0, originalEndOffset);
+ // "Otherwise, if last partially contained child is not null:"
+ } else if (lastPartiallyContainedChild) {
+ // "Let clone be the result of calling cloneNode(false) on last
+ // partially contained child."
+ var clone = lastPartiallyContainedChild.cloneNode(false);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Let subrange be a new Range whose start is (last partially
+ // contained child, 0) and whose end is (original end node, original
+ // end offset)."
+ var subrange = ownerDoc.createRange();
+ subrange.setStart(lastPartiallyContainedChild, 0);
+ subrange.setEnd(originalEndNode, originalEndOffset);
+
+ // "Let subfrag be the result of calling extractContents() on
+ // subrange."
+ var subfrag = myExtractContents(subrange);
+
+ // "For each child of subfrag, in order, append that child to clone as
+ // its last child."
+ for (var i = 0; i < subfrag.childNodes.length; i++) {
+ clone.appendChild(subfrag.childNodes[i]);
+ }
+ }
+
+ // "Set the context object's start and end to (new node, new offset)."
+ range.setStart(newNode, newOffset);
+ range.setEnd(newNode, newOffset);
+
+ // "Return frag."
+ return frag;
+}
+
+/**
+ * insertNode() implementation, following the spec. If an exception is
+ * supposed to be thrown, will return a string with the name (e.g.,
+ * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also
+ * return an arbitrary human-readable string if a condition is hit that implies
+ * a spec bug.
+ */
+function myInsertNode(range, newNode) {
+ // "If the context object's detached flag is set, raise an
+ // INVALID_STATE_ERR exception and abort these steps."
+ //
+ // Assume that if accessing collapsed throws, it's detached.
+ try {
+ range.collapsed;
+ } catch (e) {
+ return "INVALID_STATE_ERR";
+ }
+
+ // "If the context object's start node is a Text or Comment node and its
+ // parent is null, raise an HIERARCHY_REQUEST_ERR exception and abort these
+ // steps."
+ if ((range.startContainer.nodeType == Node.TEXT_NODE
+ || range.startContainer.nodeType == Node.COMMENT_NODE)
+ && !range.startContainer.parentNode) {
+ return "HIERARCHY_REQUEST_ERR";
+ }
+
+ // "If the context object's start node is a Text node, run splitText() on
+ // it with the context object's start offset as its argument, and let
+ // reference node be the result."
+ var referenceNode;
+ if (range.startContainer.nodeType == Node.TEXT_NODE) {
+ // We aren't testing how ranges vary under mutations, and browsers vary
+ // in how they mutate for splitText, so let's just force the correct
+ // way.
+ var start = [range.startContainer, range.startOffset];
+ var end = [range.endContainer, range.endOffset];
+
+ referenceNode = range.startContainer.splitText(range.startOffset);
+
+ if (start[0] == end[0]
+ && end[1] > start[1]) {
+ end[0] = referenceNode;
+ end[1] -= start[1];
+ } else if (end[0] == start[0].parentNode
+ && end[1] > indexOf(referenceNode)) {
+ end[1]++;
+ }
+ range.setStart(start[0], start[1]);
+ range.setEnd(end[0], end[1]);
+ // "Otherwise, if the context object's start node is a Comment, let
+ // reference node be the context object's start node."
+ } else if (range.startContainer.nodeType == Node.COMMENT_NODE) {
+ referenceNode = range.startContainer;
+ // "Otherwise, let reference node be the child of the context object's
+ // start node with index equal to the context object's start offset, or
+ // null if there is no such child."
+ } else {
+ referenceNode = range.startContainer.childNodes[range.startOffset];
+ if (typeof referenceNode == "undefined") {
+ referenceNode = null;
+ }
+ }
+
+ // "If reference node is null, let parent node be the context object's
+ // start node."
+ var parentNode;
+ if (!referenceNode) {
+ parentNode = range.startContainer;
+ // "Otherwise, let parent node be the parent of reference node."
+ } else {
+ parentNode = referenceNode.parentNode;
+ }
+
+ // "Call insertBefore(newNode, reference node) on parent node, re-raising
+ // any exceptions that call raised."
+ try {
+ parentNode.insertBefore(newNode, referenceNode);
+ } catch (e) {
+ return getDomExceptionName(e);
+ }
+}
+
+/**
+ * Asserts that two nodes are equal, in the sense of isEqualNode(). If they
+ * aren't, tries to print a relatively informative reason why not. TODO: Move
+ * this to testharness.js?
+ */
+function assertNodesEqual(actual, expected, msg) {
+ if (!actual.isEqualNode(expected)) {
+ msg = "Actual and expected mismatch for " + msg + ". ";
+
+ while (actual && expected) {
+ assert_true(actual.nodeType === expected.nodeType
+ && actual.nodeName === expected.nodeName
+ && actual.nodeValue === expected.nodeValue
+ && actual.childNodes.length === expected.childNodes.length,
+ "First differing node: expected " + format_value(expected)
+ + ", got " + format_value(actual));
+ actual = nextNode(actual);
+ expected = nextNode(expected);
+ }
+
+ assert_unreached("DOMs were not equal but we couldn't figure out why");
+ }
+}
+
+/**
+ * Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR"). In
+ * theory this should be just e.name, but in practice it's not. So I could
+ * legitimately just return e.name, but then every engine but WebKit would fail
+ * every test, since no one seems to care much for standardizing DOMExceptions.
+ * Instead I mangle it to account for browser bugs, so as not to fail
+ * insertNode() tests (for instance) for insertBefore() bugs. Of course, a
+ * standards-compliant browser will work right in any event.
+ *
+ * If the exception has no string property called "name" or "message", we just
+ * re-throw it.
+ */
+function getDomExceptionName(e) {
+ if (typeof e.name == "string"
+ && /^[A-Z_]+_ERR$/.test(e.name)) {
+ // Either following the standard, or prefixing NS_ERROR_DOM (I'm
+ // looking at you, Gecko).
+ return e.name.replace(/^NS_ERROR_DOM_/, "");
+ }
+
+ if (typeof e.message == "string"
+ && /^[A-Z_]+_ERR$/.test(e.message)) {
+ // Opera
+ return e.message;
+ }
+
+ if (typeof e.message == "string"
+ && /^DOM Exception:/.test(e.message)) {
+ // IE
+ return /[A-Z_]+_ERR/.exec(e.message)[0];
+ }
+
+ throw e;
+}
+
+/**
+ * Given an array of endpoint data [start container, start offset, end
+ * container, end offset], returns a Range with those endpoints.
+ */
+function rangeFromEndpoints(endpoints) {
+ // If we just use document instead of the ownerDocument of endpoints[0],
+ // WebKit will throw on setStart/setEnd. This is a WebKit bug, but it's in
+ // range, not selection, so we don't want to fail anything for it.
+ var range = ownerDocument(endpoints[0]).createRange();
+ range.setStart(endpoints[0], endpoints[1]);
+ range.setEnd(endpoints[2], endpoints[3]);
+ return range;
+}
+
+/**
+ * Given an array of endpoint data [start container, start offset, end
+ * container, end offset], sets the selection to have those endpoints. Uses
+ * addRange, so the range will be forwards. Accepts an empty array for
+ * endpoints, in which case the selection will just be emptied.
+ */
+function setSelectionForwards(endpoints) {
+ selection.removeAllRanges();
+ if (endpoints.length) {
+ selection.addRange(rangeFromEndpoints(endpoints));
+ }
+}
+
+/**
+ * Given an array of endpoint data [start container, start offset, end
+ * container, end offset], sets the selection to have those endpoints, with the
+ * direction backwards. Uses extend, so it will throw in IE. Accepts an empty
+ * array for endpoints, in which case the selection will just be emptied.
+ */
+function setSelectionBackwards(endpoints) {
+ selection.removeAllRanges();
+ if (endpoints.length) {
+ selection.collapse(endpoints[2], endpoints[3]);
+ selection.extend(endpoints[0], endpoints[1]);
+ }
+}
+
+/**
+ * Verify that the specified func doesn't change the selection.
+ * This function should be used in testharness tests.
+ */
+function assertSelectionNoChange(func) {
+ var originalCount = selection.rangeCount;
+ var originalRange = originalCount == 0 ? null : selection.getRangeAt(0);
+ var originalAnchorNode = selection.anchorNode;
+ var originalAnchorOffset = selection.anchorOffset;
+ var originalFocusNode = selection.focusNode;
+ var originalFocusOffset = selection.focusOffset;
+
+ func();
+
+ assert_equals(selection.rangeCount, originalCount,
+ "The operation should not add Range");
+ assert_equals(selection.anchorNode, originalAnchorNode,
+ "The operation should not update anchorNode");
+ assert_equals(selection.anchorOffset, originalAnchorOffset,
+ "The operation should not update anchorOffset");
+ assert_equals(selection.focusNode, originalFocusNode,
+ "The operation should not update focusNode");
+ assert_equals(selection.focusOffset, originalFocusOffset,
+ "The operation should not update focusOffset");
+ if (originalCount < 1)
+ return;
+ assert_equals(selection.getRangeAt(0), originalRange,
+ "The operation should not replace a registered Range");
+}
+
+/**
+ * Check if the specified node can be selectable with window.getSelection()
+ * methods.
+ */
+function isSelectableNode(node) {
+ if (!node)
+ return false;
+ if (node.nodeType == Node.DOCUMENT_TYPE_NODE)
+ return false;
+ return document.contains(node);
+}
diff --git a/testing/web-platform/tests/selection/contenteditable/cefalse-on-boundaries.html b/testing/web-platform/tests/selection/contenteditable/cefalse-on-boundaries.html
new file mode 100644
index 0000000000..945058dfc3
--- /dev/null
+++ b/testing/web-platform/tests/selection/contenteditable/cefalse-on-boundaries.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Selection of contenteditable=false at the beginning and end of contenteditable element</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div contenteditable id="host">
+ <div contenteditable="false" id="beginning">&nbsp;</div>
+ <p id="paragraph">Lorem ipsum dolor sit amet.</p>
+ <div contenteditable="false" id="end">&nbsp;</div>
+</div>
+<script>
+test( () => {
+ const range = document.createRange();
+
+ range.setStartBefore( beginning );
+ range.setEndAfter( paragraph );
+ getSelection().removeAllRanges();
+ getSelection().addRange( range );
+
+ const selectedRange = getSelection().getRangeAt( 0 );
+
+ assert_equals( selectedRange.startContainer, host );
+ assert_equals( selectedRange.startOffset, 1 );
+ assert_equals( selectedRange.endContainer, host );
+ assert_equals( selectedRange.endOffset, 4 );
+}, 'Selection can start on cE=false element at the beginning of the cE=true element' );
+
+test( () => {
+ const range = document.createRange();
+
+ range.setStartBefore( paragraph );
+ range.setEndAfter( end );
+ getSelection().removeAllRanges();
+ getSelection().addRange( range );
+
+ const selectedRange = getSelection().getRangeAt( 0 );
+
+ assert_equals( selectedRange.startContainer, host );
+ assert_equals( selectedRange.startOffset, 3 );
+ assert_equals( selectedRange.endContainer, host );
+ assert_equals( selectedRange.endOffset, 6 );
+}, 'Selection can end on cE=false element at the end of the cE=true element' );
+
+test( () => {
+ const range = document.createRange();
+
+ range.setStartBefore( beginning );
+ range.setEndAfter( end );
+ getSelection().removeAllRanges();
+ getSelection().addRange( range );
+
+ const selectedRange = getSelection().getRangeAt( 0 );
+
+ assert_equals( selectedRange.startContainer, host );
+ assert_equals( selectedRange.startOffset, 1 );
+ assert_equals( selectedRange.endContainer, host );
+ assert_equals( selectedRange.endOffset, 6 );
+}, 'Selection can start and end on cE=false elements at the boundaries of cE=true element' );
+
+test( () => {
+ const range = document.createRange();
+
+ range.selectNodeContents( host );
+
+ const selectedRange = getSelection().getRangeAt( 0 );
+
+ assert_equals( selectedRange.startContainer, host );
+ assert_equals( selectedRange.startOffset, 1 );
+ assert_equals( selectedRange.endContainer, host );
+ assert_equals( selectedRange.endOffset, 6 );
+}, 'Range#selectNodeContents() correctly select contents of cE=true element with cE=false elements on boundaries' );
+</script>
diff --git a/testing/web-platform/tests/selection/contenteditable/collapse.html b/testing/web-platform/tests/selection/contenteditable/collapse.html
new file mode 100644
index 0000000000..6081d05516
--- /dev/null
+++ b/testing/web-platform/tests/selection/contenteditable/collapse.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Selection across multiple contenteditable</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<button id="button"></button>
+<div contenteditable id="host1"></div>
+<div contenteditable id="host2"></div>
+<div contenteditable id="host3">
+ <div contenteditable="false">
+ <div contenteditable id="host4"></div>
+ </div>
+</div>
+<script>
+function clearFocus() {
+ // Move focus from editable host to a button to remove selection limiter
+ button.focus();
+}
+test(() => {
+ clearFocus();
+ getSelection().collapse(host1);
+ assert_equals(document.activeElement, host1);
+ getSelection().collapse(host2);
+ assert_equals(document.activeElement, host2);
+}, "Selection.collapse() must succeed across siblings");
+
+test(() => {
+ clearFocus();
+ getSelection().collapse(host4);
+ assert_equals(document.activeElement, host4);
+ getSelection().collapse(host3);
+ assert_equals(document.activeElement, host3);
+}, "Selection.collapse() must succeed for the ancestor");
+
+
+test(() => {
+ clearFocus();
+ getSelection().collapse(host3);
+ assert_equals(document.activeElement, host3);
+ getSelection().collapse(host4);
+ assert_equals(document.activeElement, host4);
+}, "Selection.collapse() must succeed for the descendant");
+</script>
diff --git a/testing/web-platform/tests/selection/contenteditable/initial-selection-on-focus.tentative.html b/testing/web-platform/tests/selection/contenteditable/initial-selection-on-focus.tentative.html
new file mode 100644
index 0000000000..fcaf70d877
--- /dev/null
+++ b/testing/web-platform/tests/selection/contenteditable/initial-selection-on-focus.tentative.html
@@ -0,0 +1,473 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name="variant" content="?div">
+<meta name="variant" content="?span">
+<title>initial selection on focus of contenteditable</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>var testsJsLibraryOnly = true</script>
+<script src="../../editing/include/tests.js"></script>
+<body>
+<script>
+"use strict";
+
+(function() {
+ const editingHostTagName = document.location.search.substr(1);
+ const editor = document.createElement(editingHostTagName);
+ editor.style.minHeight = "1em";
+ editor.setAttribute("contenteditable", "");
+ document.body.insertBefore(editor, document.body.firstChild);
+ editor.focus();
+ editor.getBoundingClientRect();
+
+ const tests = [
+ { description: "empty editor should set focus to start of it",
+ content: "{}",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to start of the text node",
+ content: "[]abc",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <br> node",
+ content: "{}<br>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the first <br> node",
+ content: "{}<br><br>",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should set selection to start of the text node in the <p> node",
+ content: "<p>[]abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the first visible character in the text node in the <p> node",
+ content: "<p> []abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the <p> node because of preformatted white-space",
+ content: "<p style=\"white-space: pre\">[] abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the <p> node because of preformatted line break",
+ content: "<p style=\"white-space: pre\">[]\nabc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the <p> node",
+ content: "<p>{}<br></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the first <br> node in the <p> node",
+ content: "<p>{}<br><br></p>",
+ canTestInInlineEditingHost: false,
+ },
+
+ { description: "editor should set selection to start of the text node in the <span> node",
+ content: "<span>[]abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <br> node in the <span> node",
+ content: "<span>{}<br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the first <br> node in the <span> node",
+ content: "<span>{}<br><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should set selection to before the empty <span> node",
+ content: "{}<span></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the empty <b> node",
+ content: "{}<b></b>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the empty <i> node",
+ content: "{}<i></i>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the empty <u> node",
+ content: "{}<u></u>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the empty <s> node",
+ content: "{}<s></s>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the empty <code> node",
+ content: "{}<code></code>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the empty <a> node",
+ content: "{}<a href=\"foo.html\"></a>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the empty <foobar> node",
+ content: "{}<foobar></foobar>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <input> node",
+ content: "{}<input>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <img> node",
+ content: "{}<img alt=\"foo\">",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should set selection to start of the text node in the second <span> node",
+ content: "<span></span><span>[]abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <br> node in the second <span> node",
+ content: "<span></span><span>{}<br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to start of the text node in the first <span> node #1",
+ content: "<span>[]abc</span><span>abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to start of the text node in the first <span> node #2",
+ content: "<span>[]abc</span><span><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <br> node in the first <span> node #1",
+ content: "<span>{}<br></span><span><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <br> node in the first <span> node #2",
+ content: "<span>{}<br></span><span>abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should set selection to start of the text node in the second <span> node since the text node in the first <span> node is only whitespaces",
+ content: "<span> </span><span>[]abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <br> node in the second <span> node since the text node in the first <span> node is only whitespaces",
+ content: "<span> </span><span>{}<br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to start of the text node in the second <span> node even if there is a whitespace only text node before the first <span> node",
+ content: " <span></span><span>[]abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should set selection to before the <br> node in the second <span> node even if there is a whitespace only text node before the first <span> node",
+ content: " <span></span><span>{}<br></span>",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should set selection to start of the text node in the second <p> node following the empty <p> node",
+ content: "<p></p><p>[]abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the second <p> node following another <p> node containing only a comment node",
+ content: "<p><!-- comment --></p><p>[]abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the second <p> node",
+ content: "<p></p><p>{}<br></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the first <p> node #1",
+ content: "<p>[]abc</p><p>abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the first <p> node #2",
+ content: "<p>[]abc</p><p><br></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the first <p> node #1",
+ content: "<p>{}<br></p><p><br></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the first <p> node #2",
+ content: "<p>{}<br></p><p>abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+
+ { description: "editor should set selection to start of the text node in the second <p> node since the text node in the first <p> node is only whitespaces",
+ content: "<p> </p><p>[]abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the first <p> node whose white-spaces are preformatted",
+ content: "<p style=\"white-space: pre\">[] </p><p>abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the first <p> node whose line breaks are preformatted",
+ content: "<p style=\"white-space: pre\">[]\n</p><p>abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the second <p> node since the text node in the first <p> node is only whitespaces",
+ content: "<p> </p><p>{}<br></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the second <p> node even if there is a whitespace only text node before the first <p> node",
+ content: " <p></p><p>[]abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the second <p> node even if there is a whitespace only text node before the first <p> node",
+ content: " <p></p><p>{}<br></p>",
+ canTestInInlineEditingHost: false,
+ },
+
+ { description: "editor should set selection to start of the text node in the <span> node in the second <p> node",
+ content: "<p><span></span></p><p><span>[]abc</span></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the <span> node in the second <p> node",
+ content: "<p><span></span></p><p><span>{}<br></span></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the <span> node in the first <p> node #1",
+ content: "<p><span>[]abc</span></p><p><span>abc</span></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to start of the text node in the <span> node in the first <p> node #2",
+ content: "<p><span>[]abc</span></p><p><span><br></span></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the <span> node in the first <p> node #1",
+ content: "<p><span>{}<br></span></p><p><span><br></span></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should set selection to before the <br> node in the <span> node in the first <p> node #2",
+ content: "<p><span>{}<br></span></p><p><span>abc</span></p>",
+ canTestInInlineEditingHost: false,
+ },
+
+ { description: "editor should collapse selection before the non-editable <span> node",
+ content: "{}<span contenteditable=\"false\"></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection before the non-editable <span> node even if it has a text node",
+ content: "{}<span contenteditable=\"false\">abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection before the non-editable <span> node even if it has a <br> node",
+ content: "{}<span contenteditable=\"false\"><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should collapse selection before the non-editable empty <span> node followed by a text node",
+ content: "{}<span contenteditable=\"false\"></span><span>abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection before the non-editable <span> node having a text node and followed by another text node",
+ content: "{}<span contenteditable=\"false\">abc</span><span>def</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection before the non-editable <span> node having a <br> node and followed by a text node",
+ content: "{}<span contenteditable=\"false\"><br></span><span>abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection before the non-editable empty <span> node followed by a <br> node",
+ content: "{}<span contenteditable=\"false\"></span><span><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection before the non-editable <span> node having text node and followed by a <br> node",
+ content: "{}<span contenteditable=\"false\">abc</span><span><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection before the non-editable <span> node having a <br> node and followed by another <br> node",
+ content: "{}<span contenteditable=\"false\"><br></span><span><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should collapse selection before the non-editable empty <p> node followed by a text node",
+ content: "{}<p contenteditable=\"false\"></p><p>abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection before the non-editable <p> node having a text node and followed by another text node",
+ content: "{}<p contenteditable=\"false\">abc</p><p>def</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection before the non-editable <p> node having a <br> node and followed by a text node",
+ content: "{}<p contenteditable=\"false\"><br></p><p>abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection before the non-editable empty <p> node followed by a <br> node",
+ content: "{}<p contenteditable=\"false\"></p><p><br></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection before the non-editable <p> node having text node and followed by a <br> node",
+ content: "{}<p contenteditable=\"false\">abc</p><p><br></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection before the non-editable <p> node having a <br> node and followed by another <br> node",
+ content: "{}<p contenteditable=\"false\"><br></p><p><br></p>",
+ canTestInInlineEditingHost: false,
+ },
+
+ { description: "editor should collapse selection to start of itself when there is only empty inline elements before the non-editable node before first editable text node",
+ content: "{}<span></span><span contenteditable=\"false\"></span><span>abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to start of itself when there is only empty inline elements before the non-editable node having a text node before first editable text node",
+ content: "{}<span></span><span contenteditable=\"false\">abc</span><span>def</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to start of itself when there is only empty inline elements before the non-editable node having a <br> node before first editable text node",
+ content: "{}<span></span><span contenteditable=\"false\"><br></span><span>abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to start of itself when there is only empty inline elements before the non-editable node before first editable <br> node",
+ content: "{}<span></span><span contenteditable=\"false\"></span><span><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to start of itself when there is only empty inline elements before the non-editable node having a text node before first editable <br> node",
+ content: "{}<span></span><span contenteditable=\"false\">abc</span><span><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to start of itself when there is only empty inline elements before the non-editable node having a <br> node before first editable <br> node",
+ content: "{}<span></span><span contenteditable=\"false\"><br></span><span><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should collapse selection to start of the first dive element when there is only empty inline elements before the non-editable node before first editable text node",
+ content: "<div>{}<span></span><span contenteditable=\"false\"></span><span>abc</span></div>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to start of the first dive element when there is only empty inline elements before the non-editable node having a text node before first editable text node",
+ content: "<div>{}<span></span><span contenteditable=\"false\">abc</span><span>def</span></div>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to start of the first dive element when there is only empty inline elements before the non-editable node having a <br> node before first editable text node",
+ content: "<div>{}<span></span><span contenteditable=\"false\"><br></span><span>abc</span></div>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to start of the first dive element when there is only empty inline elements before the non-editable node before first editable <br> node",
+ content: "<div>{}<span></span><span contenteditable=\"false\"></span><span><br></span></div>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to start of the first dive element when there is only empty inline elements before the non-editable node having a text node before first editable <br> node",
+ content: "<div>{}<span></span><span contenteditable=\"false\">abc</span><span><br></span></div>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to start of the first dive element when there is only empty inline elements before the non-editable node having a <br> node before first editable <br> node",
+ content: "<div>{}<span></span><span contenteditable=\"false\"><br></span><span><br></span></div>",
+ canTestInInlineEditingHost: false,
+ },
+
+ { description: "editor should collapse selection to the first editable text node in the first <span> node even if followed by a non-editable node",
+ content: "<span>[]abc</span><span contenteditable=\"false\"></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to the first editable text node in the first <span> node even if followed by a non-editable node having another text node",
+ content: "<span>[]abc</span><span contenteditable=\"false\">def</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to the first editable text node in the first <span> node even if followed by a non-editable node having a <br> node",
+ content: "<span>[]abc</span><span contenteditable=\"false\"><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to the first editable <br> node in the first <span> node even if followed by a non-editable node",
+ content: "<span>{}<br></span><span contenteditable=\"false\"></span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to the first editable <br> node in the first <span> node even if followed by a non-editable node having a text node",
+ content: "<span>{}<br></span><span contenteditable=\"false\">abc</span>",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to the first editable <br> node in the first <span> node even if followed by a non-editable node having a <br> node",
+ content: "<span>{}<br></span><span contenteditable=\"false\"><br></span>",
+ canTestInInlineEditingHost: true,
+ },
+
+ { description: "editor should collapse selection to the first editable text node in the first <p> node even if followed by a non-editable node",
+ content: "<p>[]abc</p><p contenteditable=\"false\"></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to the first editable text node in the first <p> node even if followed by a non-editable node having another text node",
+ content: "<p>[]abc</p><p contenteditable=\"false\">def</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to the first editable text node in the first <p> node even if followed by a non-editable node having a <br> node",
+ content: "<p>[]abc</p><p contenteditable=\"false\"><br></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to the first editable <br> node in the first <p> node even if followed by a non-editable node",
+ content: "<p>{}<br></p><p contenteditable=\"false\"></p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to the first editable <br> node in the first <p> node even if followed by a non-editable node having a text node",
+ content: "<p>{}<br></p><p contenteditable=\"false\">abc</p>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to the first editable <br> node in the first <p> node even if followed by a non-editable node having a <br> node",
+ content: "<p>{}<br></p><p contenteditable=\"false\"><br></p>",
+ canTestInInlineEditingHost: false,
+ },
+
+ { description: "editor should collapse selection to start of itself if first content is an input element",
+ content: "{}<input>abc",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to start of itself if first content is an hr element",
+ content: "{}<hr>abc",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to start of itself if first content is an textarea element",
+ content: "{}<textarea>abc</textarea>def",
+ canTestInInlineEditingHost: true,
+ },
+ { description: "editor should collapse selection to the input element",
+ content: "<div>{}<input>abc</div>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to the hr element",
+ content: "<div>{}<hr>abc</div>",
+ canTestInInlineEditingHost: false,
+ },
+ { description: "editor should collapse selection to the textarea element",
+ content: "<div>{}<textarea>abc</textarea>def</div>",
+ canTestInInlineEditingHost: false,
+ },
+ ];
+
+ const isInlineEditingHost = editingHostTagName == "span";
+
+ const selection = document.getSelection();
+ for (const testData of tests) {
+ if (isInlineEditingHost && !testData.canTestInInlineEditingHost) {
+ continue;
+ }
+ test(function() {
+ editor.blur();
+ selection.removeAllRanges();
+ editor.innerHTML = testData.content.replace(/[{}\[\]]/g, "");
+ editor.focus();
+ editor.getBoundingClientRect();
+
+ assert_equals(selection.rangeCount, 1, "Only one caret should be in the editor");
+ if (selection.rangeCount) {
+ addBrackets(selection.getRangeAt(0));
+ assert_equals(editor.innerHTML, testData.content);
+ }
+ }, testData.description);
+ }
+
+ test(function() {
+ // Check if selection is initialized after temporarily blurred.
+ editor.innerHTML =
+ `<${editingHostTagName}>abc</${editingHostTagName}><${editingHostTagName}>def</${editingHostTagName}>`;
+ editor.focus();
+ // Move selection to the second paragraph.
+ selection.collapse(editor.firstChild.nextSibling.firstChild);
+ // Reset focus.
+ editor.blur();
+ editor.focus();
+ // Then, selection should still be in the second paragraph.
+ assert_equals(selection.rangeCount, 1, "Only one caret should be in the editor");
+ if (selection.rangeCount) {
+ addBrackets(selection.getRangeAt(0));
+ assert_equals(
+ editor.innerHTML,
+ `<${editingHostTagName}>abc</${editingHostTagName}><${editingHostTagName}>[]def</${editingHostTagName}>`
+ );
+ }
+ }, "editor shouldn't reset selection when it gets focus again");
+})();
+</script>
diff --git a/testing/web-platform/tests/selection/contenteditable/modify.tentative.html b/testing/web-platform/tests/selection/contenteditable/modify.tentative.html
new file mode 100644
index 0000000000..a3afd52ea2
--- /dev/null
+++ b/testing/web-platform/tests/selection/contenteditable/modify.tentative.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Selection.modify() inside contenteditable</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div contenteditable id="host">Editable</div>
+<div>Non-editable</div>
+<div id="inlinehosts">
+ Prefix: <span contenteditable title="inline">Editable</span>: Suffix<br>
+ Prefix: <span contenteditable style="display:inline-block;" title="inline-block">Editable</span>: Suffix<br>
+ <span contenteditable title="suffix only">Editable</span>: Suffix<br>
+ Prefix: <span contenteditable title="prefix only">Editable</span><br>
+ <span contenteditable title="standalone">Editable</span><br>
+ Prefix: <span contenteditable title="inline linebreak">Edit<br>able</span>: Suffix<br>
+ Prefix: <span contenteditable style="display:inline-block;" title="inline-block linebreak">Edit<br>able</span>:
+ Suffix<br>
+</div>
+<script>
+ /** @param {Node} parent */
+ function* textNodeEntries(parent) {
+ for (const [i, node] of parent.childNodes.entries()) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ yield [i, node];
+ }
+ }
+ }
+
+ const selection = getSelection();
+ test(() => {
+ selection.collapse(host);
+ selection.modify('extend', 'forward', 'word');
+ selection.modify('extend', 'forward', 'word');
+ assert_equals(selection.focusNode.parentElement, host);
+ }, "Selection.modify() must not select outside of the host");
+
+ /** @type {NodeListOf<HTMLElement>} */
+ const hosts = inlinehosts.querySelectorAll("span[contenteditable]");
+ for (const host of hosts) {
+ test(() => {
+ for (const [i, text] of textNodeEntries(host)) {
+ selection.collapse(text, 1);
+ selection.modify("move", "forward", "lineboundary");
+ if (selection.focusNode === host) {
+ assert_equals(selection.focusOffset, i + 1, "focusOffset should be after the text node");
+ } else {
+ assert_equals(selection.focusNode, text, "focusNode should be the text node");
+ assert_equals(selection.focusOffset, text.textContent.length, "focusOffset should be the length of the text node");
+ }
+ }
+ }, `Selection.modify('move', 'forward', 'lineboundary') must be within the inline editing host: ${host.title}`);
+ test(() => {
+ for (const [i, text] of textNodeEntries(host)) {
+ selection.collapse(text, 1);
+ selection.modify("move", "backward", "lineboundary");
+ assert_equals(selection.focusNode, text, "focusNode should be the text node");
+ assert_equals(selection.focusOffset, 0, "focusOffset should be 0");
+ }
+ }, `Selection.modify('move', 'backward', 'lineboundary') must be within the inline editing host: ${host.title}`);
+ }
+</script>
diff --git a/testing/web-platform/tests/selection/contenteditable/modifying-selection-with-middle-mouse-button.tentative.html b/testing/web-platform/tests/selection/contenteditable/modifying-selection-with-middle-mouse-button.tentative.html
new file mode 100644
index 0000000000..a8287d9002
--- /dev/null
+++ b/testing/web-platform/tests/selection/contenteditable/modifying-selection-with-middle-mouse-button.tentative.html
@@ -0,0 +1,213 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing default action of `mousedown` of middle button and `mouseup` of middle button</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>
+<style>
+span {
+ white-space: nowrap;
+}
+</style>
+</head>
+<body>
+<div contenteditable></div>
+<script>
+"use strict";
+
+var editor = document.querySelector("div[contenteditable]");
+var span1, span2, link;
+var selection = getSelection();
+
+function preventDefault(event) {
+ event.preventDefault();
+}
+editor.addEventListener("paste", preventDefault, {capture: true});
+
+function resetEditor() {
+ editor.innerHTML =
+ '<span id="span1">first span.</span><br><span id="span2">second span.</span><br><a id="link" href="#top">link.</a>';
+ span1 = document.getElementById("span1");
+ span2 = document.getElementById("span2");
+ link = document.getElementById("link");
+}
+
+promise_test(async () => {
+ resetEditor();
+ editor.blur();
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span1})
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .send();
+
+ assert_equals(document.activeElement, editor,
+ "The clicked editor should get focus");
+ assert_true(selection.isCollapsed,
+ "Selection should be collapsed after middle button click");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should be collapsed in the first <span> element which was clicked by middle button");
+}, "Middle click should set focus to clicked editable element and collapse selection around the clicked point");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .send();
+
+ assert_equals(document.activeElement, editor,
+ "The clicked editor should keep having focus");
+ assert_true(selection.isCollapsed,
+ "Selection should be collapsed after middle button click");
+ assert_equals(selection.focusNode, span2.firstChild,
+ "Selection should be collapsed in the second <span> element which was clicked by middle button");
+}, "Middle click should move caret in an editable element");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ addEventListener("mousedown", preventDefault);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .send();
+ removeEventListener("mousedown", preventDefault);
+
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should keep collapsed selection in the first <span> element");
+ assert_equals(selection.focusOffset, 2,
+ "Selection should keep collapsed selection at 2 of the first <span> element");
+}, "Middle click shouldn't move caret in an editable element if the default of mousedown event is prevented");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ addEventListener("pointerdown", preventDefault);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .send();
+ removeEventListener("pointerdown", preventDefault);
+
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should keep collapsed selection in the first <span> element");
+ assert_equals(selection.focusOffset, 2,
+ "Selection should keep collapsed selection at 2 of the first <span> element");
+}, "Middle click shouldn't move caret in an editable element if the default of pointerdown event is prevented");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .keyDown("\uE008")
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .keyUp("\uE008")
+ .send();
+
+ assert_equals(selection.anchorNode, span1.firstChild,
+ "Selection#anchorNode should keep in the first <span> element");
+ assert_equals(selection.anchorOffset, 2,
+ "Selection#anchorNode should keep at 2 of the first <span> element");
+ assert_equals(selection.focusNode, span2.firstChild,
+ "Selection#focusNode should be in the second <span> element which was clicked by middle button");
+}, "Shift + Middle click should extend the selection");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: link})
+ .keyDown("\uE008")
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .keyUp("\uE008")
+ .send();
+
+ assert_equals(selection.focusNode, link.firstChild,
+ "Selection#focusNode should be in the <a href> element which was clicked by middle button");
+ assert_true(selection.isCollapsed,
+ "Selection#isCollapsed should be true");
+}, "Shift + Middle click in a link shouldn't extend the selection");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ editor.addEventListener("pointerdown", () => {
+ assert_true(selection.isCollapsed,
+ "Selection shouldn't be modified before pointerdown event");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should stay in the first <span> element when pointerdown event is fired (focusNode)");
+ assert_equals(selection.focusOffset, 2,
+ "Selection should stay in the first <span> element when pointerdown event is fired (focusOffset)");
+ }, {once: true});
+ editor.addEventListener("mousedown", () => {
+ assert_true(selection.isCollapsed,
+ "Selection shouldn't be modified before mousedown event");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should stay in the first <span> element when mousedown event is fired (focusNode)");
+ assert_equals(selection.focusOffset, 2,
+ "Selection should stay in the first <span> element when mousedown event is fired (focusOffset)");
+ }, {once: true});
+ editor.addEventListener("pointerup", () => {
+ assert_true(selection.isCollapsed,
+ "Selection should be collapsed before pointerup event");
+ assert_equals(selection.focusNode, span2.firstChild,
+ "Selection should be collapsed in the second <span> element which was clicked by middle button before pointerup event ");
+ }, {once: true});
+ let focusOffsetAtMouseUp;
+ editor.addEventListener("mouseup", () => {
+ assert_true(selection.isCollapsed,
+ "Selection should be collapsed before mouseup event");
+ assert_equals(selection.focusNode, span2.firstChild,
+ "Selection should be collapsed in the second <span> element which was clicked by middle button before mouseup event ");
+ focusOffsetAtMouseUp = selection.focusOffset;
+ }, {once: true});
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerMove(0, 0, {origin: span1})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .send();
+
+ assert_true(selection.isCollapsed,
+ "Selection shouldn't be extended by pointer moves during pressing middle button");
+ assert_equals(selection.focusNode, span2.firstChild,
+ "Selection#focusNode should stay in the second <span> element");
+ assert_equals(selection.focusOffset, focusOffsetAtMouseUp,
+ "Selection#focusOffset should stay in the second <span> element");
+}, "Middle mouse button down should move caret, but middle mouse button up shouldn't move caret");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/selection/contenteditable/modifying-selection-with-primary-mouse-button.tentative.html b/testing/web-platform/tests/selection/contenteditable/modifying-selection-with-primary-mouse-button.tentative.html
new file mode 100644
index 0000000000..57586a6060
--- /dev/null
+++ b/testing/web-platform/tests/selection/contenteditable/modifying-selection-with-primary-mouse-button.tentative.html
@@ -0,0 +1,212 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Testing default action of `mousedown` of primary button and `mouseup` of primary button</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>
+<style>
+span {
+ white-space: nowrap;
+}
+</style>
+</head>
+<body>
+<div contenteditable></div>
+<script>
+"use strict";
+
+var editor = document.querySelector("div[contenteditable]");
+var span1, span2, link;
+var selection = getSelection();
+
+function preventDefault(event) {
+ event.preventDefault();
+}
+editor.addEventListener("paste", preventDefault, {capture: true});
+
+function resetEditor() {
+ editor.innerHTML =
+ '<span id="span1">first span.</span><br><span id="span2">second span.</span><br><a id="link" href="#top">link.</a>';
+ span1 = document.getElementById("span1");
+ span2 = document.getElementById("span2");
+ link = document.getElementById("link");
+}
+
+promise_test(async () => {
+ resetEditor();
+ editor.blur();
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span1})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+
+ assert_equals(document.activeElement, editor,
+ "The clicked editor should get focus");
+ assert_true(selection.isCollapsed,
+ "Selection should be collapsed after primary button click");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should be collapsed in the first <span> element which was clicked by primary button");
+}, "Primary click should set focus to clicked editable element and collapse selection around the clicked point");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+
+ assert_equals(document.activeElement, editor,
+ "The clicked editor should keep having focus");
+ assert_true(selection.isCollapsed,
+ "Selection should be collapsed after primary button click");
+ assert_equals(selection.focusNode, span2.firstChild,
+ "Selection should be collapsed in the second <span> element which was clicked by primary button");
+}, "Primary click should move caret in an editable element");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ addEventListener("mousedown", preventDefault);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+ removeEventListener("mousedown", preventDefault);
+
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should keep collapsed selection in the first <span> element");
+ assert_equals(selection.focusOffset, 2,
+ "Selection should keep collapsed selection at 2 of the first <span> element");
+}, "Primary click shouldn't move caret in an editable element if the default of mousedown event is prevented");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ addEventListener("pointerdown", preventDefault);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+ removeEventListener("pointerdown", preventDefault);
+
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should keep collapsed selection in the first <span> element");
+ assert_equals(selection.focusOffset, 2,
+ "Selection should keep collapsed selection at 2 of the first <span> element");
+}, "Primary click shouldn't move caret in an editable element if the default of pointerdown event is prevented");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .keyDown("\uE008")
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .keyUp("\uE008")
+ .send();
+
+ assert_equals(selection.anchorNode, span1.firstChild,
+ "Selection#anchorNode should keep in the first <span> element");
+ assert_equals(selection.anchorOffset, 2,
+ "Selection#anchorNode should keep at 2 of the first <span> element");
+ assert_equals(selection.focusNode, span2.firstChild,
+ "Selection#focusNode should be in the second <span> element which was clicked by primary button");
+}, "Shift + Primary click should extend the selection");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: link})
+ .keyDown("\uE008")
+ .pointerDown({button: actions.ButtonType.MIDDLE})
+ .pointerUp({button: actions.ButtonType.MIDDLE})
+ .keyUp("\uE008")
+ .send();
+
+ assert_equals(selection.focusNode, link.firstChild,
+ "Selection#focusNode should be in the <a href> element which was clicked by primary button");
+ assert_true(selection.isCollapsed,
+ "Selection#isCollapsed should be true");
+}, "Shift + Primary click in a link shouldn't extend the selection");
+
+promise_test(async () => {
+ resetEditor();
+ editor.focus();
+ selection.collapse(span1.firstChild, 2);
+ editor.addEventListener("pointerdown", () => {
+ assert_true(selection.isCollapsed,
+ "Selection shouldn't be modified before pointerdown event");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should stay in the first <span> element when pointerdown event is fired (focusNode)");
+ assert_equals(selection.focusOffset, 2,
+ "Selection should stay in the first <span> element when pointerdown event is fired (focusOffset)");
+ }, {once: true});
+ editor.addEventListener("mousedown", () => {
+ assert_true(selection.isCollapsed,
+ "Selection shouldn't be modified before mousedown event");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should stay in the first <span> element when mousedown event is fired (focusNode)");
+ assert_equals(selection.focusOffset, 2,
+ "Selection should stay in the first <span> element when mousedown event is fired (focusOffset)");
+ }, {once: true});
+ editor.addEventListener("pointerup", () => {
+ assert_true(!selection.isCollapsed,
+ "Selection should be modified before pointerup event");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should be modified to extend the range before pointerup event ");
+ }, {once: true});
+ let anchorOffsetAtMouseUp;
+ editor.addEventListener("mouseup", () => {
+ assert_true(!selection.isCollapsed,
+ "Selection should be modified before mouseup event");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection should be modified to extend the range before mouseup event ");
+ anchorOffsetAtMouseUp = selection.anchorOffset;
+ }, {once: true});
+ let actions = new test_driver.Actions();
+ await actions
+ .pointerMove(0, 0)
+ .pointerMove(0, 0, {origin: span2})
+ .pointerDown({button: actions.ButtonType.LEFT})
+ .pointerMove(0, 0, {origin: span1})
+ .pointerUp({button: actions.ButtonType.LEFT})
+ .send();
+
+ assert_equals(selection.anchorNode, span2.firstChild,
+ "Selection#anchorNode should stay in the second <span> element which mousedown occurred on");
+ assert_equals(selection.anchorOffset, anchorOffsetAtMouseUp,
+ "Selection#anchorOffset should stay in the second <span> element which mousedown occurred on");
+ assert_equals(selection.focusNode, span1.firstChild,
+ "Selection#focusNode should be in the first <span> element which mouseup occurred on");
+}, "Primary mouse button down should move caret, and primary mouse button up should extend the selection");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/selection/crashtests/table.html b/testing/web-platform/tests/selection/crashtests/table.html
new file mode 100644
index 0000000000..36eb803a5c
--- /dev/null
+++ b/testing/web-platform/tests/selection/crashtests/table.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script>
+ function go() {
+ let b = window.getSelection();
+ b.setPosition(a.insertRow(0), 0)
+ b.modify("extend", "right", "word")
+ }
+</script>
+
+<body onload=go()>
+ <dd dir="rtl">M!}\\)M6\X</dd>
+ <table id="a">
+ <tr>
diff --git a/testing/web-platform/tests/selection/deleteFromDocument.html b/testing/web-platform/tests/selection/deleteFromDocument.html
new file mode 100644
index 0000000000..b7e036009f
--- /dev/null
+++ b/testing/web-platform/tests/selection/deleteFromDocument.html
@@ -0,0 +1,97 @@
+<!doctype html>
+<title>Selection.deleteFromDocument() tests</title>
+<link rel=author title="Aryeh Gregor" href=ayg@aryeh.name>
+<p>To debug test failures, add a query parameter with the test id (like
+"?5"). Only that test will be run. Then you can look at the resulting
+iframes in the DOM.
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+// We need to use explicit_done, because in Chrome 16 dev and Opera 12.00, the
+// second iframe doesn't block the load event -- even though it is added before
+// the load event.
+setup({explicit_done: true});
+
+// Specified by WebIDL
+test(function() {
+ assert_equals(Selection.prototype.deleteFromDocument.length, 0,
+ "Selection.prototype.deleteFromDocument.length must equal 0");
+}, "Selection.prototype.deleteFromDocument.length must equal 0");
+
+testDiv.parentNode.removeChild(testDiv);
+
+// Test an empty selection too
+testRanges.unshift("empty");
+
+var actualIframe = document.createElement("iframe");
+
+var expectedIframe = document.createElement("iframe");
+
+var referenceDoc = document.implementation.createHTMLDocument("");
+referenceDoc.removeChild(referenceDoc.documentElement);
+
+actualIframe.onload = function() {
+ expectedIframe.onload = function() {
+ for (var i = 0; i < testRanges.length; i++) {
+ if (location.search && i != Number(location.search)) {
+ continue;
+ }
+
+ test(function() {
+ initializeIframe(actualIframe, testRanges[i]);
+ initializeIframe(expectedIframe, testRanges[i]);
+
+ var actualRange = actualIframe.contentWindow.testRange;
+ var expectedRange = expectedIframe.contentWindow.testRange;
+
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual deleteFromDocument");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated deleteFromDocument");
+
+ actualIframe.contentWindow.getSelection().removeAllRanges();
+ if (testRanges[i] != "empty") {
+ assert_equals(typeof actualRange, "object",
+ "Range produced in actual iframe must be an object");
+ assert_equals(typeof expectedRange, "object",
+ "Range produced in expected iframe must be an object");
+ assert_true(actualRange instanceof actualIframe.contentWindow.Range,
+ "Range produced in actual iframe must be instanceof Range");
+ assert_true(expectedRange instanceof expectedIframe.contentWindow.Range,
+ "Range produced in expected iframe must be instanceof Range");
+ actualIframe.contentWindow.getSelection().addRange(actualIframe.contentWindow.testRange);
+ expectedIframe.contentWindow.testRange.deleteContents();
+ }
+ actualIframe.contentWindow.getSelection().deleteFromDocument();
+
+ assertNodesEqual(actualIframe.contentDocument, expectedIframe.contentDocument, "DOM contents");
+ }, "Range " + i + ": " + testRanges[i]);
+ }
+ actualIframe.style.display = "none";
+ expectedIframe.style.display = "none";
+ done();
+ };
+ expectedIframe.src = "test-iframe.html";
+ document.body.appendChild(expectedIframe);
+ referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
+};
+actualIframe.src = "test-iframe.html";
+document.body.appendChild(actualIframe);
+
+function initializeIframe(iframe, endpoints) {
+ while (iframe.contentDocument.firstChild) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
+ }
+ iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
+ iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
+ iframe.contentWindow.setupRangeTests();
+ if (endpoints != "empty") {
+ iframe.contentWindow.testRangeInput = endpoints;
+ iframe.contentWindow.run();
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/selection/dir-manual.html b/testing/web-platform/tests/selection/dir-manual.html
new file mode 100644
index 0000000000..39cf655523
--- /dev/null
+++ b/testing/web-platform/tests/selection/dir-manual.html
@@ -0,0 +1,106 @@
+<!doctype html>
+<title>Selection direction tests</title>
+<meta charset=utf-8>
+<div id=test>
+ <p>This is a manual test, since there's no way to synthesize keyboard or
+ mouse input. Click after the letter "c" in the following paragraph and
+ drag backwards so that both the "b" and the "c" are highlighted, then click
+ the "Test" button:
+
+ <p>abcd <button onclick=testDirection()>Test</button>
+
+ <p>efghi
+</div>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+setup({explicit_done: true});
+
+function testDirection() {
+ var testDiv = document.getElementById("test");
+ var p = testDiv.getElementsByTagName("p")[1].firstChild;
+ var selection = getSelection();
+ var range = selection.getRangeAt(0);
+ test(function() {
+ assert_equals(range.toString(), "bc");
+ }, "The expected range is selected");
+ test(function() {
+ assert_equals(selection.anchorNode, p);
+ assert_equals(selection.focusNode, p);
+ }, "Expected node is initially selected");
+ test(function() {
+ assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [1, 3]);
+ }, "Expected offsets are initially selected (maybe not in order)");
+ test(function() {
+ assert_equals(selection.anchorOffset, 3);
+ assert_equals(selection.focusOffset, 1);
+ }, "Offsets are backwards for initial selection"),
+ test(function() {
+ assert_equals(selection.anchorNode, range.endContainer);
+ assert_equals(selection.anchorOffset, range.endOffset);
+ assert_equals(selection.focusNode, range.startContainer);
+ assert_equals(selection.focusOffset, range.startOffset);
+ }, "Offsets match the range for initial selection");
+
+ // Per spec, the direction of the selection remains even if you zap a range
+ // and add a new one.
+ test(function() {
+ selection.removeRange(range);
+ range = document.createRange();
+ p = testDiv.getElementsByTagName("p")[0].firstChild;
+ range.setStart(p, 0);
+ range.setEnd(p, 4);
+ assert_equals(range.toString(), "This");
+ selection.addRange(range);
+ }, "removeRange()/addRange() successful");
+ test(function() {
+ assert_equals(selection.anchorNode, p);
+ assert_equals(selection.focusNode, p);
+ }, "Expected node is selected after remove/addRange()");
+ test(function() {
+ assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [0, 4]);
+ }, "Expected offsets are selected after remove/addRange() (maybe not in order)");
+ test(function() {
+ assert_equals(selection.anchorOffset, 4);
+ assert_equals(selection.focusOffset, 0);
+ }, "Offsets are backwards after remove/addRange()"),
+ test(function() {
+ assert_equals(selection.anchorNode, range.endContainer);
+ assert_equals(selection.anchorOffset, range.endOffset);
+ assert_equals(selection.focusNode, range.startContainer);
+ assert_equals(selection.focusOffset, range.startOffset);
+ }, "Offsets match the range after remove/addRange()");
+
+ // But if you call removeAllRanges(), the direction should reset to
+ // forwards.
+ test(function() {
+ selection.removeAllRanges();
+ range = document.createRange();
+ p = testDiv.getElementsByTagName("p")[2].firstChild;
+ range.setStart(p, 2);
+ range.setEnd(p, 5);
+ assert_equals(range.toString(), "ghi");
+ selection.addRange(range);
+ }, "removeAllRanges() successful");
+ test(function() {
+ assert_equals(selection.anchorNode, p);
+ assert_equals(selection.focusNode, p);
+ }, "Expected node is selected after removeAllRanges()");
+ test(function() {
+ assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [2, 5]);
+ }, "Expected offsets are selected after removeAllRanges() (maybe not in order)");
+ test(function() {
+ assert_equals(selection.anchorOffset, 2);
+ assert_equals(selection.focusOffset, 5);
+ }, "Offsets are forwards after removeAllRanges()");
+ test(function() {
+ assert_equals(selection.anchorNode, range.startContainer);
+ assert_equals(selection.anchorOffset, range.startOffset);
+ assert_equals(selection.focusNode, range.endContainer);
+ assert_equals(selection.focusOffset, range.endOffset);
+ }, "Offsets match the range after removeAllRanges()");
+
+ done();
+}
+</script>
diff --git a/testing/web-platform/tests/selection/drag-disabled-textarea-shadow-dom.html b/testing/web-platform/tests/selection/drag-disabled-textarea-shadow-dom.html
new file mode 100644
index 0000000000..9270cfc5c6
--- /dev/null
+++ b/testing/web-platform/tests/selection/drag-disabled-textarea-shadow-dom.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<!-- Conceptually this is just a crashtest, but we need testharness.js so that testdriver works as expected, see https://github.com/web-platform-tests/wpt/issues/31739 -->
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<link rel=help href="https://bugzilla.mozilla.org/show_bug.cgi?id=1739079">
+<span id="outer"></span>
+<script>
+ const t = async_test("Shouldn't crash when dragging disabled textarea in shadow dom");
+ const outer = document.getElementById("outer");
+ outer.attachShadow({ mode: "open" }).innerHTML = `
+ <span id="inner" style="pointer-events: none">
+ <textarea disabled></textarea>
+ </span>
+ `;
+ const inner = outer.shadowRoot.getElementById("inner");
+ inner.attachShadow({ mode: "open" }).innerHTML = `
+ <div style="display: flex">
+ <slot></slot>
+ </div>
+ `;
+ const textarea = outer.shadowRoot.querySelector("textarea");
+ window.addEventListener("load", t.step_func(function() {
+ const rect = textarea.getBoundingClientRect();
+ new test_driver.Actions()
+ .pointerMove(rect.left + 5, rect.top + 5)
+ .pointerDown()
+ .pointerMove(rect.left, + 50, rect.top + 50)
+ .pointerMove(0, 0)
+ .pointerUp()
+ .send()
+ .then(t.step_func_done(function() {
+ assert_true(true, "Didn't crash nor hang");
+ }));
+ }));
+</script>
diff --git a/testing/web-platform/tests/selection/extend-00.html b/testing/web-platform/tests/selection/extend-00.html
new file mode 100644
index 0000000000..8943524878
--- /dev/null
+++ b/testing/web-platform/tests/selection/extend-00.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Selection extend() tests</title>
+<meta charset=utf-8>
+<meta name=timeout content=long>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script src=extend.js></script>
+<div id=log></div>
+<script>
+"use strict";
+
+testExtendSubSet(0, 20);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/extend-20.html b/testing/web-platform/tests/selection/extend-20.html
new file mode 100644
index 0000000000..b5ca161d98
--- /dev/null
+++ b/testing/web-platform/tests/selection/extend-20.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Selection extend() tests</title>
+<meta charset=utf-8>
+<meta name=timeout content=long>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script src=extend.js></script>
+<div id=log></div>
+<script>
+"use strict";
+
+testExtendSubSet(20, 40);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/extend-40.html b/testing/web-platform/tests/selection/extend-40.html
new file mode 100644
index 0000000000..f0b78d6973
--- /dev/null
+++ b/testing/web-platform/tests/selection/extend-40.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<title>Selection extend() tests</title>
+<meta charset=utf-8>
+<meta name=timeout content=long>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script src=extend.js></script>
+<div id=log></div>
+<script>
+"use strict";
+
+testExtendSubSet(40);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/extend-exception.html b/testing/web-platform/tests/selection/extend-exception.html
new file mode 100644
index 0000000000..67e880fa1c
--- /dev/null
+++ b/testing/web-platform/tests/selection/extend-exception.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+<title>Selection extend() test thrown exceptions</title>
+<meta charset=utf-8>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+test(function() {
+ let div = document.createElement("div");
+ document.body.appendChild(div);
+
+ assert_throws_dom("INVALID_STATE_ERR", function() { getSelection().extend(div) }, "InvalidStateError exception should be thrown for extend() due to no ranges being added to Selection");
+
+ this.add_cleanup(function() { div.remove() });
+}, "InvalidStateError exception is thrown for extend() when no ranges are present in Selection");
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/selection/extend.js b/testing/web-platform/tests/selection/extend.js
new file mode 100644
index 0000000000..d5d199068c
--- /dev/null
+++ b/testing/web-platform/tests/selection/extend.js
@@ -0,0 +1,161 @@
+"use strict";
+
+// Also test a selection with no ranges
+testRanges.unshift("[]");
+
+// Run a subset of all of extend tests.
+// Huge number of tests in a single file causes problems. Each of
+// extend-NN.html runs a part of them.
+//
+// startIndex - Start index in testRanges array
+// optionalEndIndex - End index in testRanges array + 1. If this argument is
+// omitted, testRanges.length is applied.
+function testExtendSubSet(startIndex, optionalEndIndex) {
+ var endIndex = optionalEndIndex === undefined ? testRanges.length : optionalEndIndex;
+ if (startIndex < 0 || startIndex >= testRanges.length)
+ throw "Sanity check: Specified index is invalid.";
+ if (endIndex < 0 || endIndex > testRanges.length)
+ throw "Sanity check: Specified index is invalid.";
+
+ // We test Selections that go both forwards and backwards here. In the
+ // latter case we need to use extend() to force it to go backwards, which is
+ // fair enough, since that's what we're testing. We test collapsed
+ // selections only once.
+ for (var i = startIndex; i < endIndex; i++) {
+ var endpoints = eval(testRanges[i]);
+ // We can't test extend() with unselectable endpoints.
+ if (!isSelectableNode(endpoints[0]) || !isSelectableNode(endpoints[2]))
+ continue;
+ for (var j = 0; j < testPoints.length; j++) {
+ if (endpoints[0] == endpoints[2]
+ && endpoints[1] == endpoints[3]) {
+ // Test collapsed selections only once
+ test(function() {
+ setSelectionForwards(endpoints);
+ testExtend(endpoints, eval(testPoints[j]));
+ }, "extend() with range " + i + " " + testRanges[i]
+ + " and point " + j + " " + testPoints[j]);
+ } else {
+ test(function() {
+ setSelectionForwards(endpoints);
+ testExtend(endpoints, eval(testPoints[j]));
+ }, "extend() forwards with range " + i + " " + testRanges[i]
+ + " and point " + j + " " + testPoints[j]);
+
+ test(function() {
+ setSelectionBackwards(endpoints);
+ testExtend(endpoints, eval(testPoints[j]));
+ }, "extend() backwards with range " + i + " " + testRanges[i]
+ + " and point " + j + " " + testPoints[j]);
+ }
+ }
+ }
+}
+
+function testExtend(endpoints, target) {
+ assert_equals(getSelection().rangeCount, endpoints.length/4,
+ "Sanity check: rangeCount must be correct");
+
+ var node = target[0];
+ var offset = target[1];
+
+ // "If node's root is not the document associated with the context object,
+ // abort these steps."
+ if (!document.contains(node)) {
+ assertSelectionNoChange(function() {
+ selection.extend(node, offset);
+ });
+ return;
+ }
+
+ // "If the context object's range is null, throw an InvalidStateError
+ // exception and abort these steps."
+ if (getSelection().rangeCount == 0) {
+ assert_throws_dom("INVALID_STATE_ERR", function() {
+ selection.extend(node, offset);
+ }, "extend() when rangeCount is 0 must throw InvalidStateError");
+ return;
+ }
+
+ assert_equals(getSelection().getRangeAt(0).startContainer, endpoints[0],
+ "Sanity check: startContainer must be correct");
+ assert_equals(getSelection().getRangeAt(0).startOffset, endpoints[1],
+ "Sanity check: startOffset must be correct");
+ assert_equals(getSelection().getRangeAt(0).endContainer, endpoints[2],
+ "Sanity check: endContainer must be correct");
+ assert_equals(getSelection().getRangeAt(0).endOffset, endpoints[3],
+ "Sanity check: endOffset must be correct");
+
+ // "Let anchor and focus be the context object's anchor and focus, and let
+ // new focus be the boundary point (node, offset)."
+ var anchorNode = getSelection().anchorNode;
+ var anchorOffset = getSelection().anchorOffset;
+ var focusNode = getSelection().focusNode;
+ var focusOffset = getSelection().focusOffset;
+
+ // "Let new range be a new range."
+ //
+ // We'll always be setting either new range's start or its end to new
+ // focus, so we'll always throw at some point. Test that now.
+ //
+ // From DOM4's "set the start or end of a range": "If node is a doctype,
+ // throw an "InvalidNodeTypeError" exception and terminate these steps."
+ if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function() {
+ selection.extend(node, offset);
+ }, "extend() to a doctype must throw InvalidNodeTypeError");
+ return;
+ }
+
+ // From DOM4's "set the start or end of a range": "If offset is greater
+ // than node's length, throw an "IndexSizeError" exception and terminate
+ // these steps."
+ //
+ // FIXME: We should be casting offset to an unsigned int per WebIDL. Until
+ // we do, we need the offset < 0 check too.
+ if (offset < 0 || offset > getNodeLength(node)) {
+ assert_throws_dom("INDEX_SIZE_ERR", function() {
+ selection.extend(node, offset);
+ }, "extend() to an offset that's greater than node length (" + getNodeLength(node) + ") must throw IndexSizeError");
+ return;
+ }
+
+ // Now back to the editing spec.
+ var originalRange = getSelection().getRangeAt(0);
+
+ // "If node's root is not the same as the context object's range's root,
+ // set new range's start and end to (node, offset)."
+ //
+ // "Otherwise, if anchor is before or equal to new focus, set new range's
+ // start to anchor, then set its end to new focus."
+ //
+ // "Otherwise, set new range's start to new focus, then set its end to
+ // anchor."
+ //
+ // "Set the context object's range to new range."
+ //
+ // "If new focus is before anchor, set the context object's direction to
+ // backwards. Otherwise, set it to forwards."
+ //
+ // The upshot of all these is summed up by just testing the anchor and
+ // offset.
+ getSelection().extend(node, offset);
+
+ if (furthestAncestor(anchorNode) == furthestAncestor(node)) {
+ assert_equals(getSelection().anchorNode, anchorNode,
+ "anchorNode must not change if the node passed to extend() has the same root as the original range");
+ assert_equals(getSelection().anchorOffset, anchorOffset,
+ "anchorOffset must not change if the node passed to extend() has the same root as the original range");
+ } else {
+ assert_equals(getSelection().anchorNode, node,
+ "anchorNode must be the node passed to extend() if it has a different root from the original range");
+ assert_equals(getSelection().anchorOffset, offset,
+ "anchorOffset must be the offset passed to extend() if the node has a different root from the original range");
+ }
+ assert_equals(getSelection().focusNode, node,
+ "focusNode must be the node passed to extend()");
+ assert_equals(getSelection().focusOffset, offset,
+ "focusOffset must be the offset passed to extend()");
+ assert_not_equals(getSelection().getRangeAt(0), originalRange,
+ "extend() must replace any existing range with a new one, not mutate the existing one");
+}
diff --git a/testing/web-platform/tests/selection/getRangeAt.html b/testing/web-platform/tests/selection/getRangeAt.html
new file mode 100644
index 0000000000..a84ad46f22
--- /dev/null
+++ b/testing/web-platform/tests/selection/getRangeAt.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>The getRangeAt method</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-getrangeat">
+<body>
+<script>
+const sel = getSelection();
+
+test(function() {
+ var range = document.createRange();
+ sel.addRange(range);
+ assert_throws_dom("INDEX_SIZE_ERR", function() { sel.getRangeAt(-1); })
+ assert_throws_dom("INDEX_SIZE_ERR", function() { sel.getRangeAt(1); })
+}, "Only supports index 0");
+
+test(() => {
+ sel.removeAllRanges();
+ assert_throws_dom("INDEX_SIZE_ERR", () => sel.getRangeAt(0))
+}, "Throws when with no range");
+
+test(() => {
+ sel.addRange(document.createRange());
+ assert_equals(sel.getRangeAt(0), sel.getRangeAt(0));
+}, "Returns the same range object when with no selection change");
+
+test(() => {
+ const range = sel.getRangeAt(0);
+ sel.collapse(document.body);
+ assert_not_equals(range, sel.getRangeAt(0));
+}, "Returns a different range object when with selection change");
+</script>
diff --git a/testing/web-platform/tests/selection/getSelection.html b/testing/web-platform/tests/selection/getSelection.html
new file mode 100644
index 0000000000..ea119f2fc4
--- /dev/null
+++ b/testing/web-platform/tests/selection/getSelection.html
@@ -0,0 +1,160 @@
+<!doctype html>
+<title>getSelection() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+// TODO: Figure out more places where defaultView is or is not guaranteed to be
+// null, and test whether getSelection() is null.
+//
+// TODO: Figure out a good way to test display: none iframes.
+
+test(function() {
+ // Sanity checks like this are to flag known browser bugs with clearer
+ // error messages, instead of throwing inscrutable exceptions.
+ assert_true("Selection" in window,
+ "Sanity check: window must have Selection property");
+
+ assert_true(window.getSelection() instanceof Selection);
+}, "window.getSelection() instanceof Selection");
+
+test(function() {
+ assert_equals(window.getSelection(), window.getSelection());
+}, "window.getSelection() === window.getSelection()");
+
+test(function() {
+ assert_true("Selection" in window,
+ "Sanity check: window must have Selection property");
+ // This sanity check (which occurs a number of times below, too) is because
+ // document.getSelection() is supposed to return null if defaultView is
+ // null, so we need to figure out whether defaultView is null or not before
+ // we can make correct assertions about getSelection().
+ assert_not_equals(document.defaultView, null,
+ "Sanity check: document.defaultView must not be null");
+
+ assert_equals(typeof document.getSelection(), "object",
+ "document.getSelection() must be an object");
+ assert_true(document.getSelection() instanceof Selection);
+}, "document.getSelection() instanceof Selection");
+
+test(function() {
+ assert_not_equals(document.defaultView, null,
+ "Sanity check: document.defaultView must not be null");
+ assert_equals(document.getSelection(), document.getSelection());
+}, "document.getSelection() === document.getSelection()");
+
+test(function() {
+ assert_not_equals(document.defaultView, null,
+ "Sanity check: document.defaultView must not be null");
+ assert_equals(window.getSelection(), document.getSelection());
+}, "window.getSelection() === document.getSelection()");
+
+// "Each selection is associated with a single range, which may be null and is
+// initially null."
+//
+// "The rangeCount attribute must return 0 if the context object's range is
+// null, otherwise 1."
+test(function() {
+ assert_equals(window.getSelection().rangeCount, 0,
+ "window.getSelection().rangeCount must initially be 0");
+ assert_equals(typeof document.getSelection(), "object",
+ "Sanity check: document.getSelection() must be an object");
+ assert_equals(document.getSelection().rangeCount, 0,
+ "document.getSelection().rangeCount must initially be 0");
+}, "Selection's range must initially be null");
+
+test(function() {
+ var doc = document.implementation.createHTMLDocument("");
+ assert_equals(doc.defaultView, null,
+ "Sanity check: defaultView of created HTML document must be null");
+ assert_equals(doc.getSelection(), null);
+}, "getSelection() on HTML document with null defaultView must be null");
+
+test(function() {
+ var xmlDoc = document.implementation.createDocument(null, "", null);
+
+ assert_true("getSelection" in xmlDoc, "XML document must have getSelection()");
+
+ assert_equals(xmlDoc.defaultView, null,
+ "Sanity check: defaultView of created XML document must be null");
+ assert_equals(xmlDoc.getSelection(), null);
+}, "getSelection() on XML document with null defaultView must be null");
+
+
+// Run a bunch of iframe tests, once immediately after the iframe is appended
+// to the document and once onload. This makes a difference, because browsers
+// differ (at the time of this writing) in whether they load about:blank in
+// iframes synchronously or not. Per the HTML spec, there must be a browsing
+// context associated with the iframe as soon as it's appended to the document,
+// so there should be a selection too.
+var iframe = document.createElement("iframe");
+add_completion_callback(function() {
+ document.body.removeChild(iframe);
+});
+
+var testDescs = [];
+var testFuncs = [];
+testDescs.push("window.getSelection() instanceof Selection in an iframe");
+testFuncs.push(function() {
+ assert_true("Selection" in iframe.contentWindow,
+ "Sanity check: window must have Selection property");
+ assert_not_equals(iframe.contentWindow.document.defaultView, null,
+ "Sanity check: document.defaultView must not be null");
+ assert_not_equals(iframe.contentWindow.getSelection(), null,
+ "window.getSelection() must not be null");
+ assert_true(iframe.contentWindow.getSelection() instanceof iframe.contentWindow.Selection);
+});
+
+testDescs.push("document.getSelection() instanceof Selection in an iframe");
+testFuncs.push(function() {
+ assert_true("Selection" in iframe.contentWindow,
+ "Sanity check: window must have Selection property");
+ assert_not_equals(iframe.contentDocument.defaultView, null,
+ "Sanity check: document.defaultView must not be null");
+ assert_not_equals(iframe.contentDocument.getSelection(), null,
+ "document.getSelection() must not be null");
+ assert_equals(typeof iframe.contentDocument.getSelection(), "object",
+ "document.getSelection() must be an object");
+ assert_true(iframe.contentDocument.getSelection() instanceof iframe.contentWindow.Selection);
+});
+
+testDescs.push("window.getSelection() === document.getSelection() in an iframe");
+testFuncs.push(function() {
+ assert_not_equals(iframe.contentDocument.defaultView, null,
+ "Sanity check: document.defaultView must not be null");
+ assert_equals(iframe.contentWindow.getSelection(), iframe.contentDocument.getSelection());
+});
+
+testDescs.push("getSelection() inside and outside iframe must return different objects");
+testFuncs.push(function() {
+ assert_not_equals(iframe.contentWindow.getSelection(), getSelection());
+});
+
+testDescs.push("getSelection() on HTML document with null defaultView must be null inside an iframe");
+testFuncs.push(function() {
+ var doc = iframe.contentDocument.implementation.createHTMLDocument("");
+ assert_equals(doc.defaultView, null,
+ "Sanity check: defaultView of created HTML document must be null");
+ assert_equals(doc.getSelection(), null);
+});
+
+var asyncTests = [];
+testDescs.forEach(function(desc) {
+ asyncTests.push(async_test(desc + " onload"));
+});
+
+iframe.onload = function() {
+ asyncTests.forEach(function(t, i) {
+ t.step(testFuncs[i]);
+ t.done();
+ });
+};
+
+document.body.appendChild(iframe);
+
+testDescs.forEach(function(desc, i) {
+ test(testFuncs[i], desc + " immediately after appendChild");
+});
+</script>
diff --git a/testing/web-platform/tests/selection/idlharness.window.js b/testing/web-platform/tests/selection/idlharness.window.js
new file mode 100644
index 0000000000..543fcb3eff
--- /dev/null
+++ b/testing/web-platform/tests/selection/idlharness.window.js
@@ -0,0 +1,18 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+// https://w3c.github.io/selection-api/
+
+idl_test(
+ ['selection-api'],
+ ['html', 'dom'],
+ idlArray => {
+ idlArray.add_objects({
+ Window: ['window'],
+ Document: ['document'],
+ Selection: ['getSelection()'],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/selection/isCollapsed.html b/testing/web-platform/tests/selection/isCollapsed.html
new file mode 100644
index 0000000000..819a3e297a
--- /dev/null
+++ b/testing/web-platform/tests/selection/isCollapsed.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>Selection.isCollapsed tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+test(function() {
+ selection.removeAllRanges();
+ assert_true(selection.isCollapsed, "isCollapsed must be true if both anchor and focus are null");
+}, "Empty selection");
+
+for (var i = 0; i < testRanges.length; i++) {
+ var endpoints = eval(testRanges[i]);
+ if (!isSelectableNode(endpoints[0]) || !isSelectableNode(endpoints[2]))
+ continue;
+ test(function() {
+ selection.removeAllRanges();
+ var range = ownerDocument(endpoints[0]).createRange();
+ range.setStart(endpoints[0], endpoints[1]);
+ range.setEnd(endpoints[2], endpoints[3]);
+ selection.addRange(range);
+
+ assert_equals(selection.isCollapsed,
+ endpoints[0] === endpoints[2] && endpoints[1] === endpoints[3],
+ "Value of isCollapsed");
+ }, "Range " + i + " " + testRanges[i]);
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/modify-extend-word-trailing-inline-block.tentative.html b/testing/web-platform/tests/selection/modify-extend-word-trailing-inline-block.tentative.html
new file mode 100644
index 0000000000..1eaa6ec68d
--- /dev/null
+++ b/testing/web-platform/tests/selection/modify-extend-word-trailing-inline-block.tentative.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Selection.modify: Extending a word towards the end of a line ended by an inline-block.</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1698705">
+
+<div id="block">This is a line<div style="display:inline-block"></div></div>
+
+<script>
+test(function() {
+ const selection = getSelection();
+ const block = document.getElementById("block");
+ selection.collapse(block.childNodes[0], 0);
+ assert_equals(selection.toString(), "", "Empty selection at beginning");
+ for (let i = 0; i < 4; ++i) {
+ selection.modify("extend", "forward", "word");
+ }
+ assert_equals(selection.toString(), "This is a line", "Should've found the four words");
+});
+</script>
diff --git a/testing/web-platform/tests/selection/modify-line-flex-column.tentative.html b/testing/web-platform/tests/selection/modify-line-flex-column.tentative.html
new file mode 100644
index 0000000000..984bb81913
--- /dev/null
+++ b/testing/web-platform/tests/selection/modify-line-flex-column.tentative.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Selection.modify(): line navigation on a column-oriented flex container</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<div id="container" style="display: flex; flex-direction: column; font-family: monospace">
+ <p id="one">One</p>
+ <p id="two">Two</p>
+ <p id="three">Three</p>
+ <p id="four">Four</p>
+</div>
+<script>
+const selection = getSelection();
+test(() => {
+ // Put the caret in the second character in "One"
+ selection.collapse(one.childNodes[0], 2);
+ // Move forward a line, selection should be after "Tw".
+ selection.modify("extend", "forward", "line")
+ assert_equals(selection.focusNode, two.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ // Move forward another line, selection should be after "Th".
+ selection.modify("extend", "forward", "line")
+ assert_equals(selection.focusNode, three.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ assert_equals(selection.toString().replaceAll("\r\n", "\n"), "e\n\nTwo\n\nTh");
+}, "forward");
+
+test(() => {
+ // Put the caret in the second character in "Three"
+ selection.collapse(three.childNodes[0], 2);
+ // Move backward a line, selection should be after "Tw".
+ selection.modify("extend", "backward", "line")
+ assert_equals(selection.focusNode, two.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ // Move backward another line, selection should be after "On".
+ selection.modify("extend", "backward", "line")
+ assert_equals(selection.focusNode, one.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ assert_equals(selection.toString().replaceAll("\r\n", "\n"), "e\n\nTwo\n\nTh");
+}, "backward");
+</script>
diff --git a/testing/web-platform/tests/selection/modify-line-flex-row.tentative.html b/testing/web-platform/tests/selection/modify-line-flex-row.tentative.html
new file mode 100644
index 0000000000..28b22eba03
--- /dev/null
+++ b/testing/web-platform/tests/selection/modify-line-flex-row.tentative.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Selection.modify(): line navigation on a row-oriented flex container</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<style>
+ p { display: flex }
+</style>
+<div id="container" style="font-family: monospace">
+ <p><div></div><div id="one">One</div></p>
+ <p><div></div><div id="two">Two</div></p>
+ <p><div></div><div id="three">Three</div></p>
+ <p><div></div><div id="four">Four</div></p>
+</div>
+<script>
+const selection = getSelection();
+test(() => {
+ // Put the caret in the second character in "One"
+ selection.collapse(one.childNodes[0], 2);
+ // Move forward a line, selection should be after "Tw".
+ selection.modify("extend", "forward", "line")
+ assert_equals(selection.focusNode, two.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ // Move forward another line, selection should be after "Th".
+ selection.modify("extend", "forward", "line")
+ assert_equals(selection.focusNode, three.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ assert_equals(selection.toString().replaceAll("\r\n", "\n"), "e\n\nTwo\n\nTh");
+}, "forward");
+
+test(() => {
+ // Put the caret in the second character in "Three"
+ selection.collapse(three.childNodes[0], 2);
+ // Move backward a line, selection should be after "Tw".
+ selection.modify("extend", "backward", "line")
+ assert_equals(selection.focusNode, two.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ // Move backward another line, selection should be after "On".
+ selection.modify("extend", "backward", "line")
+ assert_equals(selection.focusNode, one.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ assert_equals(selection.toString().replaceAll("\r\n", "\n"), "e\n\nTwo\n\nTh");
+}, "backward");
+</script>
diff --git a/testing/web-platform/tests/selection/modify-line-grid-basic.tentative.html b/testing/web-platform/tests/selection/modify-line-grid-basic.tentative.html
new file mode 100644
index 0000000000..faadd93c64
--- /dev/null
+++ b/testing/web-platform/tests/selection/modify-line-grid-basic.tentative.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Selection.modify(): line navigation on a grid container</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<div id="container" style="display: grid; font-family: monospace">
+ <p id="one">One</p>
+ <p id="two">Two</p>
+ <p id="three">Three</p>
+ <p id="four">Four</p>
+</div>
+<script>
+const selection = getSelection();
+test(() => {
+ // Put the caret in the second character in "One"
+ selection.collapse(one.childNodes[0], 2);
+ // Move forward a line, selection should be after "Tw".
+ selection.modify("extend", "forward", "line")
+ assert_equals(selection.focusNode, two.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ // Move forward another line, selection should be after "Th".
+ selection.modify("extend", "forward", "line")
+ assert_equals(selection.focusNode, three.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ assert_equals(selection.toString().replaceAll("\r\n", "\n"), "e\n\nTwo\n\nTh");
+}, "forward");
+
+test(() => {
+ // Put the caret in the second character in "Three"
+ selection.collapse(three.childNodes[0], 2);
+ // Move backward a line, selection should be after "Tw".
+ selection.modify("extend", "backward", "line")
+ assert_equals(selection.focusNode, two.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ // Move backward another line, selection should be after "On".
+ selection.modify("extend", "backward", "line")
+ assert_equals(selection.focusNode, one.childNodes[0]);
+ assert_equals(selection.focusOffset, 2);
+
+ assert_equals(selection.toString().replaceAll("\r\n", "\n"), "e\n\nTwo\n\nTh");
+}, "backward");
+</script>
diff --git a/testing/web-platform/tests/selection/modify.tentative.html b/testing/web-platform/tests/selection/modify.tentative.html
new file mode 100644
index 0000000000..37231571ed
--- /dev/null
+++ b/testing/web-platform/tests/selection/modify.tentative.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Selection.modify() tests</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<div>Test, these are <strong id="strong"> strong </strong> <em id="em"> italic </em> normal.</div>
+
+<pre id="preLinefeed">
+foo
+bar
+</pre>
+
+<pre id="preBr">
+foo<br>bar
+</pre>
+
+<pre id="preLinefeedBr">
+foo
+<br>
+bar
+</pre>
+
+<script>
+const selection = getSelection();
+test(() => {
+ selection.collapse(strong.childNodes[0], 4);
+ selection.modify("extend", "backward", "word")
+ assert_equals(selection.focusNode, strong.childNodes[0]);
+ assert_equals(selection.focusOffset, 1);
+ selection.collapse(em.childNodes[0], 4);
+ selection.modify("extend", "backward", "word")
+ assert_equals(selection.focusNode, em.childNodes[0]);
+ assert_equals(selection.focusOffset, 1);
+}, "Stop at previous word boundary when whitespaces are trimmed");
+
+test(() => {
+ const preLinefeed = document.getElementById("preLinefeed");
+ const textChild = preLinefeed.childNodes[0];
+ selection.collapse(textChild, 3);
+ selection.modify("move", "forward", "character");
+ assert_equals(selection.focusNode, textChild);
+ assert_equals(selection.focusOffset, 4);
+}, "Jump linefeed forward");
+
+test(() => {
+ const preLinefeed = document.getElementById("preLinefeed");
+ const textChild = preLinefeed.childNodes[0];
+ selection.collapse(textChild, 4);
+ selection.modify("move", "backward", "character");
+ assert_equals(selection.focusNode, textChild);
+ assert_equals(selection.focusOffset, 3);
+}, "Jump linefeed backward");
+
+test(() => {
+ const preBr = document.getElementById("preBr");
+ const [firstTextChild, br, secondTextChild] = preBr.childNodes;
+ selection.collapse(firstTextChild, 3);
+ selection.modify("move", "forward", "character");
+ assert_equals(selection.focusNode, secondTextChild);
+ assert_equals(selection.focusOffset, 0);
+}, "Jump <br> forward");
+
+test(() => {
+ const preBr = document.getElementById("preBr");
+ const [firstTextChild, br, secondTextChild] = preBr.childNodes;
+ selection.collapse(secondTextChild, 0);
+ selection.modify("move", "backward", "character");
+ assert_equals(selection.focusNode, firstTextChild);
+ assert_equals(selection.focusOffset, 3);
+}, "Jump <br> backward");
+
+test(() => {
+ const preLinefeedBr = document.getElementById("preLinefeedBr");
+ selection.collapse(preLinefeedBr, 1);
+ selection.modify("move", "forward", "character");
+ const secondTextChild = preLinefeedBr.childNodes[2];
+ assert_equals(selection.focusNode, secondTextChild);
+ assert_equals(selection.focusOffset, 0);
+}, "Jump <br> forward which follows a linefeed");
+
+test(() => {
+ const preLinefeedBr = document.getElementById("preLinefeedBr");
+ selection.collapse(preLinefeedBr, 2);
+ selection.modify("move", "backward", "character");
+ const textChild = preLinefeedBr.childNodes[0];
+ assert_equals(selection.focusNode, textChild);
+ assert_equals(selection.focusOffset, textChild.textContent.length);
+}, "Jump <br> backward which follows a linefeed");
+</script>
diff --git a/testing/web-platform/tests/selection/removeAllRanges.html b/testing/web-platform/tests/selection/removeAllRanges.html
new file mode 100644
index 0000000000..026280d6b8
--- /dev/null
+++ b/testing/web-platform/tests/selection/removeAllRanges.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<title>Selection.removeAllRanges()/empty() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+// Also test a selection with no ranges
+testRanges.unshift("[]");
+
+var range = rangeFromEndpoints([paras[0].firstChild, 0, paras[0].firstChild, 1]);
+
+function testRange(rangeDesc, method) {
+ var endpoints = eval(testRanges[i]);
+ if (endpoints.length && (!isSelectableNode(endpoints[0]) ||
+ !isSelectableNode(endpoints[2]))) {
+ return;
+ }
+ test(function() {
+ setSelectionForwards(endpoints);
+ selection[method]();
+ assert_equals(selection.rangeCount, 0,
+ "After " + method + "(), rangeCount must be 0");
+ // Test that it's forwards
+ selection.addRange(range);
+ assert_equals(selection.anchorOffset, selection.getRangeAt(0).startOffset,
+ "After " + method + "(), addRange() must be forwards, so anchorOffset must equal startOffset rather than endOffset");
+ assert_equals(selection.focusOffset, selection.getRangeAt(0).endOffset,
+ "After " + method + "(), addRange() must be forwards, so focusOffset must equal endOffset rather than startOffset");
+ }, method + " on " + rangeDesc + " forwards");
+
+ // Copy-pasted from above
+ test(function() {
+ setSelectionBackwards(endpoints);
+ selection[method]();
+ assert_equals(selection.rangeCount, 0,
+ "After " + method + "(), rangeCount must be 0");
+ // Test that it's forwards
+ selection.addRange(range);
+ assert_equals(selection.anchorOffset, selection.getRangeAt(0).startOffset,
+ "After " + method + "(), addRange() must be forwards, so anchorOffset must equal startOffset rather than endOffset");
+ assert_equals(selection.focusOffset, selection.getRangeAt(0).endOffset,
+ "After " + method + "(), addRange() must be forwards, so focusOffset must equal endOffset rather than startOffset");
+ }, method + " on " + rangeDesc + " backwards");
+}
+
+for (var i = 0; i < testRanges.length; i++) {
+ testRange(testRanges[i], "removeAllRanges");
+ testRange(testRanges[i], "empty");
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/removeRange.html b/testing/web-platform/tests/selection/removeRange.html
new file mode 100644
index 0000000000..8dcd6d2c07
--- /dev/null
+++ b/testing/web-platform/tests/selection/removeRange.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>Selection.removeRange tests</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="common.js"></script>
+<script>
+"use strict";
+
+testRanges.forEach(function(rangeData, index) {
+ var endpoints = eval(rangeData);
+ if (!isSelectableNode(endpoints[0]) || !isSelectableNode(endpoints[2]))
+ return;
+ test(function() {
+ var selection = getSelection();
+ selection.removeAllRanges();
+ var range = ownerDocument(endpoints[0]).createRange();
+ range.setStart(endpoints[0], endpoints[1]);
+ range.setEnd(endpoints[2], endpoints[3]);
+
+ selection.addRange(range);
+ assert_equals(selection.rangeCount, 1);
+ selection.removeRange(range);
+ assert_equals(selection.rangeCount, 0, 'Range should be correctly removed.');
+ assert_equals(selection.anchorNode, null);
+ assert_equals(selection.focusNode, null);
+
+ selection.addRange(range);
+ assert_equals(selection.rangeCount, 1);
+ var equivalentRange = ownerDocument(endpoints[0]).createRange();
+ equivalentRange.setStart(endpoints[0], endpoints[1]);
+ equivalentRange.setEnd(endpoints[2], endpoints[3]);
+ assert_throws_dom("NotFoundError",
+ function() { selection.removeRange(equivalentRange) },
+ "Removing a different range should throw");
+ assert_equals(selection.rangeCount, 1, 'Equivalent Range should not remove the registered Range.');
+
+ }, 'removeRange() with Range ' + index);
+});
+
+test(function() {
+ var selection = getSelection();
+ assert_throws_js(TypeError, function() { selection.removeRange(null); });
+ assert_throws_js(TypeError, function() { selection.removeRange(selection); });
+}, 'removeRange() argument is non-optional Range');
+</script>
+</body>
diff --git a/testing/web-platform/tests/selection/script-and-style-elements.html b/testing/web-platform/tests/selection/script-and-style-elements.html
new file mode 100644
index 0000000000..b75665917d
--- /dev/null
+++ b/testing/web-platform/tests/selection/script-and-style-elements.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Selection: STYLE and SCRIPT elements should be included in Selection.toString() if they are display!=none</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+window.onload = function() {
+ test(function() {
+ var selection = window.getSelection();
+ var p1 = document.getElementById("p1");
+
+ var range = document.createRange();
+ test_block_script();
+ range.selectNode(p1);
+ selection.addRange(range);
+ assert_equals(selection.toString().replace(/\r\n/g, "\n"), '\nstyle { display:block; color: green; } script { color: blue; }\nfunction test_block_script() { let pre = document.createElement("pre"); pre.append(document.createTextNode("PASS")); document.getElementById("p1").append(pre); }\n\nPASS');
+ });
+};
+</script>
+<div id=log></div>
+<div id="p1">
+<style style="display:none">#not_included{}</style>
+<style>
+ style { display:block; color: green; }
+ script { color: blue; }
+</style>
+<script>function not_included(){}</script>
+<script style="display:block">
+function test_block_script() {
+ let pre = document.createElement("pre");
+ pre.append(document.createTextNode("PASS"));
+ document.getElementById("p1").append(pre);
+}
+</script>
+</div>
+</body>
+</html>
diff --git a/testing/web-platform/tests/selection/select-end-of-line-image.tentative.html b/testing/web-platform/tests/selection/select-end-of-line-image.tentative.html
new file mode 100644
index 0000000000..572296c443
--- /dev/null
+++ b/testing/web-platform/tests/selection/select-end-of-line-image.tentative.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+ <meta charset="utf-8">
+ <title>Selection: Select the image at the end of the line</title>
+ <link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com">
+ <link rel="author" title="Mozilla" href="https://www.mozilla.org/">
+ <link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1745435">
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src='/resources/testdriver-vendor.js'></script>
+
+ <style>
+ img {
+ inline-size: 100px;
+ block-size: 20px;
+ background: orange;
+ }
+ </style>
+
+ <body>
+ You shouldn't see an orange image at the end of this line
+ <img id="target" src='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><text>hello</text></svg>'>
+ </body>
+
+ <script>
+ promise_test(async function() {
+ let target = document.getElementById("target");
+ let actions = new test_driver.Actions();
+
+ // Move the pointer from the left edge of the image, to the right for about
+ // 3/4 width of the image. This should be sufficient to select the image.
+ await actions.pointerMove(0, 10, {origin: target})
+ .pointerDown()
+ .pointerMove(75, 10, {origin: target})
+ .pointerUp()
+ .send();
+
+ // Delete the image to verify the image is selected. It's tricky to verify
+ // it using Range API.
+ window.getSelection().deleteFromDocument();
+
+ assert_equals(document.getElementById("target"), null,
+ "The image should be selected and then deleted.")
+ }, "Select image at the end of the line.");
+ </script>
+</html>
diff --git a/testing/web-platform/tests/selection/selectAllChildren.html b/testing/web-platform/tests/selection/selectAllChildren.html
new file mode 100644
index 0000000000..9a472a4b2f
--- /dev/null
+++ b/testing/web-platform/tests/selection/selectAllChildren.html
@@ -0,0 +1,64 @@
+<!doctype html>
+<title>Selection.selectAllChildren tests</title>
+<meta name="timeout" content="long">
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+testRanges.unshift("[]");
+
+for (var i = 0; i < testRanges.length; i++) {
+ var endpoints = eval(testRanges[i]);
+
+ for (var j = 0; j < testNodes.length; j++) {
+ var node = eval(testNodes[j]);
+
+ test(function() {
+ setSelectionForwards(endpoints);
+ var originalRange = getSelection().rangeCount
+ ? getSelection().getRangeAt(0)
+ : null;
+
+ if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function() {
+ selection.selectAllChildren(node);
+ }, "selectAllChildren() on a DocumentType must throw InvalidNodeTypeError");
+ return;
+ }
+
+ selection.selectAllChildren(node);
+ if (!document.contains(node)) {
+ if (originalRange) {
+ assert_equals(getSelection().getRangeAt(0), originalRange,
+ "selectAllChildren must do nothing");
+ } else {
+ assert_equals(getSelection().rangeCount, 0,
+ "selectAllChildren must do nothing");
+ }
+ return;
+ }
+ // This implicitly tests that the selection is forwards, by using
+ // anchorOffset/focusOffset instead of getRangeAt.
+ assert_equals(selection.rangeCount, 1,
+ "After selectAllChildren, rangeCount must be 1");
+ assert_equals(selection.anchorNode, node,
+ "After selectAllChildren, anchorNode must be the given node");
+ assert_equals(selection.anchorOffset, 0,
+ "After selectAllChildren, anchorOffset must be 0");
+ assert_equals(selection.focusNode, node,
+ "After selectAllChildren, focusNode must be the given node");
+ assert_equals(selection.focusOffset, node.childNodes.length,
+ "After selectAllChildren, focusOffset must be the given node's number of children");
+ if (originalRange) {
+ assert_not_equals(getSelection().getRangeAt(0), originalRange,
+ "selectAllChildren must replace any existing range, not mutate it");
+ }
+ }, "Range " + i + " " + testRanges[i] + ", node " + j + " " + testNodes[j]);
+ }
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/selection-select-all-move-input-crash.html b/testing/web-platform/tests/selection/selection-select-all-move-input-crash.html
new file mode 100644
index 0000000000..6f692dc5e7
--- /dev/null
+++ b/testing/web-platform/tests/selection/selection-select-all-move-input-crash.html
@@ -0,0 +1,10 @@
+<script>
+ function start() {
+ document.execCommand('selectAll', false)
+ document.documentElement.appendChild(document.getElementById('input'))
+ }
+</script>
+<body onload="start()">
+<input id="input" autofocus>
+<canvas contenteditable="true" hidden></canvas>
+</body>
diff --git a/testing/web-platform/tests/selection/selection-shadow-dom-crash-print.html b/testing/web-platform/tests/selection/selection-shadow-dom-crash-print.html
new file mode 100644
index 0000000000..cf62609173
--- /dev/null
+++ b/testing/web-platform/tests/selection/selection-shadow-dom-crash-print.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>Printing with odd selections doesn't crash</title>
+<!-- Really a crashtest but since we can't really have print crashtests, we assert that we print something -->
+<link rel="mismatch" href="/css/reference/blank.html">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1672856">
+<script>
+function go() {
+ var x = document.getSelection()
+ x.extend(b)
+ x.modify("move", "left", "word")
+ a.remove()
+}
+</script>
+<body onload=go()>
+<audio id="a" controls="">
+<progress contentEditable>
+</audio>
+<li id="b">x</li>
diff --git a/testing/web-platform/tests/selection/setBaseAndExtent.html b/testing/web-platform/tests/selection/setBaseAndExtent.html
new file mode 100644
index 0000000000..13108bb506
--- /dev/null
+++ b/testing/web-platform/tests/selection/setBaseAndExtent.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<title>Selection.setBaseAndExtent() tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+for (var i = 0; i < testRanges.length; i++) {
+ test(function() {
+ var data = eval(testRanges[i]);
+ selection.removeAllRanges();
+ selection.setBaseAndExtent(data[0], data[1], data[2], data[3]);
+ if (!document.contains(data[0]) || !document.contains(data[2])) {
+ assert_equals(selection.rangeCount, 0);
+ return;
+ }
+ assert_equals(selection.rangeCount, 1,
+ "selection.rangeCount must equal 1");
+ assert_equals(selection.anchorNode, data[0],
+ "anchorNode must equal the requested anchor node");
+ assert_equals(selection.anchorOffset, data[1],
+ "anchorOffset must equal the requested anchor offset");
+ assert_equals(selection.focusNode, data[2],
+ "focusNode must equal the requested focus node");
+ assert_equals(selection.focusOffset, data[3],
+ "focusOffset must equal the requested focus offset");
+ }, "Range " + i + " " + testRanges[i] + " setBaseAndExtent()");
+
+ test(function() {
+ var data = eval(testRanges[i]);
+ selection.removeAllRanges();
+ selection.setBaseAndExtent(data[2], data[3], data[0], data[1]);
+ if (!document.contains(data[0]) || !document.contains(data[2])) {
+ assert_equals(selection.rangeCount, 0);
+ return;
+ }
+ assert_equals(selection.rangeCount, 1,
+ "selection.rangeCount must equal 1");
+ assert_equals(selection.anchorNode, data[2],
+ "anchorNode must equal the requested focus node");
+ assert_equals(selection.anchorOffset, data[3],
+ "anchorOffset must equal the requested focus offset");
+ assert_equals(selection.focusNode, data[0],
+ "focusNode must equal the requested anchor node");
+ assert_equals(selection.focusOffset, data[1],
+ "focusOffset must equal the requested anchor offset");
+ }, "Reverse range " + i + " " + testRanges[i] + " setBaseAndExtent()");
+}
+
+test(function() {
+ var title = document.getElementsByTagName('title')[0];
+ try {
+ selection.setBaseAndExtent(title, 0, title, 99);
+ assert_true(false, "focus offset, must throw an IndexSizeError exception")
+ } catch (e) {
+ assert_equals(e.name, "IndexSizeError", "focus offset, got an IndexSizeError exception")
+ }
+ try {
+ selection.setBaseAndExtent(title, 99, title, 0);
+ assert_true(false, "anchor offset, must throw an IndexSizeError exception")
+ } catch (e) {
+ assert_equals(e.name, "IndexSizeError", "anchor offset, got an IndexSizeError exception")
+ }
+}, "setBaseAndExtent() with index larger than length");
+
+test(function() {
+ var title = document.getElementsByTagName('title')[0];
+ try {
+ selection.setBaseAndExtent(title, 0, title, -1);
+ assert_true(false, "focus offset, must throw an IndexSizeError exception")
+ } catch (e) {
+ assert_equals(e.name, "IndexSizeError", "focus offset, got an IndexSizeError exception")
+ }
+ try {
+ selection.setBaseAndExtent(title, -1, title, 0);
+ assert_true(false, "anchor offset, must throw an IndexSizeError exception")
+ } catch (e) {
+ assert_equals(e.name, "IndexSizeError", "anchor offset, got an IndexSizeError exception")
+ }
+}, "setBaseAndExtent() with negative index");
+
+test(function() {
+ var title = document.getElementsByTagName('title')[0];
+ try {
+ selection.setBaseAndExtent(title, 0, null, 0);
+ assert_true(false, "focus node, must throw an TypeError exception")
+ } catch (e) {
+ assert_equals(e.name, "TypeError", "focus node, got an TypeError exception")
+ }
+ try {
+ selection.setBaseAndExtent(null, 0, title, 0);
+ assert_true(false, "anchor node, must throw an TypeError exception")
+ } catch (e) {
+ assert_equals(e.name, "TypeError", "anchor node, got an TypeError exception")
+ }
+ try {
+ selection.setBaseAndExtent(null, 0, null, 0);
+ assert_true(false, "both nodes, must throw an TypeError exception")
+ } catch (e) {
+ assert_equals(e.name, "TypeError", "both nodes, got an TypeError exception")
+ }
+}, "setBaseAndExtent() with null nodes");
+
+test(function() {
+ var title = document.getElementsByTagName('title')[0];
+ try {
+ selection.setBaseAndExtent(title, 0, title);
+ assert_true(false, "focus offset, must throw an TypeError exception")
+ } catch (e) {
+ assert_equals(e.name, "TypeError", "focus offset, got an TypeError exception")
+ }
+ try {
+ selection.setBaseAndExtent(title, 0);
+ assert_true(false, "focus node, must throw an TypeError exception")
+ } catch (e) {
+ assert_equals(e.name, "TypeError", "focus node, got an TypeError exception")
+ }
+ try {
+ selection.setBaseAndExtent(title);
+ assert_true(false, "anchor offset, must throw an TypeError exception")
+ } catch (e) {
+ assert_equals(e.name, "TypeError", "anchor offset, got an TypeError exception")
+ }
+}, "setBaseAndExtent() with too few params");
+
+testDiv.style.display = "none";
+
+</script>
diff --git a/testing/web-platform/tests/selection/stringifier.tentative.html b/testing/web-platform/tests/selection/stringifier.tentative.html
new file mode 100644
index 0000000000..f4cae7eb5d
--- /dev/null
+++ b/testing/web-platform/tests/selection/stringifier.tentative.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Selection: stringifier</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+window.onload = () => {
+ test(() => {
+ const selection = getSelection();
+ const p = document.querySelector("p");
+
+ const range = document.createRange();
+ range.selectNode(p);
+ selection.addRange(range);
+
+ // In Chrome there are trailing \n characters. There is no spec for the
+ // stringifier, just a link to
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=10583
+ assert_equals(selection.toString(), "foo bar");
+ });
+};
+</script>
+<!-- The structure of the document matters, in particular making this look like
+ in addRange.htm would mask the problem. -->
+<body>
+ <p>foo bar</p>
+</body>
+</html>
diff --git a/testing/web-platform/tests/selection/test-iframe.html b/testing/web-platform/tests/selection/test-iframe.html
new file mode 100644
index 0000000000..42b982324b
--- /dev/null
+++ b/testing/web-platform/tests/selection/test-iframe.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>Selection test iframe</title>
+<link rel=author title="Aryeh Gregor" href=ayg@aryeh.name>
+<body>
+<script src=common.js></script>
+<script>
+"use strict";
+
+// This script only exists because we want to evaluate the range endpoints
+// in each iframe using that iframe's local variables set up by common.js. It
+// just creates a range with the endpoints given by
+// eval(window.testRangeInput), and assigns the result to window.testRange. If
+// there's an exception, it's assigned to window.unexpectedException.
+// Everything else is to be done by the script that created the iframe.
+window.unexpectedException = null;
+
+function run() {
+ window.unexpectedException = null;
+ try {
+ window.testRange = rangeFromEndpoints(eval(window.testRangeInput));
+ } catch(e) {
+ window.unexpectedException = e;
+ }
+}
+
+// Remove the scripts so they don't run repeatedly when the iframe is
+// reinitialized
+[].forEach.call(document.querySelectorAll("script"), function(script) {
+ script.parentNode.removeChild(script);
+});
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/selection/textcontrols/focus.html b/testing/web-platform/tests/selection/textcontrols/focus.html
new file mode 100644
index 0000000000..8c2e0b5591
--- /dev/null
+++ b/testing/web-platform/tests/selection/textcontrols/focus.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Stop selection extension when focus changes</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<link rel="stylesheet" href="/fonts/ahem.css">
+<style>
+#p {
+ font: 16px/1 Ahem;
+}
+</style>
+<p id="p">
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
+ aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+</p>
+<textarea id="textarea">Hello</textarea>
+<script>
+ const selection = getSelection();
+ const { p, textarea } = document.all;
+ document.addEventListener("selectionchange", ev => {
+ if (p.contains(selection.focusNode)) {
+ textarea.focus();
+ }
+ });
+
+ promise_test(async () => {
+ await new test_driver.Actions()
+ .pointerMove(5, 5, {origin: p})
+ .pointerDown()
+ .pointerMove(50, 50)
+ .pointerUp()
+ .send();
+ assert_equals(selection.focusNode, document.body);
+ assert_equals(selection.focusOffset, 2);
+ assert_equals(selection.anchorNode, document.body);
+ assert_equals(selection.focusOffset, 2);
+ }, "focus() should cancel selection extension by pointer device");
+</script>
diff --git a/testing/web-platform/tests/selection/textcontrols/onselectionchange-content-attribute.html b/testing/web-platform/tests/selection/textcontrols/onselectionchange-content-attribute.html
new file mode 100644
index 0000000000..3120b3bff5
--- /dev/null
+++ b/testing/web-platform/tests/selection/textcontrols/onselectionchange-content-attribute.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test that setting "onselectionchange" content attribute adds an event listener</title>
+<link rel="help" href="https://w3c.github.io/selection-api/#extensions-to-globaleventhandlers-interface">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<div id="testElement" onselectionchange="window.handlerSetFromParserWasFired()"></div>
+<script>
+ promise_test(() => {
+ return new Promise(resolve => {
+ window.handlerSetFromParserWasFired = resolve;
+ testElement.dispatchEvent(new Event("selectionchange"));
+ });
+ }, "handler set from parser");
+
+ promise_test(() => {
+ const el = document.createElement("div");
+ el.setAttribute("onselectionchange", "window.handlerSetViaSetAttributeWasFired()");
+ document.body.append(el);
+
+ return new Promise(resolve => {
+ window.handlerSetViaSetAttributeWasFired = resolve;
+ el.dispatchEvent(new Event("selectionchange"));
+ });
+ }, "handler set via setAttribute()");
+</script>
diff --git a/testing/web-platform/tests/selection/textcontrols/selectionchange-bubble.html b/testing/web-platform/tests/selection/textcontrols/selectionchange-bubble.html
new file mode 100644
index 0000000000..834b32bdd3
--- /dev/null
+++ b/testing/web-platform/tests/selection/textcontrols/selectionchange-bubble.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test selectionchange events bubbling from text controls</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<input id="input" width="200" value="foo"><br>
+<textarea id="textarea" width="200">foo</textarea>
+
+<script>
+ function untilEvent(element, eventName) {
+ return new Promise(resolve => {
+ element.addEventListener(eventName, resolve, { once: true });
+ });
+ }
+
+ for (const element of [input, textarea]) {
+ const name = element.localName;
+ for (const focus of [false, true]) {
+ let focused = focus ? " when focused" : "";
+ let offset = focus ? 2 : 1;
+ promise_test(async () => {
+ if (focus) {
+ element.focus();
+ }
+
+ element.setSelectionRange(offset, offset);
+ const ev = await untilEvent(element, "selectionchange");
+ assert_equals(ev.bubbles, true);
+ }, `selectionchange bubbles from ${name}${focused}`);
+ }
+ }
+</script>
diff --git a/testing/web-platform/tests/selection/textcontrols/selectionchange.html b/testing/web-platform/tests/selection/textcontrols/selectionchange.html
new file mode 100644
index 0000000000..2b43cfe44b
--- /dev/null
+++ b/testing/web-platform/tests/selection/textcontrols/selectionchange.html
@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test selectionchange events from text controls</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="stylesheet" href="/fonts/ahem.css">
+<style>
+ input,
+ textarea {
+ font: 16px/1 Ahem;
+ }
+</style>
+
+<input id="input" width="200"><br>
+<textarea id="textarea" width="200"></textarea>
+
+<script>
+ class SelectionChangeCollector {
+ /**
+ * @param {HTMLElement} target
+ */
+ constructor(target) {
+ this.target = target;
+ this.events = [];
+ target.addEventListener("selectionchange", ev => {
+ this.events.push(ev);
+ });
+ }
+ clear() {
+ this.events.length = 0;
+ }
+ }
+
+ const data = {
+ collectors: [
+ new SelectionChangeCollector(input),
+ new SelectionChangeCollector(input.cloneNode()),
+ new SelectionChangeCollector(textarea),
+ new SelectionChangeCollector(textarea.cloneNode(true)),
+ ],
+ async initialize() {
+ for (const collector of this.collectors) {
+ collector.target.value = "XXXXXXXXXXXXXXXXXXX";
+ collector.target.blur();
+ collector.target.setSelectionRange(0, 0);
+ }
+ await this.spin();
+ for (const collector of this.collectors) {
+ collector.clear();
+ }
+ },
+ spin() {
+ return new Promise(setTimeout);
+ },
+ async assert_empty_spin() {
+ // firing selectionchange must be asynchronous
+ for (const collector of this.collectors) {
+ assert_equals(collector.events.length, 0);
+ }
+ await this.spin();
+ }
+ };
+
+ for (const collector of data.collectors) {
+ const target = collector.target;
+ const name = `the ${!target.parentNode ? "disconnected " : ""}${target.localName} element`;
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.selectionStart = 1;
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Modifying selectionStart value of ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.selectionEnd = 1;
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Modifying selectionEnd value of ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.setSelectionRange(0, 4);
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Calling setSelectionRange() on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.select();
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Calling select() on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.setRangeText("newmiddle", 2, 3, "select");
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Calling setRangeText() on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.selectionStart = 0;
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 0);
+ }, `Setting initial zero selectionStart value on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.selectionStart = 2;
+ target.selectionStart = 2;
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Setting the same selectionStart value twice on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.selectionEnd = 0;
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 0);
+ }, `Setting initial zero selectionEnd value on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.selectionEnd = 2;
+ target.selectionEnd = 2;
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Setting the same selectionEnd value twice on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.setSelectionRange(0, 0);
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 0);
+ }, `Setting initial zero selection range on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.setSelectionRange(3, 3);
+ target.setSelectionRange(3, 3);
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Setting the same selection range twice on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.select();
+ target.select();
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 1);
+ }, `Calling select() twice on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.select();
+ target.setRangeText("foo", 2, 6);
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 2);
+ }, `Calling setRangeText() after select() on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.select();
+ target.setRangeText("", 10, 12);
+ target.setRangeText("", 10, 12);
+ target.setRangeText("", 10, 12);
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 4);
+ }, `Calling setRangeText() repeatedly on ${name}`);
+
+ promise_test(async () => {
+ await data.initialize();
+
+ target.value = "";
+ target.setRangeText("foo");
+
+ await data.assert_empty_spin();
+ assert_equals(collector.events.length, 0);
+ }, `Calling setRangeText() on empty ${name}`);
+ }
+</script>
diff --git a/testing/web-platform/tests/selection/toString-ff-bug-001.html b/testing/web-platform/tests/selection/toString-ff-bug-001.html
new file mode 100644
index 0000000000..985be73b5c
--- /dev/null
+++ b/testing/web-platform/tests/selection/toString-ff-bug-001.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<title>Can serialize a range which starts at the end of an element</title>
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1571517">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<table>
+ <tr>
+ <td id="left">Foo</td>
+ <td id="right">Hello</td>
+ </tr>
+</table>
+<script>
+test(function() {
+ let s = getSelection();
+ let r = new Range();
+ r.setStart(document.getElementById("left"), 1);
+ r.setEnd(document.getElementById("right").firstChild, 4);
+ s.addRange(r);
+ assert_equals(s.toString().trim(), "Hell");
+}, "Can serialize a range which starts at the end of an element");
+</script>
diff --git a/testing/web-platform/tests/selection/type.html b/testing/web-platform/tests/selection/type.html
new file mode 100644
index 0000000000..7be8ba61bc
--- /dev/null
+++ b/testing/web-platform/tests/selection/type.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<title>Selection.type tests</title>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=common.js></script>
+<script>
+"use strict";
+
+test(() => {
+ assert_equals(getSelection().rangeCount, 0, "Sanity check");
+ assert_equals(getSelection().type, "None");
+}, "Empty selection");
+
+for (var i = 0; i < testRanges.length; i++) {
+ var endpoints = eval(testRanges[i]);
+ if (!isSelectableNode(endpoints[0]) || !isSelectableNode(endpoints[2])) {
+ continue;
+ }
+ test(() => {
+ var range = rangeFromEndpoints(endpoints);
+ getSelection().removeAllRanges();
+ getSelection().addRange(range);
+ if (endpoints[0] == endpoints[2] && endpoints[1] == endpoints[3]) {
+ assert_equals(getSelection().type, "Caret");
+ } else {
+ assert_equals(getSelection().type, "Range");
+ }
+ }, testRanges[i]);
+}
+</script>
diff --git a/testing/web-platform/tests/selection/user-select-on-input-and-contenteditable.html b/testing/web-platform/tests/selection/user-select-on-input-and-contenteditable.html
new file mode 100644
index 0000000000..6cbde6914b
--- /dev/null
+++ b/testing/web-platform/tests/selection/user-select-on-input-and-contenteditable.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<meta name="timeout" content="long">
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<title>Test: used 'user-select' is always 'contain' on editable elements</title>
+<link rel="help" href="https://drafts.csswg.org/css-ui/#propdef-user-select">
+<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>
+<input type=text value="I should be selectable">
+<input type=search value="I should be selectable">
+<input type=url value="https://example.org/">
+<input type=tel value="555-555-5555">
+<input type=password value="I should be selectable">
+<textarea>I should be selectable</textarea>
+<div contenteditable="true">I should be selectable</div>
+<script>
+ const valuesToTest = ["auto", "text", "none", "contain", "all"];
+
+ for (let value of valuesToTest) {
+ promise_test(async function () {
+ for (let element of document.querySelectorAll("input,textarea")) {
+ element.style.userSelect = value;
+ element.focus();
+ let start = element.selectionStart;
+ await test_driver.click(element);
+ assert_not_equals(element.selectionStart, start, "Selection should've moved on click.");
+ element.selectionStart = 0;
+ }
+
+ let div = document.querySelector("div");
+ div.style.userSelect = value;
+ div.focus();
+ let oldOffset = getSelection().focusOffset;
+ await test_driver.click(div);
+ assert_not_equals(oldOffset, getSelection().focusOffset, "Selection should've moved on click.");
+ getSelection().focusOffet = 0;
+ }, `selection for ${value}`);
+ }
+</script>