diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/selection | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
78 files changed, 5004 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..e8bd262868 --- /dev/null +++ b/testing/web-platform/tests/selection/caret/collapse-pre-linestart-1.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 +<br></pre> +<script> + target.focus(); + 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..0feee464c9 --- /dev/null +++ b/testing/web-platform/tests/selection/caret/collapse-pre-linestart-2.html @@ -0,0 +1,14 @@ +<!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> + target.focus(); + 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>Äb̈c̈d̈ëf̈g̈ḧ\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"> </div> + <p id="paragraph">Lorem ipsum dolor sit amet.</p> + <div contenteditable="false" id="end"> </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/selection-clip-crash.html b/testing/web-platform/tests/selection/crashtests/selection-clip-crash.html new file mode 100644 index 0000000000..66d10b4742 --- /dev/null +++ b/testing/web-platform/tests/selection/crashtests/selection-clip-crash.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" title="Philip Rogers" href="mailto:pdr@chromium.org"> +<meta name="assert" content="Moving a selection to a different clipper should not crash"> +<html class=reftest-wait> + <script> + function test() { + document.designMode = "on"; + + const a = document.getElementById('a'); + const range = new Range(); + range.setStart(a, 1); + range.setEnd(a, 1); + document.getSelection().addRange(range); + requestAnimationFrame(function() { + requestAnimationFrame(function() { + const b = document.getElementById('b'); + const updated_range = window.getSelection().getRangeAt(0); + updated_range.setStart(b, 1); + updated_range.setEnd(b, 1); + + document.documentElement.classList.remove("reftest-wait"); + }); + }); + } + </script> + <body onload=test()> + <div id=a style="width: 100px; height: 100px;"> </div> + <div id=b style="width: 100px; height: 100px; overflow: clip; margin-top: -100px;"> </div> + </body> +</html> diff --git a/testing/web-platform/tests/selection/crashtests/selection-modify-around-input.html b/testing/web-platform/tests/selection/crashtests/selection-modify-around-input.html new file mode 100644 index 0000000000..24b9ae7d35 --- /dev/null +++ b/testing/web-platform/tests/selection/crashtests/selection-modify-around-input.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<style> +div { + border: medium solid red; + border-width: 32em; +} +</style> +<script> +document.addEventListener("DOMContentLoaded", () => { + document.execCommand("selectAll"); + getSelection().modify("move", "backward", "character"); + getSelection().collapseToStart(); +}, {once: true}); +</script> +</head> +<body> +<button contenteditable></button> +<input value="a"> +<div></div> +</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> |