summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/ranges
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/dom/ranges
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/dom/ranges')
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-adopt-test.html52
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-attributes.html23
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-cloneContents.html461
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-cloneRange.html112
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-collapse.html67
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-commonAncestorContainer-2.html33
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-commonAncestorContainer.html40
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-compareBoundaryPoints.html182
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-comparePoint-2.html23
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-comparePoint.html92
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-constructor.html20
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-deleteContents.html337
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-detach.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-extractContents.html252
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-insertNode.html286
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-intersectsNode-2.html36
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-intersectsNode-binding.html25
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-intersectsNode-shadow.html19
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-intersectsNode.html70
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-isPointInRange.html83
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-appendChild.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-appendData.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-dataChange.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-deleteData.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-insertBefore.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-insertData.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-removeChild.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-replaceChild.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-replaceData.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations-splitText.html14
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-mutations.js921
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-selectNode.html99
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-set.html221
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-stringifier.html44
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-surroundContents.html324
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-test-iframe.html56
-rw-r--r--testing/web-platform/tests/dom/ranges/StaticRange-constructor.html200
37 files changed, 4232 insertions, 0 deletions
diff --git a/testing/web-platform/tests/dom/ranges/Range-adopt-test.html b/testing/web-platform/tests/dom/ranges/Range-adopt-test.html
new file mode 100644
index 0000000000..3735fc38fd
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-adopt-test.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<script>
+function createRangeWithUnparentedContainerOfSingleElement() {
+ const range = document.createRange();
+ const container = document.createElement("container");
+ const element = document.createElement("element");
+ container.appendChild(element);
+ range.selectNode(element);
+ return range;
+}
+function nestRangeInOuterContainer(range) {
+ range.startContainer.ownerDocument.createElement("outer").appendChild(range.startContainer);
+}
+function moveNodeToNewlyCreatedDocumentWithAppendChild(node) {
+ document.implementation.createDocument(null, null).appendChild(node);
+}
+
+//Tests removing only element
+test(()=>{
+ const range = createRangeWithUnparentedContainerOfSingleElement();
+ range.startContainer.removeChild(range.startContainer.firstChild);
+ assert_equals(range.endOffset, 0);
+}, "Range in document: Removing the only element in the range must collapse the range");
+
+
+//Tests removing only element after parented container moved to another document
+test(()=>{
+ const range = createRangeWithUnparentedContainerOfSingleElement();
+ nestRangeInOuterContainer(range);
+ moveNodeToNewlyCreatedDocumentWithAppendChild(range.startContainer);
+ assert_equals(range.endOffset, 0);
+}, "Parented range container moved to another document with appendChild: Moving the element to the other document must collapse the range");
+
+//Tests removing only element after parentless container moved oo another document
+test(()=>{
+ const range = createRangeWithUnparentedContainerOfSingleElement();
+ moveNodeToNewlyCreatedDocumentWithAppendChild(range.startContainer);
+ range.startContainer.removeChild(range.startContainer.firstChild);
+ assert_equals(range.endOffset, 0);
+}, "Parentless range container moved to another document with appendChild: Removing the only element in the range must collapse the range");
+
+//Tests removing only element after parentless container of container moved to another document
+test(()=>{
+ const range = createRangeWithUnparentedContainerOfSingleElement();
+ nestRangeInOuterContainer(range);
+ moveNodeToNewlyCreatedDocumentWithAppendChild(range.startContainer.parentNode);
+ range.startContainer.removeChild(range.startContainer.firstChild);
+ assert_equals(range.endOffset, 0);
+}, "Range container's parentless container moved to another document with appendChild: Removing the only element in the range must collapse the range");
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-attributes.html b/testing/web-platform/tests/dom/ranges/Range-attributes.html
new file mode 100644
index 0000000000..ced47edc50
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-attributes.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Range attributes</title>
+<link rel="author" title="Ms2ger" href="mailto:ms2ger@gmail.com">
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var r = document.createRange();
+ assert_equals(r.startContainer, document)
+ assert_equals(r.endContainer, document)
+ assert_equals(r.startOffset, 0)
+ assert_equals(r.endOffset, 0)
+ assert_true(r.collapsed)
+ r.detach()
+ assert_equals(r.startContainer, document)
+ assert_equals(r.endContainer, document)
+ assert_equals(r.startOffset, 0)
+ assert_equals(r.endOffset, 0)
+ assert_true(r.collapsed)
+})
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-cloneContents.html b/testing/web-platform/tests/dom/ranges/Range-cloneContents.html
new file mode 100644
index 0000000000..6064151f62
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-cloneContents.html
@@ -0,0 +1,461 @@
+<!doctype html>
+<title>Range.cloneContents() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<meta name=timeout content=long>
+<p>To debug test failures, add a query parameter "subtest" with the test id (like
+"?subtest=5"). Only that test will be run. Then you can look at the resulting
+iframe 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";
+
+testDiv.parentNode.removeChild(testDiv);
+
+var actualIframe = document.createElement("iframe");
+actualIframe.style.display = "none";
+document.body.appendChild(actualIframe);
+
+var expectedIframe = document.createElement("iframe");
+expectedIframe.style.display = "none";
+document.body.appendChild(expectedIframe);
+
+function myCloneContents(range) {
+ // "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, ProcessingInstruction, or Comment node:"
+ if (range.startContainer == range.endContainer
+ && (range.startContainer.nodeType == Node.TEXT_NODE
+ || range.startContainer.nodeType == Node.COMMENT_NODE
+ || range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_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);
+
+ // "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 first partially contained child is a Text, ProcessingInstruction, or Comment node:"
+ if (firstPartiallyContainedChild
+ && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE
+ || firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE
+ || firstPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_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,
+ nodeLength(originalStartNode) - originalStartOffset);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+ // "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,
+ nodeLength(firstPartiallyContainedChild));
+
+ // "Let subfrag be the result of calling cloneContents() on
+ // subrange."
+ var subfrag = myCloneContents(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:"
+ for (var i = 0; i < containedChildren.length; i++) {
+ // "Let clone be the result of calling cloneNode(true) of contained
+ // child."
+ var clone = containedChildren[i].cloneNode(true);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+ }
+
+ // "If last partially contained child is a Text, ProcessingInstruction, or Comment node:"
+ if (lastPartiallyContainedChild
+ && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE
+ || lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE
+ || lastPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_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);
+ // "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 cloneContents() on
+ // subrange."
+ var subfrag = myCloneContents(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]);
+ }
+ }
+
+ // "Return frag."
+ return frag;
+}
+
+function restoreIframe(iframe, i) {
+ // Most of this function is designed to work around the fact that Opera
+ // doesn't let you add a doctype to a document that no longer has one, in
+ // any way I can figure out. I eventually compromised on something that
+ // will still let Opera pass most tests that don't actually involve
+ // doctypes.
+ while (iframe.contentDocument.firstChild
+ && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.firstChild);
+ }
+
+ while (iframe.contentDocument.lastChild
+ && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
+ }
+
+ if (!iframe.contentDocument.firstChild) {
+ // This will throw an exception in Opera if we reach here, which is why
+ // I try to avoid it. It will never happen in a browser that obeys the
+ // spec, so it's really just insurance. I don't think it actually gets
+ // hit by anything.
+ iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
+ }
+ iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
+ iframe.contentWindow.setupRangeTests();
+ iframe.contentWindow.testRangeInput = testRanges[i];
+ iframe.contentWindow.run();
+}
+
+function testCloneContents(i) {
+ restoreIframe(actualIframe, i);
+ restoreIframe(expectedIframe, i);
+
+ var actualRange = actualIframe.contentWindow.testRange;
+ var expectedRange = expectedIframe.contentWindow.testRange;
+ var actualFrag, expectedFrag;
+ var actualRoots, expectedRoots;
+
+ domTests[i].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual cloneContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated cloneContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+
+ // NOTE: We could just assume that cloneContents() doesn't change
+ // anything. That would simplify these tests, taken in isolation. But
+ // once we've already set up the whole apparatus for extractContents()
+ // and deleteContents(), we just reuse it here, on the theory of "why
+ // not test some more stuff if it's easy to do".
+ //
+ // Just to be pedantic, we'll test not only that the tree we're
+ // modifying is the same in expected vs. actual, but also that all the
+ // nodes originally in it were the same. Typically some nodes will
+ // become detached when the algorithm is run, but they still exist and
+ // references can still be kept to them, so they should also remain the
+ // same.
+ //
+ // We initialize the list to all nodes, and later on remove all the
+ // ones which still have parents, since the parents will presumably be
+ // tested for isEqualNode() and checking the children would be
+ // redundant.
+ var actualAllNodes = [];
+ var node = furthestAncestor(actualRange.startContainer);
+ do {
+ actualAllNodes.push(node);
+ } while (node = nextNode(node));
+
+ var expectedAllNodes = [];
+ var node = furthestAncestor(expectedRange.startContainer);
+ do {
+ expectedAllNodes.push(node);
+ } while (node = nextNode(node));
+
+ expectedFrag = myCloneContents(expectedRange);
+ if (typeof expectedFrag == "string") {
+ assert_throws_dom(
+ expectedFrag,
+ actualIframe.contentWindow.DOMException,
+ function() {
+ actualRange.cloneContents();
+ }
+ );
+ } else {
+ actualFrag = actualRange.cloneContents();
+ }
+
+ actualRoots = [];
+ for (var j = 0; j < actualAllNodes.length; j++) {
+ if (!actualAllNodes[j].parentNode) {
+ actualRoots.push(actualAllNodes[j]);
+ }
+ }
+
+ expectedRoots = [];
+ for (var j = 0; j < expectedAllNodes.length; j++) {
+ if (!expectedAllNodes[j].parentNode) {
+ expectedRoots.push(expectedAllNodes[j]);
+ }
+ }
+
+ for (var j = 0; j < actualRoots.length; j++) {
+ assertNodesEqual(actualRoots[j], expectedRoots[j], j ? "detached node #" + j : "tree root");
+
+ if (j == 0) {
+ // Clearly something is wrong if the node lists are different
+ // lengths. We want to report this only after we've already
+ // checked the main tree for equality, though, so it doesn't
+ // mask more interesting errors.
+ assert_equals(actualRoots.length, expectedRoots.length,
+ "Actual and expected DOMs were broken up into a different number of pieces by cloneContents() (this probably means you created or detached nodes when you weren't supposed to)");
+ }
+ }
+ });
+ domTests[i].done();
+
+ positionTests[i].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual cloneContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated cloneContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+
+ assert_true(actualRoots[0].isEqualNode(expectedRoots[0]),
+ "The resulting DOMs were not equal, so comparing positions makes no sense");
+
+ if (typeof expectedFrag == "string") {
+ // It's no longer true that, e.g., startContainer and endContainer
+ // must always be the same
+ return;
+ }
+
+ assert_equals(actualRange.startOffset, expectedRange.startOffset,
+ "Unexpected startOffset after cloneContents()");
+ // How do we decide that the two nodes are equal, since they're in
+ // different trees? Since the DOMs are the same, it's enough to check
+ // that the index in the parent is the same all the way up the tree.
+ // But we can first cheat by just checking they're actually equal.
+ assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer),
+ "Unexpected startContainer after cloneContents(), expected " +
+ expectedRange.startContainer.nodeName.toLowerCase() + " but got " +
+ actualRange.startContainer.nodeName.toLowerCase());
+ var currentActual = actualRange.startContainer;
+ var currentExpected = expectedRange.startContainer;
+ var actual = "";
+ var expected = "";
+ while (currentActual && currentExpected) {
+ actual = indexOf(currentActual) + "-" + actual;
+ expected = indexOf(currentExpected) + "-" + expected;
+
+ currentActual = currentActual.parentNode;
+ currentExpected = currentExpected.parentNode;
+ }
+ actual = actual.substr(0, actual.length - 1);
+ expected = expected.substr(0, expected.length - 1);
+ assert_equals(actual, expected,
+ "startContainer superficially looks right but is actually the wrong node if you trace back its index in all its ancestors (I'm surprised this actually happened");
+ });
+ positionTests[i].done();
+
+ fragTests[i].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual cloneContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated cloneContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+
+ if (typeof expectedFrag == "string") {
+ // Comparing makes no sense
+ return;
+ }
+ assertNodesEqual(actualFrag, expectedFrag,
+ "returned fragment");
+ });
+ fragTests[i].done();
+}
+
+// First test a Range that has the no-op detach() called on it, synchronously
+test(function() {
+ var range = document.createRange();
+ range.detach();
+ assert_array_equals(range.cloneContents().childNodes, []);
+}, "Range.detach()");
+
+var iStart = 0;
+var iStop = testRanges.length;
+
+if (/subtest=[0-9]+/.test(location.search)) {
+ var matches = /subtest=([0-9]+)/.exec(location.search);
+ iStart = Number(matches[1]);
+ iStop = Number(matches[1]) + 1;
+}
+
+var domTests = [];
+var positionTests = [];
+var fragTests = [];
+
+for (var i = iStart; i < iStop; i++) {
+ domTests[i] = async_test("Resulting DOM for range " + i + " " + testRanges[i]);
+ positionTests[i] = async_test("Resulting cursor position for range " + i + " " + testRanges[i]);
+ fragTests[i] = async_test("Returned fragment for range " + i + " " + testRanges[i]);
+}
+
+var referenceDoc = document.implementation.createHTMLDocument("");
+referenceDoc.removeChild(referenceDoc.documentElement);
+
+actualIframe.onload = function() {
+ expectedIframe.onload = function() {
+ for (var i = iStart; i < iStop; i++) {
+ testCloneContents(i);
+ }
+ }
+ expectedIframe.src = "Range-test-iframe.html";
+ referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
+}
+actualIframe.src = "Range-test-iframe.html";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-cloneRange.html b/testing/web-platform/tests/dom/ranges/Range-cloneRange.html
new file mode 100644
index 0000000000..6c736df29f
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-cloneRange.html
@@ -0,0 +1,112 @@
+<!doctype html>
+<title>Range.cloneRange() and document.createRange() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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";
+
+function testCloneRange(rangeEndpoints) {
+ var range;
+ if (rangeEndpoints == "detached") {
+ range = document.createRange();
+ range.detach();
+ var clonedRange = range.cloneRange();
+ assert_equals(clonedRange.startContainer, range.startContainer,
+ "startContainers must be equal after cloneRange()");
+ assert_equals(clonedRange.startOffset, range.startOffset,
+ "startOffsets must be equal after cloneRange()");
+ assert_equals(clonedRange.endContainer, range.endContainer,
+ "endContainers must be equal after cloneRange()");
+ assert_equals(clonedRange.endOffset, range.endOffset,
+ "endOffsets must be equal after cloneRange()");
+ return;
+ }
+
+ // Have to account for Ranges involving Documents! We could just create
+ // the Range from the current document unconditionally, but some browsers
+ // (WebKit) don't implement setStart() and setEnd() per spec and will throw
+ // spurious exceptions at the time of this writing. No need to mask other
+ // bugs.
+ var ownerDoc = rangeEndpoints[0].nodeType == Node.DOCUMENT_NODE
+ ? rangeEndpoints[0]
+ : rangeEndpoints[0].ownerDocument;
+ range = ownerDoc.createRange();
+ // Here we throw in some createRange() tests, because why not. Have to
+ // test it someplace.
+ assert_equals(range.startContainer, ownerDoc,
+ "doc.createRange() must create Range whose startContainer is doc");
+ assert_equals(range.endContainer, ownerDoc,
+ "doc.createRange() must create Range whose endContainer is doc");
+ assert_equals(range.startOffset, 0,
+ "doc.createRange() must create Range whose startOffset is 0");
+ assert_equals(range.endOffset, 0,
+ "doc.createRange() must create Range whose endOffset is 0");
+
+ range.setStart(rangeEndpoints[0], rangeEndpoints[1]);
+ range.setEnd(rangeEndpoints[2], rangeEndpoints[3]);
+
+ // Make sure we bail out now if setStart or setEnd are buggy, so it doesn't
+ // create misleading failures later.
+ assert_equals(range.startContainer, rangeEndpoints[0],
+ "Sanity check on setStart()");
+ assert_equals(range.startOffset, rangeEndpoints[1],
+ "Sanity check on setStart()");
+ assert_equals(range.endContainer, rangeEndpoints[2],
+ "Sanity check on setEnd()");
+ assert_equals(range.endOffset, rangeEndpoints[3],
+ "Sanity check on setEnd()");
+
+ var clonedRange = range.cloneRange();
+
+ assert_equals(clonedRange.startContainer, range.startContainer,
+ "startContainers must be equal after cloneRange()");
+ assert_equals(clonedRange.startOffset, range.startOffset,
+ "startOffsets must be equal after cloneRange()");
+ assert_equals(clonedRange.endContainer, range.endContainer,
+ "endContainers must be equal after cloneRange()");
+ assert_equals(clonedRange.endOffset, range.endOffset,
+ "endOffsets must be equal after cloneRange()");
+
+ // Make sure that modifying one doesn't affect the other.
+ var testNode1 = ownerDoc.createTextNode("testing");
+ var testNode2 = ownerDoc.createTextNode("testing with different length");
+
+ range.setStart(testNode1, 1);
+ range.setEnd(testNode1, 2);
+ assert_equals(clonedRange.startContainer, rangeEndpoints[0],
+ "Modifying a Range must not modify its clone's startContainer");
+ assert_equals(clonedRange.startOffset, rangeEndpoints[1],
+ "Modifying a Range must not modify its clone's startOffset");
+ assert_equals(clonedRange.endContainer, rangeEndpoints[2],
+ "Modifying a Range must not modify its clone's endContainer");
+ assert_equals(clonedRange.endOffset, rangeEndpoints[3],
+ "Modifying a Range must not modify its clone's endOffset");
+
+ clonedRange.setStart(testNode2, 3);
+ clonedRange.setStart(testNode2, 4);
+
+ assert_equals(range.startContainer, testNode1,
+ "Modifying a clone must not modify the original Range's startContainer");
+ assert_equals(range.startOffset, 1,
+ "Modifying a clone must not modify the original Range's startOffset");
+ assert_equals(range.endContainer, testNode1,
+ "Modifying a clone must not modify the original Range's endContainer");
+ assert_equals(range.endOffset, 2,
+ "Modifying a clone must not modify the original Range's endOffset");
+}
+
+var tests = [];
+for (var i = 0; i < testRanges.length; i++) {
+ tests.push([
+ "Range " + i + " " + testRanges[i],
+ eval(testRanges[i])
+ ]);
+}
+generate_tests(testCloneRange, tests);
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-collapse.html b/testing/web-platform/tests/dom/ranges/Range-collapse.html
new file mode 100644
index 0000000000..141dbdf610
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-collapse.html
@@ -0,0 +1,67 @@
+<!doctype html>
+<title>Range.collapse() and .collapsed tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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";
+
+function testCollapse(rangeEndpoints, toStart) {
+ // Have to account for Ranges involving Documents!
+ var ownerDoc = rangeEndpoints[0].nodeType == Node.DOCUMENT_NODE
+ ? rangeEndpoints[0]
+ : rangeEndpoints[0].ownerDocument;
+ var range = ownerDoc.createRange();
+ range.setStart(rangeEndpoints[0], rangeEndpoints[1]);
+ range.setEnd(rangeEndpoints[2], rangeEndpoints[3]);
+
+ var expectedContainer = toStart ? range.startContainer : range.endContainer;
+ var expectedOffset = toStart ? range.startOffset : range.endOffset;
+
+ assert_equals(range.collapsed, range.startContainer == range.endContainer
+ && range.startOffset == range.endOffset,
+ "collapsed must be true if and only if the start and end are equal");
+
+ if (toStart === undefined) {
+ range.collapse();
+ } else {
+ range.collapse(toStart);
+ }
+
+ assert_equals(range.startContainer, expectedContainer,
+ "Wrong startContainer");
+ assert_equals(range.endContainer, expectedContainer,
+ "Wrong endContainer");
+ assert_equals(range.startOffset, expectedOffset,
+ "Wrong startOffset");
+ assert_equals(range.endOffset, expectedOffset,
+ "Wrong endOffset");
+ assert_true(range.collapsed,
+ ".collapsed must be set after .collapsed()");
+}
+
+var tests = [];
+for (var i = 0; i < testRanges.length; i++) {
+ tests.push([
+ "Range " + i + " " + testRanges[i] + ", toStart true",
+ eval(testRanges[i]),
+ true
+ ]);
+ tests.push([
+ "Range " + i + " " + testRanges[i] + ", toStart false",
+ eval(testRanges[i]),
+ false
+ ]);
+ tests.push([
+ "Range " + i + " " + testRanges[i] + ", toStart omitted",
+ eval(testRanges[i]),
+ undefined
+ ]);
+}
+generate_tests(testCollapse, tests);
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-commonAncestorContainer-2.html b/testing/web-platform/tests/dom/ranges/Range-commonAncestorContainer-2.html
new file mode 100644
index 0000000000..f0a3e451cd
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-commonAncestorContainer-2.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<title>Range.commonAncestorContainer</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var range = document.createRange();
+ range.detach();
+ assert_equals(range.commonAncestorContainer, document);
+}, "Detached Range")
+test(function() {
+ var df = document.createDocumentFragment();
+ var foo = df.appendChild(document.createElement("foo"));
+ foo.appendChild(document.createTextNode("Foo"));
+ var bar = df.appendChild(document.createElement("bar"));
+ bar.appendChild(document.createComment("Bar"));
+ [
+ // start node, start offset, end node, end offset, expected cAC
+ [foo, 0, bar, 0, df],
+ [foo, 0, foo.firstChild, 3, foo],
+ [foo.firstChild, 0, bar, 0, df],
+ [foo.firstChild, 3, bar.firstChild, 2, df]
+ ].forEach(function(t) {
+ test(function() {
+ var range = document.createRange();
+ range.setStart(t[0], t[1]);
+ range.setEnd(t[2], t[3]);
+ assert_equals(range.commonAncestorContainer, t[4]);
+ })
+ });
+}, "Normal Ranges")
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-commonAncestorContainer.html b/testing/web-platform/tests/dom/ranges/Range-commonAncestorContainer.html
new file mode 100644
index 0000000000..7882ccc31f
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-commonAncestorContainer.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<title>Range.commonAncestorContainer tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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("[detached]");
+
+for (var i = 0; i < testRanges.length; i++) {
+ test(function() {
+ var range;
+ if (i == 0) {
+ range = document.createRange();
+ range.detach();
+ } else {
+ range = rangeFromEndpoints(eval(testRanges[i]));
+ }
+
+ // "Let container be start node."
+ var container = range.startContainer;
+
+ // "While container is not an inclusive ancestor of end node, let
+ // container be container's parent."
+ while (container != range.endContainer
+ && !isAncestor(container, range.endContainer)) {
+ container = container.parentNode;
+ }
+
+ // "Return container."
+ assert_equals(range.commonAncestorContainer, container);
+ }, i + ": range " + testRanges[i]);
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-compareBoundaryPoints.html b/testing/web-platform/tests/dom/ranges/Range-compareBoundaryPoints.html
new file mode 100644
index 0000000000..9d150ae0ab
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-compareBoundaryPoints.html
@@ -0,0 +1,182 @@
+<!doctype html>
+<title>Range.compareBoundaryPoints() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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";
+
+var testRangesCached = [];
+testRangesCached.push(document.createRange());
+testRangesCached[0].detach();
+for (var i = 0; i < testRangesShort.length; i++) {
+ try {
+ testRangesCached.push(rangeFromEndpoints(eval(testRangesShort[i])));
+ } catch(e) {
+ testRangesCached.push(null);
+ }
+}
+
+var testRangesCachedClones = [];
+testRangesCachedClones.push(document.createRange());
+testRangesCachedClones[0].detach();
+for (var i = 1; i < testRangesCached.length; i++) {
+ if (testRangesCached[i]) {
+ testRangesCachedClones.push(testRangesCached[i].cloneRange());
+ } else {
+ testRangesCachedClones.push(null);
+ }
+}
+
+// We want to run a whole bunch of extra tests with invalid "how" values (not
+// 0-3), but it's excessive to run them for every single pair of ranges --
+// there are too many of them. So just run them for a handful of the tests.
+var extraTests = [0, // detached
+ 1 + testRanges.indexOf("[paras[0].firstChild, 2, paras[0].firstChild, 8]"),
+ 1 + testRanges.indexOf("[paras[0].firstChild, 3, paras[3], 1]"),
+ 1 + testRanges.indexOf("[testDiv, 0, comment, 5]"),
+ 1 + testRanges.indexOf("[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]")];
+
+for (var i = 0; i < testRangesCached.length; i++) {
+ var range1 = testRangesCached[i];
+ var range1Desc = i + " " + (i == 0 ? "[detached]" : testRanges[i - 1]);
+ for (var j = 0; j <= testRangesCachedClones.length; j++) {
+ var range2;
+ var range2Desc;
+ if (j == testRangesCachedClones.length) {
+ range2 = range1;
+ range2Desc = "same as first range";
+ } else {
+ range2 = testRangesCachedClones[j];
+ range2Desc = j + " " + (j == 0 ? "[detached]" : testRanges[j - 1]);
+ }
+
+ var hows = [Range.START_TO_START, Range.START_TO_END, Range.END_TO_END,
+ Range.END_TO_START];
+ if (extraTests.indexOf(i) != -1 && extraTests.indexOf(j) != -1) {
+ // TODO: Make some type of reusable utility function to do this
+ // work.
+ hows.push(-1, 4, 5, NaN, -0, +Infinity, -Infinity);
+ [65536, -65536, 65536*65536, 0.5, -0.5, -72.5].forEach(function(addend) {
+ hows.push(-1 + addend, 0 + addend, 1 + addend,
+ 2 + addend, 3 + addend, 4 + addend);
+ });
+ hows.forEach(function(how) { hows.push(String(how)) });
+ hows.push("6.5536e4", null, undefined, true, false, "", "quasit");
+ }
+
+ for (var k = 0; k < hows.length; k++) {
+ var how = hows[k];
+ test(function() {
+ assert_not_equals(range1, null,
+ "Creating context range threw an exception");
+ assert_not_equals(range2, null,
+ "Creating argument range threw an exception");
+
+ // Convert how per WebIDL. TODO: Make some type of reusable
+ // utility function to do this work.
+ // "Let number be the result of calling ToNumber on the input
+ // argument."
+ var convertedHow = Number(how);
+
+ // "If number is NaN, +0, −0, +∞, or −∞, return +0."
+ if (isNaN(convertedHow)
+ || convertedHow == 0
+ || convertedHow == Infinity
+ || convertedHow == -Infinity) {
+ convertedHow = 0;
+ } else {
+ // "Let posInt be sign(number) * floor(abs(number))."
+ var posInt = (convertedHow < 0 ? -1 : 1) * Math.floor(Math.abs(convertedHow));
+
+ // "Let int16bit be posInt modulo 2^16; that is, a finite
+ // integer value k of Number type with positive sign and
+ // less than 2^16 in magnitude such that the mathematical
+ // difference of posInt and k is mathematically an integer
+ // multiple of 2^16."
+ //
+ // "Return int16bit."
+ convertedHow = posInt % 65536;
+ if (convertedHow < 0) {
+ convertedHow += 65536;
+ }
+ }
+
+ // Now to the actual algorithm.
+ // "If how is not one of
+ // START_TO_START,
+ // START_TO_END,
+ // END_TO_END, and
+ // END_TO_START,
+ // throw a "NotSupportedError" exception and terminate these
+ // steps."
+ if (convertedHow != Range.START_TO_START
+ && convertedHow != Range.START_TO_END
+ && convertedHow != Range.END_TO_END
+ && convertedHow != Range.END_TO_START) {
+ assert_throws_dom("NOT_SUPPORTED_ERR", function() {
+ range1.compareBoundaryPoints(how, range2);
+ }, "NotSupportedError required if first parameter doesn't convert to 0-3 per WebIDL");
+ return;
+ }
+
+ // "If context object's root is not the same as sourceRange's
+ // root, throw a "WrongDocumentError" exception and terminate
+ // these steps."
+ if (furthestAncestor(range1.startContainer) != furthestAncestor(range2.startContainer)) {
+ assert_throws_dom("WRONG_DOCUMENT_ERR", function() {
+ range1.compareBoundaryPoints(how, range2);
+ }, "WrongDocumentError required if the ranges don't share a root");
+ return;
+ }
+
+ // "If how is:
+ // START_TO_START:
+ // Let this point be the context object's start.
+ // Let other point be sourceRange's start.
+ // START_TO_END:
+ // Let this point be the context object's end.
+ // Let other point be sourceRange's start.
+ // END_TO_END:
+ // Let this point be the context object's end.
+ // Let other point be sourceRange's end.
+ // END_TO_START:
+ // Let this point be the context object's start.
+ // Let other point be sourceRange's end."
+ var thisPoint = convertedHow == Range.START_TO_START || convertedHow == Range.END_TO_START
+ ? [range1.startContainer, range1.startOffset]
+ : [range1.endContainer, range1.endOffset];
+ var otherPoint = convertedHow == Range.START_TO_START || convertedHow == Range.START_TO_END
+ ? [range2.startContainer, range2.startOffset]
+ : [range2.endContainer, range2.endOffset];
+
+ // "If the position of this point relative to other point is
+ // before
+ // Return −1.
+ // equal
+ // Return 0.
+ // after
+ // Return 1."
+ var position = getPosition(thisPoint[0], thisPoint[1], otherPoint[0], otherPoint[1]);
+ var expected;
+ if (position == "before") {
+ expected = -1;
+ } else if (position == "equal") {
+ expected = 0;
+ } else if (position == "after") {
+ expected = 1;
+ }
+
+ assert_equals(range1.compareBoundaryPoints(how, range2), expected,
+ "Wrong return value");
+ }, i + "," + j + "," + k + ": context range " + range1Desc + ", argument range " + range2Desc + ", how " + format_value(how));
+ }
+ }
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-comparePoint-2.html b/testing/web-platform/tests/dom/ranges/Range-comparePoint-2.html
new file mode 100644
index 0000000000..30a6c57ad9
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-comparePoint-2.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Range.comparePoint</title>
+<link rel="author" title="Ms2ger" href="mailto:ms2ger@gmail.com">
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var r = document.createRange();
+ r.detach()
+ assert_equals(r.comparePoint(document.body, 0), 1)
+})
+test(function() {
+ var r = document.createRange();
+ assert_throws_js(TypeError, function() { r.comparePoint(null, 0) })
+})
+test(function() {
+ var doc = document.implementation.createHTMLDocument("tralala")
+ var r = document.createRange();
+ assert_throws_dom("WRONG_DOCUMENT_ERR", function() { r.comparePoint(doc.body, 0) })
+})
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-comparePoint.html b/testing/web-platform/tests/dom/ranges/Range-comparePoint.html
new file mode 100644
index 0000000000..e18ac95c4c
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-comparePoint.html
@@ -0,0 +1,92 @@
+<!doctype html>
+<title>Range.comparePoint() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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";
+
+// Will be filled in on the first run for that range
+var testRangesCached = [];
+
+for (var i = 0; i < testPoints.length; i++) {
+ var node = eval(testPoints[i])[0];
+ var offset = eval(testPoints[i])[1];
+
+ // comparePoint is an unsigned long, so per WebIDL, we need to treat it as
+ // though it wrapped to an unsigned 32-bit integer.
+ var normalizedOffset = offset % Math.pow(2, 32);
+ if (normalizedOffset < 0) {
+ normalizedOffset += Math.pow(2, 32);
+ }
+
+ for (var j = 0; j < testRanges.length; j++) {
+ test(function() {
+ if (testRangesCached[j] === undefined) {
+ try {
+ testRangesCached[j] = rangeFromEndpoints(eval(testRanges[i]));
+ } catch(e) {
+ testRangesCached[j] = null;
+ }
+ }
+ assert_not_equals(testRangesCached[j], null,
+ "Setting up the range failed");
+
+ var range = testRangesCached[j].cloneRange();
+
+ // "If node's root is different from the context object's root,
+ // throw a "WrongDocumentError" exception and terminate these
+ // steps."
+ if (furthestAncestor(node) !== furthestAncestor(range.startContainer)) {
+ assert_throws_dom("WRONG_DOCUMENT_ERR", function() {
+ range.comparePoint(node, offset);
+ }, "Must throw WrongDocumentError if node and range have different roots");
+ return;
+ }
+
+ // "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() {
+ range.comparePoint(node, offset);
+ }, "Must throw InvalidNodeTypeError if node is a doctype");
+ return;
+ }
+
+ // "If offset is greater than node's length, throw an
+ // "IndexSizeError" exception and terminate these steps."
+ if (normalizedOffset > nodeLength(node)) {
+ assert_throws_dom("INDEX_SIZE_ERR", function() {
+ range.comparePoint(node, offset);
+ }, "Must throw IndexSizeError if offset is greater than length");
+ return;
+ }
+
+ // "If (node, offset) is before start, return −1 and terminate
+ // these steps."
+ if (getPosition(node, normalizedOffset, range.startContainer, range.startOffset) === "before") {
+ assert_equals(range.comparePoint(node, offset), -1,
+ "Must return -1 if point is before start");
+ return;
+ }
+
+ // "If (node, offset) is after end, return 1 and terminate these
+ // steps."
+ if (getPosition(node, normalizedOffset, range.endContainer, range.endOffset) === "after") {
+ assert_equals(range.comparePoint(node, offset), 1,
+ "Must return 1 if point is after end");
+ return;
+ }
+
+ // "Return 0."
+ assert_equals(range.comparePoint(node, offset), 0,
+ "Must return 0 if point is neither before start nor after end");
+ }, "Point " + i + " " + testPoints[i] + ", range " + j + " " + testRanges[j]);
+ }
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-constructor.html b/testing/web-platform/tests/dom/ranges/Range-constructor.html
new file mode 100644
index 0000000000..e8cfbef753
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-constructor.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>Range constructor test</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<div id=log></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+test(function() {
+ var range = new Range();
+ assert_equals(range.startContainer, document, "startContainer");
+ assert_equals(range.endContainer, document, "endContainer");
+ assert_equals(range.startOffset, 0, "startOffset");
+ assert_equals(range.endOffset, 0, "endOffset");
+ assert_true(range.collapsed, "collapsed");
+ assert_equals(range.commonAncestorContainer, document,
+ "commonAncestorContainer");
+});
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-deleteContents.html b/testing/web-platform/tests/dom/ranges/Range-deleteContents.html
new file mode 100644
index 0000000000..40dc400125
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-deleteContents.html
@@ -0,0 +1,337 @@
+<!doctype html>
+<title>Range.deleteContents() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<meta name=timeout content=long>
+<p>To debug test failures, add a query parameter "subtest" with the test id (like
+"?subtest=5"). Only that test will be run. Then you can look at the resulting
+iframe 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";
+
+testDiv.parentNode.removeChild(testDiv);
+
+var actualIframe = document.createElement("iframe");
+actualIframe.style.display = "none";
+document.body.appendChild(actualIframe);
+
+var expectedIframe = document.createElement("iframe");
+expectedIframe.style.display = "none";
+document.body.appendChild(expectedIframe);
+
+function restoreIframe(iframe, i) {
+ // Most of this function is designed to work around the fact that Opera
+ // doesn't let you add a doctype to a document that no longer has one, in
+ // any way I can figure out. I eventually compromised on something that
+ // will still let Opera pass most tests that don't actually involve
+ // doctypes.
+ while (iframe.contentDocument.firstChild
+ && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.firstChild);
+ }
+
+ while (iframe.contentDocument.lastChild
+ && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
+ }
+
+ if (!iframe.contentDocument.firstChild) {
+ // This will throw an exception in Opera if we reach here, which is why
+ // I try to avoid it. It will never happen in a browser that obeys the
+ // spec, so it's really just insurance. I don't think it actually gets
+ // hit by anything.
+ iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
+ }
+ iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
+ iframe.contentWindow.setupRangeTests();
+ iframe.contentWindow.testRangeInput = testRanges[i];
+ iframe.contentWindow.run();
+}
+
+function myDeleteContents(range) {
+ // "If the context object's start and end are the same, abort this method."
+ if (range.startContainer == range.endContainer
+ && range.startOffset == range.endOffset) {
+ return;
+ }
+
+ // "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, ProcessingInstruction, or Comment node, replace data with node
+ // original start node, offset original start offset, count original end
+ // offset minus original start offset, and data the empty string, and then
+ // terminate these steps"
+ if (originalStartNode == originalEndNode
+ && (range.startContainer.nodeType == Node.TEXT_NODE
+ || range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE
+ || range.startContainer.nodeType == Node.COMMENT_NODE)) {
+ originalStartNode.deleteData(originalStartOffset, originalEndOffset - originalStartOffset);
+ return;
+ }
+
+ // "Let nodes to remove be a list of all the Nodes that are contained in
+ // the context object, in tree order, omitting any Node whose parent is
+ // also contained in the context object."
+ //
+ // We rely on the fact that the contained nodes must lie in tree order
+ // between the start node, and the end node's last descendant (inclusive).
+ var nodesToRemove = [];
+ var stop = nextNodeDescendants(range.endContainer);
+ for (var node = range.startContainer; node != stop; node = nextNode(node)) {
+ if (isContained(node, range)
+ && !(node.parentNode && isContained(node.parentNode, range))) {
+ nodesToRemove.push(node);
+ }
+ }
+
+ // "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;
+ var newOffset;
+ if (originalStartNode == originalEndNode
+ || originalEndNode.compareDocumentPosition(originalStartNode) & Node.DOCUMENT_POSITION_CONTAINS) {
+ 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
+ && referenceNode.parentNode != originalEndNode
+ && !(originalEndNode.compareDocumentPosition(referenceNode.parentNode) & Node.DOCUMENT_POSITION_CONTAINS)) {
+ 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 original start node is a Text, ProcessingInstruction, or Comment node,
+ // replace data with node original start node, offset original start offset,
+ // count original start node's length minus original start offset, data the
+ // empty start"
+ if (originalStartNode.nodeType == Node.TEXT_NODE
+ || originalStartNode.nodeType == Node.PROCESSING_INSTRUCTION_NODE
+ || originalStartNode.nodeType == Node.COMMENT_NODE) {
+ originalStartNode.deleteData(originalStartOffset, nodeLength(originalStartNode) - originalStartOffset);
+ }
+
+ // "For each node in nodes to remove, in order, remove node from its
+ // parent."
+ for (var i = 0; i < nodesToRemove.length; i++) {
+ nodesToRemove[i].parentNode.removeChild(nodesToRemove[i]);
+ }
+
+ // "If original end node is a Text, ProcessingInstruction, or Comment node,
+ // replace data with node original end node, offset 0, count original end
+ // offset, and data the empty string."
+ if (originalEndNode.nodeType == Node.TEXT_NODE
+ || originalEndNode.nodeType == Node.PROCESSING_INSTRUCTION_NODE
+ || originalEndNode.nodeType == Node.COMMENT_NODE) {
+ originalEndNode.deleteData(0, originalEndOffset);
+ }
+
+ // "Set the context object's start and end to (new node, new offset)."
+ range.setStart(newNode, newOffset);
+ range.setEnd(newNode, newOffset);
+}
+
+function testDeleteContents(i) {
+ restoreIframe(actualIframe, i);
+ restoreIframe(expectedIframe, i);
+
+ var actualRange = actualIframe.contentWindow.testRange;
+ var expectedRange = expectedIframe.contentWindow.testRange;
+ var actualRoots, expectedRoots;
+
+ domTests[i].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual deleteContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated deleteContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+
+ // Just to be pedantic, we'll test not only that the tree we're
+ // modifying is the same in expected vs. actual, but also that all the
+ // nodes originally in it were the same. Typically some nodes will
+ // become detached when the algorithm is run, but they still exist and
+ // references can still be kept to them, so they should also remain the
+ // same.
+ //
+ // We initialize the list to all nodes, and later on remove all the
+ // ones which still have parents, since the parents will presumably be
+ // tested for isEqualNode() and checking the children would be
+ // redundant.
+ var actualAllNodes = [];
+ var node = furthestAncestor(actualRange.startContainer);
+ do {
+ actualAllNodes.push(node);
+ } while (node = nextNode(node));
+
+ var expectedAllNodes = [];
+ var node = furthestAncestor(expectedRange.startContainer);
+ do {
+ expectedAllNodes.push(node);
+ } while (node = nextNode(node));
+
+ actualRange.deleteContents();
+ myDeleteContents(expectedRange);
+
+ actualRoots = [];
+ for (var j = 0; j < actualAllNodes.length; j++) {
+ if (!actualAllNodes[j].parentNode) {
+ actualRoots.push(actualAllNodes[j]);
+ }
+ }
+
+ expectedRoots = [];
+ for (var j = 0; j < expectedAllNodes.length; j++) {
+ if (!expectedAllNodes[j].parentNode) {
+ expectedRoots.push(expectedAllNodes[j]);
+ }
+ }
+
+ for (var j = 0; j < actualRoots.length; j++) {
+ if (!actualRoots[j].isEqualNode(expectedRoots[j])) {
+ var msg = j ? "detached node #" + j : "tree root";
+ msg = "Actual and expected mismatch for " + msg + ". ";
+
+ // Find the specific error
+ var actual = actualRoots[j];
+ var expected = expectedRoots[j];
+
+ while (actual && expected) {
+ assert_equals(actual.nodeType, expected.nodeType,
+ msg + "First difference: differing nodeType");
+ assert_equals(actual.nodeName, expected.nodeName,
+ msg + "First difference: differing nodeName");
+ assert_equals(actual.nodeValue, expected.nodeValue,
+ msg + 'First difference: differing nodeValue (nodeName = "' + actual.nodeName + '")');
+ assert_equals(actual.childNodes.length, expected.childNodes.length,
+ msg + 'First difference: differing number of children (nodeName = "' + actual.nodeName + '")');
+ actual = nextNode(actual);
+ expected = nextNode(expected);
+ }
+
+ assert_unreached("DOMs were not equal but we couldn't figure out why");
+ }
+
+ if (j == 0) {
+ // Clearly something is wrong if the node lists are different
+ // lengths. We want to report this only after we've already
+ // checked the main tree for equality, though, so it doesn't
+ // mask more interesting errors.
+ assert_equals(actualRoots.length, expectedRoots.length,
+ "Actual and expected DOMs were broken up into a different number of pieces by deleteContents() (this probably means you created or detached nodes when you weren't supposed to)");
+ }
+ }
+ });
+ domTests[i].done();
+
+ positionTests[i].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual deleteContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated deleteContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+ assert_true(actualRoots[0].isEqualNode(expectedRoots[0]),
+ "The resulting DOMs were not equal, so comparing positions makes no sense");
+
+ assert_equals(actualRange.startContainer, actualRange.endContainer,
+ "startContainer and endContainer must always be the same after deleteContents()");
+ assert_equals(actualRange.startOffset, actualRange.endOffset,
+ "startOffset and endOffset must always be the same after deleteContents()");
+ assert_equals(expectedRange.startContainer, expectedRange.endContainer,
+ "Test bug! Expected startContainer and endContainer must always be the same after deleteContents()");
+ assert_equals(expectedRange.startOffset, expectedRange.endOffset,
+ "Test bug! Expected startOffset and endOffset must always be the same after deleteContents()");
+
+ assert_equals(actualRange.startOffset, expectedRange.startOffset,
+ "Unexpected startOffset after deleteContents()");
+ // How do we decide that the two nodes are equal, since they're in
+ // different trees? Since the DOMs are the same, it's enough to check
+ // that the index in the parent is the same all the way up the tree.
+ // But we can first cheat by just checking they're actually equal.
+ assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer),
+ "Unexpected startContainer after deleteContents(), expected " +
+ expectedRange.startContainer.nodeName.toLowerCase() + " but got " +
+ actualRange.startContainer.nodeName.toLowerCase());
+ var currentActual = actualRange.startContainer;
+ var currentExpected = expectedRange.startContainer;
+ var actual = "";
+ var expected = "";
+ while (currentActual && currentExpected) {
+ actual = indexOf(currentActual) + "-" + actual;
+ expected = indexOf(currentExpected) + "-" + expected;
+
+ currentActual = currentActual.parentNode;
+ currentExpected = currentExpected.parentNode;
+ }
+ actual = actual.substr(0, actual.length - 1);
+ expected = expected.substr(0, expected.length - 1);
+ assert_equals(actual, expected,
+ "startContainer superficially looks right but is actually the wrong node if you trace back its index in all its ancestors (I'm surprised this actually happened");
+ });
+ positionTests[i].done();
+}
+
+// First test a detached Range, synchronously
+test(function() {
+ var range = document.createRange();
+ range.detach();
+ range.deleteContents();
+}, "Detached Range");
+
+var iStart = 0;
+var iStop = testRanges.length;
+
+if (/subtest=[0-9]+/.test(location.search)) {
+ var matches = /subtest=([0-9]+)/.exec(location.search);
+ iStart = Number(matches[1]);
+ iStop = Number(matches[1]) + 1;
+}
+
+var domTests = [];
+var positionTests = [];
+
+for (var i = iStart; i < iStop; i++) {
+ domTests[i] = async_test("Resulting DOM for range " + i + " " + testRanges[i]);
+ positionTests[i] = async_test("Resulting cursor position for range " + i + " " + testRanges[i]);
+}
+
+var referenceDoc = document.implementation.createHTMLDocument("");
+referenceDoc.removeChild(referenceDoc.documentElement);
+
+actualIframe.onload = function() {
+ expectedIframe.onload = function() {
+ for (var i = iStart; i < iStop; i++) {
+ testDeleteContents(i);
+ }
+ }
+ expectedIframe.src = "Range-test-iframe.html";
+ referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
+}
+actualIframe.src = "Range-test-iframe.html";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-detach.html b/testing/web-platform/tests/dom/ranges/Range-detach.html
new file mode 100644
index 0000000000..ac35d71369
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-detach.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>Range.detach</title>
+<link rel="author" title="Ms2ger" href="mailto:ms2ger@gmail.com">
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var r = document.createRange();
+ r.detach()
+ r.detach()
+})
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-extractContents.html b/testing/web-platform/tests/dom/ranges/Range-extractContents.html
new file mode 100644
index 0000000000..88f8fa55f8
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-extractContents.html
@@ -0,0 +1,252 @@
+<!doctype html>
+<title>Range.extractContents() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<meta name=timeout content=long>
+<p>To debug test failures, add a query parameter "subtest" with the test id (like
+"?subtest=5"). Only that test will be run. Then you can look at the resulting
+iframe 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";
+
+testDiv.parentNode.removeChild(testDiv);
+
+var actualIframe = document.createElement("iframe");
+actualIframe.style.display = "none";
+document.body.appendChild(actualIframe);
+
+var expectedIframe = document.createElement("iframe");
+expectedIframe.style.display = "none";
+document.body.appendChild(expectedIframe);
+
+function restoreIframe(iframe, i) {
+ // Most of this function is designed to work around the fact that Opera
+ // doesn't let you add a doctype to a document that no longer has one, in
+ // any way I can figure out. I eventually compromised on something that
+ // will still let Opera pass most tests that don't actually involve
+ // doctypes.
+ while (iframe.contentDocument.firstChild
+ && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.firstChild);
+ }
+
+ while (iframe.contentDocument.lastChild
+ && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
+ }
+
+ if (!iframe.contentDocument.firstChild) {
+ // This will throw an exception in Opera if we reach here, which is why
+ // I try to avoid it. It will never happen in a browser that obeys the
+ // spec, so it's really just insurance. I don't think it actually gets
+ // hit by anything.
+ iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
+ }
+ iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
+ iframe.contentWindow.setupRangeTests();
+ iframe.contentWindow.testRangeInput = testRanges[i];
+ iframe.contentWindow.run();
+}
+
+function testExtractContents(i) {
+ restoreIframe(actualIframe, i);
+ restoreIframe(expectedIframe, i);
+
+ var actualRange = actualIframe.contentWindow.testRange;
+ var expectedRange = expectedIframe.contentWindow.testRange;
+ var actualFrag, expectedFrag;
+ var actualRoots, expectedRoots;
+
+ domTests[i].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual extractContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated extractContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+
+ // Just to be pedantic, we'll test not only that the tree we're
+ // modifying is the same in expected vs. actual, but also that all the
+ // nodes originally in it were the same. Typically some nodes will
+ // become detached when the algorithm is run, but they still exist and
+ // references can still be kept to them, so they should also remain the
+ // same.
+ //
+ // We initialize the list to all nodes, and later on remove all the
+ // ones which still have parents, since the parents will presumably be
+ // tested for isEqualNode() and checking the children would be
+ // redundant.
+ var actualAllNodes = [];
+ var node = furthestAncestor(actualRange.startContainer);
+ do {
+ actualAllNodes.push(node);
+ } while (node = nextNode(node));
+
+ var expectedAllNodes = [];
+ var node = furthestAncestor(expectedRange.startContainer);
+ do {
+ expectedAllNodes.push(node);
+ } while (node = nextNode(node));
+
+ expectedFrag = myExtractContents(expectedRange);
+ if (typeof expectedFrag == "string") {
+ assert_throws_dom(
+ expectedFrag,
+ actualIframe.contentWindow.DOMException,
+ function() {
+ actualRange.extractContents();
+ }
+ );
+ } else {
+ actualFrag = actualRange.extractContents();
+ }
+
+ actualRoots = [];
+ for (var j = 0; j < actualAllNodes.length; j++) {
+ if (!actualAllNodes[j].parentNode) {
+ actualRoots.push(actualAllNodes[j]);
+ }
+ }
+
+ expectedRoots = [];
+ for (var j = 0; j < expectedAllNodes.length; j++) {
+ if (!expectedAllNodes[j].parentNode) {
+ expectedRoots.push(expectedAllNodes[j]);
+ }
+ }
+
+ for (var j = 0; j < actualRoots.length; j++) {
+ assertNodesEqual(actualRoots[j], expectedRoots[j], j ? "detached node #" + j : "tree root");
+
+ if (j == 0) {
+ // Clearly something is wrong if the node lists are different
+ // lengths. We want to report this only after we've already
+ // checked the main tree for equality, though, so it doesn't
+ // mask more interesting errors.
+ assert_equals(actualRoots.length, expectedRoots.length,
+ "Actual and expected DOMs were broken up into a different number of pieces by extractContents() (this probably means you created or detached nodes when you weren't supposed to)");
+ }
+ }
+ });
+ domTests[i].done();
+
+ positionTests[i].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual extractContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated extractContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+
+ assert_true(actualRoots[0].isEqualNode(expectedRoots[0]),
+ "The resulting DOMs were not equal, so comparing positions makes no sense");
+
+ if (typeof expectedFrag == "string") {
+ // It's no longer true that, e.g., startContainer and endContainer
+ // must always be the same
+ return;
+ }
+ assert_equals(actualRange.startContainer, actualRange.endContainer,
+ "startContainer and endContainer must always be the same after extractContents()");
+ assert_equals(actualRange.startOffset, actualRange.endOffset,
+ "startOffset and endOffset must always be the same after extractContents()");
+ assert_equals(expectedRange.startContainer, expectedRange.endContainer,
+ "Test bug! Expected startContainer and endContainer must always be the same after extractContents()");
+ assert_equals(expectedRange.startOffset, expectedRange.endOffset,
+ "Test bug! Expected startOffset and endOffset must always be the same after extractContents()");
+
+ assert_equals(actualRange.startOffset, expectedRange.startOffset,
+ "Unexpected startOffset after extractContents()");
+ // How do we decide that the two nodes are equal, since they're in
+ // different trees? Since the DOMs are the same, it's enough to check
+ // that the index in the parent is the same all the way up the tree.
+ // But we can first cheat by just checking they're actually equal.
+ assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer),
+ "Unexpected startContainer after extractContents(), expected " +
+ expectedRange.startContainer.nodeName.toLowerCase() + " but got " +
+ actualRange.startContainer.nodeName.toLowerCase());
+ var currentActual = actualRange.startContainer;
+ var currentExpected = expectedRange.startContainer;
+ var actual = "";
+ var expected = "";
+ while (currentActual && currentExpected) {
+ actual = indexOf(currentActual) + "-" + actual;
+ expected = indexOf(currentExpected) + "-" + expected;
+
+ currentActual = currentActual.parentNode;
+ currentExpected = currentExpected.parentNode;
+ }
+ actual = actual.substr(0, actual.length - 1);
+ expected = expected.substr(0, expected.length - 1);
+ assert_equals(actual, expected,
+ "startContainer superficially looks right but is actually the wrong node if you trace back its index in all its ancestors (I'm surprised this actually happened");
+ });
+ positionTests[i].done();
+
+ fragTests[i].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual extractContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated extractContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+
+ if (typeof expectedFrag == "string") {
+ // Comparing makes no sense
+ return;
+ }
+ assertNodesEqual(actualFrag, expectedFrag,
+ "returned fragment");
+ });
+ fragTests[i].done();
+}
+
+// First test a detached Range, synchronously
+test(function() {
+ var range = document.createRange();
+ range.detach();
+ assert_array_equals(range.extractContents().childNodes, []);
+}, "Detached Range");
+
+var iStart = 0;
+var iStop = testRanges.length;
+
+if (/subtest=[0-9]+/.test(location.search)) {
+ var matches = /subtest=([0-9]+)/.exec(location.search);
+ iStart = Number(matches[1]);
+ iStop = Number(matches[1]) + 1;
+}
+
+var domTests = [];
+var positionTests = [];
+var fragTests = [];
+
+for (var i = iStart; i < iStop; i++) {
+ domTests[i] = async_test("Resulting DOM for range " + i + " " + testRanges[i]);
+ positionTests[i] = async_test("Resulting cursor position for range " + i + " " + testRanges[i]);
+ fragTests[i] = async_test("Returned fragment for range " + i + " " + testRanges[i]);
+}
+
+var referenceDoc = document.implementation.createHTMLDocument("");
+referenceDoc.removeChild(referenceDoc.documentElement);
+
+actualIframe.onload = function() {
+ expectedIframe.onload = function() {
+ for (var i = iStart; i < iStop; i++) {
+ testExtractContents(i);
+ }
+ }
+ expectedIframe.src = "Range-test-iframe.html";
+ referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
+}
+actualIframe.src = "Range-test-iframe.html";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-insertNode.html b/testing/web-platform/tests/dom/ranges/Range-insertNode.html
new file mode 100644
index 0000000000..b0a9d43f98
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-insertNode.html
@@ -0,0 +1,286 @@
+<!doctype html>
+<title>Range.insertNode() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<meta name=timeout content=long>
+<p>To debug test failures, add a query parameter "subtest" with the test id (like
+"?subtest=5,16"). 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";
+
+testDiv.parentNode.removeChild(testDiv);
+
+function restoreIframe(iframe, i, j) {
+ // Most of this function is designed to work around the fact that Opera
+ // doesn't let you add a doctype to a document that no longer has one, in
+ // any way I can figure out. I eventually compromised on something that
+ // will still let Opera pass most tests that don't actually involve
+ // doctypes.
+ while (iframe.contentDocument.firstChild
+ && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.firstChild);
+ }
+
+ while (iframe.contentDocument.lastChild
+ && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
+ }
+
+ if (!iframe.contentDocument.firstChild) {
+ // This will throw an exception in Opera if we reach here, which is why
+ // I try to avoid it. It will never happen in a browser that obeys the
+ // spec, so it's really just insurance. I don't think it actually gets
+ // hit by anything.
+ iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
+ }
+ iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
+ iframe.contentWindow.setupRangeTests();
+ iframe.contentWindow.testRangeInput = testRangesShort[i];
+ iframe.contentWindow.testNodeInput = testNodesShort[j];
+ iframe.contentWindow.run();
+}
+
+function testInsertNode(i, j) {
+ var actualRange;
+ var expectedRange;
+ var actualNode;
+ var expectedNode;
+ var actualRoots = [];
+ var expectedRoots = [];
+
+ var detached = false;
+
+ domTests[i][j].step(function() {
+ restoreIframe(actualIframe, i, j);
+ restoreIframe(expectedIframe, i, j);
+
+ actualRange = actualIframe.contentWindow.testRange;
+ expectedRange = expectedIframe.contentWindow.testRange;
+ actualNode = actualIframe.contentWindow.testNode;
+ expectedNode = expectedIframe.contentWindow.testNode;
+
+ try {
+ actualRange.collapsed;
+ } catch (e) {
+ detached = true;
+ }
+
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual insertNode()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated insertNode()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_not_equals(actualRange, null,
+ "Range produced in actual iframe was null");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+ assert_not_equals(expectedRange, null,
+ "Range produced in expected iframe was null");
+ assert_equals(typeof actualNode, "object",
+ "typeof Node produced in actual iframe");
+ assert_not_equals(actualNode, null,
+ "Node produced in actual iframe was null");
+ assert_equals(typeof expectedNode, "object",
+ "typeof Node produced in expected iframe");
+ assert_not_equals(expectedNode, null,
+ "Node produced in expected iframe was null");
+
+ // We want to test that the trees containing the ranges are equal, and
+ // also the trees containing the moved nodes. These might not be the
+ // same, if we're inserting a node from a detached tree or a different
+ // document.
+ //
+ // Detached ranges are always in the contentDocument.
+ if (detached) {
+ actualRoots.push(actualIframe.contentDocument);
+ expectedRoots.push(expectedIframe.contentDocument);
+ } else {
+ actualRoots.push(furthestAncestor(actualRange.startContainer));
+ expectedRoots.push(furthestAncestor(expectedRange.startContainer));
+ }
+
+ if (furthestAncestor(actualNode) != actualRoots[0]) {
+ actualRoots.push(furthestAncestor(actualNode));
+ }
+ if (furthestAncestor(expectedNode) != expectedRoots[0]) {
+ expectedRoots.push(furthestAncestor(expectedNode));
+ }
+
+ assert_equals(actualRoots.length, expectedRoots.length,
+ "Either the actual node and actual range are in the same tree but the expected are in different trees, or vice versa");
+
+ // This doctype stuff is to work around the fact that Opera 11.00 will
+ // move around doctypes within a document, even to totally invalid
+ // positions, but it won't allow a new doctype to be added to a
+ // document in any way I can figure out. So if we try moving a doctype
+ // to some invalid place, in Opera it will actually succeed, and then
+ // restoreIframe() will remove the doctype along with the root element,
+ // and then nothing can re-add the doctype. So instead, we catch it
+ // during the test itself and move it back to the right place while we
+ // still can.
+ //
+ // I spent *way* too much time debugging and working around this bug.
+ var actualDoctype = actualIframe.contentDocument.doctype;
+ var expectedDoctype = expectedIframe.contentDocument.doctype;
+
+ var result;
+ try {
+ result = myInsertNode(expectedRange, expectedNode);
+ } catch (e) {
+ if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
+ expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
+ }
+ throw e;
+ }
+ if (typeof result == "string") {
+ assert_throws_dom(result, actualIframe.contentWindow.DOMException, function() {
+ try {
+ actualRange.insertNode(actualNode);
+ } catch (e) {
+ if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
+ expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
+ }
+ if (actualDoctype != actualIframe.contentDocument.firstChild) {
+ actualIframe.contentDocument.insertBefore(actualDoctype, actualIframe.contentDocument.firstChild);
+ }
+ throw e;
+ }
+ }, "A " + result + " DOMException must be thrown in this case");
+ // Don't return, we still need to test DOM equality
+ } else {
+ try {
+ actualRange.insertNode(actualNode);
+ } catch (e) {
+ if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
+ expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
+ }
+ if (actualDoctype != actualIframe.contentDocument.firstChild) {
+ actualIframe.contentDocument.insertBefore(actualDoctype, actualIframe.contentDocument.firstChild);
+ }
+ throw e;
+ }
+ }
+
+ for (var k = 0; k < actualRoots.length; k++) {
+ assertNodesEqual(actualRoots[k], expectedRoots[k], k ? "moved node's tree root" : "range's tree root");
+ }
+ });
+ domTests[i][j].done();
+
+ positionTests[i][j].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual insertNode()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated insertNode()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_not_equals(actualRange, null,
+ "Range produced in actual iframe was null");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+ assert_not_equals(expectedRange, null,
+ "Range produced in expected iframe was null");
+ assert_equals(typeof actualNode, "object",
+ "typeof Node produced in actual iframe");
+ assert_not_equals(actualNode, null,
+ "Node produced in actual iframe was null");
+ assert_equals(typeof expectedNode, "object",
+ "typeof Node produced in expected iframe");
+ assert_not_equals(expectedNode, null,
+ "Node produced in expected iframe was null");
+
+ for (var k = 0; k < actualRoots.length; k++) {
+ assertNodesEqual(actualRoots[k], expectedRoots[k], k ? "moved node's tree root" : "range's tree root");
+ }
+
+ if (detached) {
+ // No further tests we can do
+ return;
+ }
+
+ assert_equals(actualRange.startOffset, expectedRange.startOffset,
+ "Unexpected startOffset after insertNode()");
+ assert_equals(actualRange.endOffset, expectedRange.endOffset,
+ "Unexpected endOffset after insertNode()");
+ // How do we decide that the two nodes are equal, since they're in
+ // different trees? Since the DOMs are the same, it's enough to check
+ // that the index in the parent is the same all the way up the tree.
+ // But we can first cheat by just checking they're actually equal.
+ assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer),
+ "Unexpected startContainer after insertNode(), expected " +
+ expectedRange.startContainer.nodeName.toLowerCase() + " but got " +
+ actualRange.startContainer.nodeName.toLowerCase());
+ var currentActual = actualRange.startContainer;
+ var currentExpected = expectedRange.startContainer;
+ var actual = "";
+ var expected = "";
+ while (currentActual && currentExpected) {
+ actual = indexOf(currentActual) + "-" + actual;
+ expected = indexOf(currentExpected) + "-" + expected;
+
+ currentActual = currentActual.parentNode;
+ currentExpected = currentExpected.parentNode;
+ }
+ actual = actual.substr(0, actual.length - 1);
+ expected = expected.substr(0, expected.length - 1);
+ assert_equals(actual, expected,
+ "startContainer superficially looks right but is actually the wrong node if you trace back its index in all its ancestors (I'm surprised this actually happened");
+ });
+ positionTests[i][j].done();
+}
+
+testRanges.unshift('"detached"');
+
+var iStart = 0;
+var iStop = testRangesShort.length;
+var jStart = 0;
+var jStop = testNodesShort.length;
+
+if (/subtest=[0-9]+,[0-9]+/.test(location.search)) {
+ var matches = /subtest=([0-9]+),([0-9]+)/.exec(location.search);
+ iStart = Number(matches[1]);
+ iStop = Number(matches[1]) + 1;
+ jStart = Number(matches[2]) + 0;
+ jStop = Number(matches[2]) + 1;
+}
+
+var domTests = [];
+var positionTests = [];
+for (var i = iStart; i < iStop; i++) {
+ domTests[i] = [];
+ positionTests[i] = [];
+ for (var j = jStart; j < jStop; j++) {
+ domTests[i][j] = async_test(i + "," + j + ": resulting DOM for range " + testRangesShort[i] + ", node " + testNodesShort[j]);
+ positionTests[i][j] = async_test(i + "," + j + ": resulting range position for range " + testRangesShort[i] + ", node " + testNodesShort[j]);
+ }
+}
+
+var actualIframe = document.createElement("iframe");
+actualIframe.style.display = "none";
+document.body.appendChild(actualIframe);
+
+var expectedIframe = document.createElement("iframe");
+expectedIframe.style.display = "none";
+document.body.appendChild(expectedIframe);
+
+var referenceDoc = document.implementation.createHTMLDocument("");
+referenceDoc.removeChild(referenceDoc.documentElement);
+
+actualIframe.onload = function() {
+ expectedIframe.onload = function() {
+ for (var i = iStart; i < iStop; i++) {
+ for (var j = jStart; j < jStop; j++) {
+ testInsertNode(i, j);
+ }
+ }
+ }
+ expectedIframe.src = "Range-test-iframe.html";
+ referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
+}
+actualIframe.src = "Range-test-iframe.html";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-intersectsNode-2.html b/testing/web-platform/tests/dom/ranges/Range-intersectsNode-2.html
new file mode 100644
index 0000000000..48072d98af
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-intersectsNode-2.html
@@ -0,0 +1,36 @@
+<!doctype htlml>
+<title>Range.intersectsNode</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="div"><span id="s0">s0</span><span id="s1">s1</span><span id="s2">s2</span></div>
+<script>
+// Taken from Chromium bug: http://crbug.com/822510
+test(() => {
+ const range = new Range();
+ const div = document.getElementById('div');
+ const s0 = document.getElementById('s0');
+ const s1 = document.getElementById('s1');
+ const s2 = document.getElementById('s2');
+
+ // Range encloses s0
+ range.setStart(div, 0);
+ range.setEnd(div, 1);
+ assert_true(range.intersectsNode(s0), '[s0] range.intersectsNode(s0)');
+ assert_false(range.intersectsNode(s1), '[s0] range.intersectsNode(s1)');
+ assert_false(range.intersectsNode(s2), '[s0] range.intersectsNode(s2)');
+
+ // Range encloses s1
+ range.setStart(div, 1);
+ range.setEnd(div, 2);
+ assert_false(range.intersectsNode(s0), '[s1] range.intersectsNode(s0)');
+ assert_true(range.intersectsNode(s1), '[s1] range.intersectsNode(s1)');
+ assert_false(range.intersectsNode(s2), '[s1] range.intersectsNode(s2)');
+
+ // Range encloses s2
+ range.setStart(div, 2);
+ range.setEnd(div, 3);
+ assert_false(range.intersectsNode(s0), '[s2] range.intersectsNode(s0)');
+ assert_false(range.intersectsNode(s1), '[s2] range.intersectsNode(s1)');
+ assert_true(range.intersectsNode(s2), '[s2] range.intersectsNode(s2)');
+}, 'Range.intersectsNode() simple cases');
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-intersectsNode-binding.html b/testing/web-platform/tests/dom/ranges/Range-intersectsNode-binding.html
new file mode 100644
index 0000000000..57d159b030
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-intersectsNode-binding.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<title>Range.intersectsNode</title>
+<link rel="author" title="Ms2ger" href="mailto:ms2ger@gmail.com">
+<meta name=timeout content=long>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var r = document.createRange();
+ assert_throws_js(TypeError, function() { r.intersectsNode(); });
+ assert_throws_js(TypeError, function() { r.intersectsNode(null); });
+ assert_throws_js(TypeError, function() { r.intersectsNode(undefined); });
+ assert_throws_js(TypeError, function() { r.intersectsNode(42); });
+ assert_throws_js(TypeError, function() { r.intersectsNode("foo"); });
+ assert_throws_js(TypeError, function() { r.intersectsNode({}); });
+ r.detach();
+ assert_throws_js(TypeError, function() { r.intersectsNode(); });
+ assert_throws_js(TypeError, function() { r.intersectsNode(null); });
+ assert_throws_js(TypeError, function() { r.intersectsNode(undefined); });
+ assert_throws_js(TypeError, function() { r.intersectsNode(42); });
+ assert_throws_js(TypeError, function() { r.intersectsNode("foo"); });
+ assert_throws_js(TypeError, function() { r.intersectsNode({}); });
+}, "Calling intersectsNode without an argument or with an invalid argument should throw a TypeError.")
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-intersectsNode-shadow.html b/testing/web-platform/tests/dom/ranges/Range-intersectsNode-shadow.html
new file mode 100644
index 0000000000..8219ba8285
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-intersectsNode-shadow.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<title>Range.intersectsNode with Shadow DOM</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="host"></div>
+<script>
+test(() => {
+ const host = document.getElementById("host");
+ host.attachShadow({ mode: "open" }).innerHTML = `<span>ABC</span>`;
+
+ const range = document.createRange();
+ range.selectNode(document.body);
+
+ assert_true(range.intersectsNode(host), "Should intersect host");
+ assert_false(range.intersectsNode(host.shadowRoot), "Should not intersect shadow root");
+ assert_false(range.intersectsNode(host.shadowRoot.firstElementChild), "Should not intersect shadow span");
+}, "Range.intersectsNode() doesn't return true for shadow children in other trees");
+</script>
+
diff --git a/testing/web-platform/tests/dom/ranges/Range-intersectsNode.html b/testing/web-platform/tests/dom/ranges/Range-intersectsNode.html
new file mode 100644
index 0000000000..97e10f6f0d
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-intersectsNode.html
@@ -0,0 +1,70 @@
+<!doctype html>
+<title>Range.intersectsNode() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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";
+
+// Will be filled in on the first run for that range
+var testRangesCached = [];
+
+for (var i = 0; i < testNodes.length; i++) {
+ var node = eval(testNodes[i]);
+
+ for (var j = 0; j < testRanges.length; j++) {
+ test(function() {
+ if (testRangesCached[j] === undefined) {
+ try {
+ testRangesCached[j] = rangeFromEndpoints(eval(testRanges[i]));
+ } catch(e) {
+ testRangesCached[j] = null;
+ }
+ }
+ assert_not_equals(testRangesCached[j], null,
+ "Setting up the range failed");
+
+ var range = testRangesCached[j].cloneRange();
+
+ // "If node's root is different from the context object's root,
+ // return false and terminate these steps."
+ if (furthestAncestor(node) !== furthestAncestor(range.startContainer)) {
+ assert_equals(range.intersectsNode(node), false,
+ "Must return false if node and range have different roots");
+ return;
+ }
+
+ // "Let parent be node's parent."
+ var parent_ = node.parentNode;
+
+ // "If parent is null, return true and terminate these steps."
+ if (!parent_) {
+ assert_equals(range.intersectsNode(node), true,
+ "Must return true if node's parent is null");
+ return;
+ }
+
+ // "Let offset be node's index."
+ var offset = indexOf(node);
+
+ // "If (parent, offset) is before end and (parent, offset + 1) is
+ // after start, return true and terminate these steps."
+ if (getPosition(parent_, offset, range.endContainer, range.endOffset) === "before"
+ && getPosition(parent_, offset + 1, range.startContainer, range.startOffset) === "after") {
+ assert_equals(range.intersectsNode(node), true,
+ "Must return true if (parent, offset) is before range end and (parent, offset + 1) is after range start");
+ return;
+ }
+
+ // "Return false."
+ assert_equals(range.intersectsNode(node), false,
+ "Must return false if (parent, offset) is not before range end or (parent, offset + 1) is not after range start");
+ }, "Node " + i + " " + testNodes[i] + ", range " + j + " " + testRanges[j]);
+ }
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-isPointInRange.html b/testing/web-platform/tests/dom/ranges/Range-isPointInRange.html
new file mode 100644
index 0000000000..80db97e844
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-isPointInRange.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<title>Range.isPointInRange() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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";
+
+var testRangesCached = [];
+test(function() {
+ for (var j = 0; j < testRanges.length; j++) {
+ test(function() {
+ testRangesCached[j] = rangeFromEndpoints(eval(testRanges[j]));
+ }, "Set up for range " + j + " " + testRanges[j]);
+ }
+ var detachedRange = document.createRange();
+ detachedRange.detach();
+ testRanges.push("detached");
+ testRangesCached.push(detachedRange);
+}, "Setup");
+
+for (var i = 0; i < testPoints.length; i++) {
+ var node = eval(testPoints[i])[0];
+ var offset = eval(testPoints[i])[1];
+
+ // isPointInRange is an unsigned long, so per WebIDL, we need to treat it
+ // as though it wrapped to an unsigned 32-bit integer.
+ var normalizedOffset = offset % Math.pow(2, 32);
+ if (normalizedOffset < 0) {
+ normalizedOffset += Math.pow(2, 32);
+ }
+
+ for (var j = 0; j < testRanges.length; j++) {
+ test(function() {
+ var range = testRangesCached[j].cloneRange();
+
+ // "If node's root is different from the context object's root,
+ // return false and terminate these steps."
+ if (furthestAncestor(node) !== furthestAncestor(range.startContainer)) {
+ assert_false(range.isPointInRange(node, offset),
+ "Must return false if node has a different root from the context object");
+ return;
+ }
+
+ // "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() {
+ range.isPointInRange(node, offset);
+ }, "Must throw InvalidNodeTypeError if node is a doctype");
+ return;
+ }
+
+ // "If offset is greater than node's length, throw an
+ // "IndexSizeError" exception and terminate these steps."
+ if (normalizedOffset > nodeLength(node)) {
+ assert_throws_dom("INDEX_SIZE_ERR", function() {
+ range.isPointInRange(node, offset);
+ }, "Must throw IndexSizeError if offset is greater than length");
+ return;
+ }
+
+ // "If (node, offset) is before start or after end, return false
+ // and terminate these steps."
+ if (getPosition(node, normalizedOffset, range.startContainer, range.startOffset) === "before"
+ || getPosition(node, normalizedOffset, range.endContainer, range.endOffset) === "after") {
+ assert_false(range.isPointInRange(node, offset),
+ "Must return false if point is before start or after end");
+ return;
+ }
+
+ // "Return true."
+ assert_true(range.isPointInRange(node, offset),
+ "Must return true if point is not before start, after end, or in different tree");
+ }, "Point " + i + " " + testPoints[i] + ", range " + j + " " + testRanges[j]);
+ }
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-appendChild.html b/testing/web-platform/tests/dom/ranges/Range-mutations-appendChild.html
new file mode 100644
index 0000000000..5b5b5a55df
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-appendChild.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>Range mutation tests - appendChild</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(appendChildTests, function(params) { return params[0] + ".appendChild(" + params[1] + ")" }, testAppendChild);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-appendData.html b/testing/web-platform/tests/dom/ranges/Range-mutations-appendData.html
new file mode 100644
index 0000000000..1d4879d57c
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-appendData.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - appendData</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(appendDataTests, function(params) { return params[0] + ".appendData(" + params[1] + ")" }, testAppendData);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-dataChange.html b/testing/web-platform/tests/dom/ranges/Range-mutations-dataChange.html
new file mode 100644
index 0000000000..3683e8bb9b
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-dataChange.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - update data by IDL attributes</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(dataChangeTests, function(params) { return params[0] + "." + eval(params[1]) + " " + eval(params[2]) + ' ' + params[3] }, testDataChange);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-deleteData.html b/testing/web-platform/tests/dom/ranges/Range-mutations-deleteData.html
new file mode 100644
index 0000000000..5f2b852f5b
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-deleteData.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - deleteData</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(deleteDataTests, function(params) { return params[0] + ".deleteData(" + params[1] + ", " + params[2] + ")" }, testDeleteData);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-insertBefore.html b/testing/web-platform/tests/dom/ranges/Range-mutations-insertBefore.html
new file mode 100644
index 0000000000..c71b239547
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-insertBefore.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - insertBefore</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(insertBeforeTests, function(params) { return params[0] + ".insertBefore(" + params[1] + ", " + params[2] + ")" }, testInsertBefore);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-insertData.html b/testing/web-platform/tests/dom/ranges/Range-mutations-insertData.html
new file mode 100644
index 0000000000..fca533d503
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-insertData.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - insertData</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(insertDataTests, function(params) { return params[0] + ".insertData(" + params[1] + ", " + params[2] + ")" }, testInsertData);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-removeChild.html b/testing/web-platform/tests/dom/ranges/Range-mutations-removeChild.html
new file mode 100644
index 0000000000..a8d2381253
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-removeChild.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - removeChild</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(removeChildTests, function(params) { return params[0] + ".parentNode.removeChild(" + params[0] + ")" }, testRemoveChild);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-replaceChild.html b/testing/web-platform/tests/dom/ranges/Range-mutations-replaceChild.html
new file mode 100644
index 0000000000..a4ef0c365e
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-replaceChild.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - replaceChild</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(replaceChildTests, function(params) { return params[0] + ".replaceChild(" + params[1] + ", " + params[2] + ")" }, testReplaceChild);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-replaceData.html b/testing/web-platform/tests/dom/ranges/Range-mutations-replaceData.html
new file mode 100644
index 0000000000..55ddb146ea
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-replaceData.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - replaceData</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(replaceDataTests, function(params) { return params[0] + ".replaceData(" + params[1] + ", " + params[2] + ", " + params[3] + ")" }, testReplaceData);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations-splitText.html b/testing/web-platform/tests/dom/ranges/Range-mutations-splitText.html
new file mode 100644
index 0000000000..fbb4c8d9b6
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations-splitText.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<title>Range mutation tests - splitText</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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="Range-mutations.js"></script>
+<script>
+doTests(splitTextTests, function(params) { return params[0] + ".splitText(" + params[1] + ")" }, testSplitText);
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations.js b/testing/web-platform/tests/dom/ranges/Range-mutations.js
new file mode 100644
index 0000000000..23c013ac6a
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-mutations.js
@@ -0,0 +1,921 @@
+"use strict";
+
+// These tests probably use too much abstraction and too little copy-paste.
+// Reader beware.
+//
+// TODO:
+//
+// * Lots and lots and lots more different types of ranges
+// * insertBefore() with DocumentFragments
+// * Fill out other insert/remove tests
+// * normalize() (https://www.w3.org/Bugs/Public/show_bug.cgi?id=13843)
+
+// Give a textual description of the range we're testing, for the test names.
+function describeRange(startContainer, startOffset, endContainer, endOffset) {
+ if (startContainer == endContainer && startOffset == endOffset) {
+ return "range collapsed at (" + startContainer + ", " + startOffset + ")";
+ } else if (startContainer == endContainer) {
+ return "range on " + startContainer + " from " + startOffset + " to " + endOffset;
+ } else {
+ return "range from (" + startContainer + ", " + startOffset + ") to (" + endContainer + ", " + endOffset + ")";
+ }
+}
+
+// Lists of the various types of nodes we'll want to use. We use strings that
+// we can later eval(), so that we can produce legible test names.
+var textNodes = [
+ "paras[0].firstChild",
+ "paras[1].firstChild",
+ "foreignTextNode",
+ "xmlTextNode",
+ "detachedTextNode",
+ "detachedForeignTextNode",
+ "detachedXmlTextNode",
+];
+var commentNodes = [
+ "comment",
+ "foreignComment",
+ "xmlComment",
+ "detachedComment",
+ "detachedForeignComment",
+ "detachedXmlComment",
+];
+var characterDataNodes = textNodes.concat(commentNodes);
+
+// This function is slightly scary, but it works well enough, so . . .
+// sourceTests is an array of test data that will be altered in mysterious ways
+// before being passed off to doTest, descFn is something that takes an element
+// of sourceTests and produces the first part of a human-readable description
+// of the test, testFn is the function that doTest will call to do the actual
+// work and tell it what results to expect.
+function doTests(sourceTests, descFn, testFn) {
+ var tests = [];
+ for (var i = 0; i < sourceTests.length; i++) {
+ var params = sourceTests[i];
+ var len = params.length;
+ tests.push([
+ descFn(params) + ", with unselected " + describeRange(params[len - 4], params[len - 3], params[len - 2], params[len - 1]),
+ // The closure here ensures that the params that testFn get are the
+ // current version of params, not the version from the last
+ // iteration of this loop. We test that none of the parameters
+ // evaluate to undefined to catch bugs in our eval'ing, like
+ // mistyping a property name.
+ function(params) { return function() {
+ var evaledParams = params.map(eval);
+ for (var i = 0; i < evaledParams.length; i++) {
+ assert_not_equals(typeof evaledParams[i], "undefined",
+ "Test bug: " + params[i] + " is undefined");
+ }
+ return testFn.apply(null, evaledParams);
+ } }(params),
+ false,
+ params[len - 4],
+ params[len - 3],
+ params[len - 2],
+ params[len - 1]
+ ]);
+ tests.push([
+ descFn(params) + ", with selected " + describeRange(params[len - 4], params[len - 3], params[len - 2], params[len - 1]),
+ function(params) { return function(selectedRange) {
+ var evaledParams = params.slice(0, len - 4).map(eval);
+ for (var i = 0; i < evaledParams.length; i++) {
+ assert_not_equals(typeof evaledParams[i], "undefined",
+ "Test bug: " + params[i] + " is undefined");
+ }
+ // Override input range with the one that was actually selected when computing the expected result.
+ evaledParams = evaledParams.concat([selectedRange.startContainer, selectedRange.startOffset, selectedRange.endContainer, selectedRange.endOffset]);
+ return testFn.apply(null, evaledParams);
+ } }(params),
+ true,
+ params[len - 4],
+ params[len - 3],
+ params[len - 2],
+ params[len - 1]
+ ]);
+ }
+ generate_tests(doTest, tests);
+}
+
+// Set up the range, call the callback function to do the DOM modification and
+// tell us what to expect. The callback function needs to return a
+// four-element array with the expected start/end containers/offsets, and
+// receives no arguments. useSelection tells us whether the Range should be
+// added to a Selection and the Selection tested to ensure that the mutation
+// affects user selections as well as other ranges; every test is run with this
+// both false and true, because when it's set to true WebKit and Opera fail all
+// tests' sanity checks, which is unhelpful. The last four parameters just
+// tell us what range to build.
+function doTest(callback, useSelection, startContainer, startOffset, endContainer, endOffset) {
+ // Recreate all the test nodes in case they were altered by the last test
+ // run.
+ setupRangeTests();
+ startContainer = eval(startContainer);
+ startOffset = eval(startOffset);
+ endContainer = eval(endContainer);
+ endOffset = eval(endOffset);
+
+ var ownerDoc = startContainer.nodeType == Node.DOCUMENT_NODE
+ ? startContainer
+ : startContainer.ownerDocument;
+ var range = ownerDoc.createRange();
+ range.setStart(startContainer, startOffset);
+ range.setEnd(endContainer, endOffset);
+
+ if (useSelection) {
+ getSelection().removeAllRanges();
+ getSelection().addRange(range);
+
+ // Some browsers refuse to add a range unless it results in an actual visible selection.
+ if (!getSelection().rangeCount)
+ return;
+
+ // Override range with the one that was actually selected as it differs in some browsers.
+ range = getSelection().getRangeAt(0);
+ }
+
+ var expected = callback(range);
+
+ assert_equals(range.startContainer, expected[0],
+ "Wrong start container");
+ assert_equals(range.startOffset, expected[1],
+ "Wrong start offset");
+ assert_equals(range.endContainer, expected[2],
+ "Wrong end container");
+ assert_equals(range.endOffset, expected[3],
+ "Wrong end offset");
+}
+
+
+// Now we get to the specific tests.
+
+function testSplitText(oldNode, offset, startContainer, startOffset, endContainer, endOffset) {
+ // Save these for later
+ var originalStartOffset = startOffset;
+ var originalEndOffset = endOffset;
+ var originalLength = oldNode.length;
+
+ var newNode;
+ try {
+ newNode = oldNode.splitText(offset);
+ } catch (e) {
+ // Should only happen if offset is negative
+ return [startContainer, startOffset, endContainer, endOffset];
+ }
+
+ // First we adjust for replacing data:
+ //
+ // "Replace data with offset offset, count count, and data the empty
+ // string."
+ //
+ // That translates to offset = offset, count = originalLength - offset,
+ // data = "". node is oldNode.
+ //
+ // "For every boundary point whose node is node, and whose offset is
+ // greater than offset but less than or equal to offset plus count, set its
+ // offset to offset."
+ if (startContainer == oldNode
+ && startOffset > offset
+ && startOffset <= originalLength) {
+ startOffset = offset;
+ }
+
+ if (endContainer == oldNode
+ && endOffset > offset
+ && endOffset <= originalLength) {
+ endOffset = offset;
+ }
+
+ // "For every boundary point whose node is node, and whose offset is
+ // greater than offset plus count, add the length of data to its offset,
+ // then subtract count from it."
+ //
+ // Can't happen: offset plus count is originalLength.
+
+ // Now we insert a node, if oldNode's parent isn't null: "For each boundary
+ // point whose node is the new parent of the affected node and whose offset
+ // is greater than the new index of the affected node, add one to the
+ // boundary point's offset."
+ if (startContainer == oldNode.parentNode
+ && startOffset > 1 + indexOf(oldNode)) {
+ startOffset++;
+ }
+
+ if (endContainer == oldNode.parentNode
+ && endOffset > 1 + indexOf(oldNode)) {
+ endOffset++;
+ }
+
+ // Finally, the splitText stuff itself:
+ //
+ // "If parent is not null, run these substeps:
+ //
+ // * "For each range whose start node is node and start offset is greater
+ // than offset, set its start node to new node and decrease its start
+ // offset by offset.
+ //
+ // * "For each range whose end node is node and end offset is greater
+ // than offset, set its end node to new node and decrease its end offset
+ // by offset.
+ //
+ // * "For each range whose start node is parent and start offset is equal
+ // to the index of node + 1, increase its start offset by one.
+ //
+ // * "For each range whose end node is parent and end offset is equal to
+ // the index of node + 1, increase its end offset by one."
+ if (oldNode.parentNode) {
+ if (startContainer == oldNode && originalStartOffset > offset) {
+ startContainer = newNode;
+ startOffset = originalStartOffset - offset;
+ }
+
+ if (endContainer == oldNode && originalEndOffset > offset) {
+ endContainer = newNode;
+ endOffset = originalEndOffset - offset;
+ }
+
+ if (startContainer == oldNode.parentNode
+ && startOffset == 1 + indexOf(oldNode)) {
+ startOffset++;
+ }
+
+ if (endContainer == oldNode.parentNode
+ && endOffset == 1 + indexOf(oldNode)) {
+ endOffset++;
+ }
+ }
+
+ return [startContainer, startOffset, endContainer, endOffset];
+}
+
+// The offset argument is unsigned, so per WebIDL -1 should wrap to 4294967295,
+// which is probably longer than the length, so it should throw an exception.
+// This is no different from the other cases where the offset is longer than
+// the length, and the wrapping complicates my testing slightly, so I won't
+// bother testing negative values here or in other cases.
+var splitTextTests = [];
+for (var i = 0; i < textNodes.length; i++) {
+ var node = textNodes[i];
+ splitTextTests.push([node, 376, node, 0, node, 1]);
+ splitTextTests.push([node, 0, node, 0, node, 0]);
+ splitTextTests.push([node, 1, node, 1, node, 1]);
+ splitTextTests.push([node, node + ".length", node, node + ".length", node, node + ".length"]);
+ splitTextTests.push([node, 1, node, 1, node, 3]);
+ splitTextTests.push([node, 2, node, 1, node, 3]);
+ splitTextTests.push([node, 3, node, 1, node, 3]);
+}
+
+splitTextTests.push(
+ ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0]", 0],
+ ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0]", 1],
+ ["paras[0].firstChild", 1, "paras[0]", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 2, "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 3, "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 2, "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 3, "paras[0]", 0, "paras[0].firstChild", 3]
+);
+
+
+function testReplaceDataAlgorithm(node, offset, count, data, callback, startContainer, startOffset, endContainer, endOffset) {
+ // Mutation works the same any time DOM Core's "replace data" algorithm is
+ // invoked. node, offset, count, data are as in that algorithm. The
+ // callback is what does the actual setting. Not to be confused with
+ // testReplaceData, which tests the replaceData() method.
+
+ // Barring any provision to the contrary, the containers and offsets must
+ // not change.
+ var expectedStartContainer = startContainer;
+ var expectedStartOffset = startOffset;
+ var expectedEndContainer = endContainer;
+ var expectedEndOffset = endOffset;
+
+ var originalParent = node.parentNode;
+ var originalData = node.data;
+
+ var exceptionThrown = false;
+ try {
+ callback();
+ } catch (e) {
+ // Should only happen if offset is greater than length
+ exceptionThrown = true;
+ }
+
+ assert_equals(node.parentNode, originalParent,
+ "Sanity check failed: changing data changed the parent");
+
+ // "User agents must run the following steps whenever they replace data of
+ // a CharacterData node, as though they were written in the specification
+ // for that algorithm after all other steps. In particular, the steps must
+ // not be executed if the algorithm threw an exception."
+ if (exceptionThrown) {
+ assert_equals(node.data, originalData,
+ "Sanity check failed: exception thrown but data changed");
+ } else {
+ assert_equals(node.data,
+ originalData.substr(0, offset) + data + originalData.substr(offset + count),
+ "Sanity check failed: data not changed as expected");
+ }
+
+ // "For every boundary point whose node is node, and whose offset is
+ // greater than offset but less than or equal to offset plus count, set
+ // its offset to offset."
+ if (!exceptionThrown
+ && startContainer == node
+ && startOffset > offset
+ && startOffset <= offset + count) {
+ expectedStartOffset = offset;
+ }
+
+ if (!exceptionThrown
+ && endContainer == node
+ && endOffset > offset
+ && endOffset <= offset + count) {
+ expectedEndOffset = offset;
+ }
+
+ // "For every boundary point whose node is node, and whose offset is
+ // greater than offset plus count, add the length of data to its offset,
+ // then subtract count from it."
+ if (!exceptionThrown
+ && startContainer == node
+ && startOffset > offset + count) {
+ expectedStartOffset += data.length - count;
+ }
+
+ if (!exceptionThrown
+ && endContainer == node
+ && endOffset > offset + count) {
+ expectedEndOffset += data.length - count;
+ }
+
+ return [expectedStartContainer, expectedStartOffset, expectedEndContainer, expectedEndOffset];
+}
+
+function testInsertData(node, offset, data, startContainer, startOffset, endContainer, endOffset) {
+ return testReplaceDataAlgorithm(node, offset, 0, data,
+ function() { node.insertData(offset, data) },
+ startContainer, startOffset, endContainer, endOffset);
+}
+
+var insertDataTests = [];
+for (var i = 0; i < characterDataNodes.length; i++) {
+ var node = characterDataNodes[i];
+ insertDataTests.push([node, 376, '"foo"', node, 0, node, 1]);
+ insertDataTests.push([node, 0, '"foo"', node, 0, node, 0]);
+ insertDataTests.push([node, 1, '"foo"', node, 1, node, 1]);
+ insertDataTests.push([node, node + ".length", '"foo"', node, node + ".length", node, node + ".length"]);
+ insertDataTests.push([node, 1, '"foo"', node, 1, node, 3]);
+ insertDataTests.push([node, 2, '"foo"', node, 1, node, 3]);
+ insertDataTests.push([node, 3, '"foo"', node, 1, node, 3]);
+
+ insertDataTests.push([node, 376, '""', node, 0, node, 1]);
+ insertDataTests.push([node, 0, '""', node, 0, node, 0]);
+ insertDataTests.push([node, 1, '""', node, 1, node, 1]);
+ insertDataTests.push([node, node + ".length", '""', node, node + ".length", node, node + ".length"]);
+ insertDataTests.push([node, 1, '""', node, 1, node, 3]);
+ insertDataTests.push([node, 2, '""', node, 1, node, 3]);
+ insertDataTests.push([node, 3, '""', node, 1, node, 3]);
+}
+
+insertDataTests.push(
+ ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0]", 0],
+ ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0]", 1],
+ ["paras[0].firstChild", 1, '"foo"', "paras[0]", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 2, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 3, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 2, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 3, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
+);
+
+
+function testAppendData(node, data, startContainer, startOffset, endContainer, endOffset) {
+ return testReplaceDataAlgorithm(node, node.length, 0, data,
+ function() { node.appendData(data) },
+ startContainer, startOffset, endContainer, endOffset);
+}
+
+var appendDataTests = [];
+for (var i = 0; i < characterDataNodes.length; i++) {
+ var node = characterDataNodes[i];
+ appendDataTests.push([node, '"foo"', node, 0, node, 1]);
+ appendDataTests.push([node, '"foo"', node, 0, node, 0]);
+ appendDataTests.push([node, '"foo"', node, 1, node, 1]);
+ appendDataTests.push([node, '"foo"', node, 0, node, node + ".length"]);
+ appendDataTests.push([node, '"foo"', node, 1, node, node + ".length"]);
+ appendDataTests.push([node, '"foo"', node, node + ".length", node, node + ".length"]);
+ appendDataTests.push([node, '"foo"', node, 1, node, 3]);
+
+ appendDataTests.push([node, '""', node, 0, node, 1]);
+ appendDataTests.push([node, '""', node, 0, node, 0]);
+ appendDataTests.push([node, '""', node, 1, node, 1]);
+ appendDataTests.push([node, '""', node, 0, node, node + ".length"]);
+ appendDataTests.push([node, '""', node, 1, node, node + ".length"]);
+ appendDataTests.push([node, '""', node, node + ".length", node, node + ".length"]);
+ appendDataTests.push([node, '""', node, 1, node, 3]);
+}
+
+appendDataTests.push(
+ ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0]", 0],
+ ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0]", 1],
+ ["paras[0].firstChild", '""', "paras[0]", 1, "paras[0]", 1],
+ ["paras[0].firstChild", '""', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0].firstChild", 3],
+
+ ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0]", 0],
+ ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0]", 1],
+ ["paras[0].firstChild", '"foo"', "paras[0]", 1, "paras[0]", 1],
+ ["paras[0].firstChild", '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
+);
+
+
+function testDeleteData(node, offset, count, startContainer, startOffset, endContainer, endOffset) {
+ return testReplaceDataAlgorithm(node, offset, count, "",
+ function() { node.deleteData(offset, count) },
+ startContainer, startOffset, endContainer, endOffset);
+}
+
+var deleteDataTests = [];
+for (var i = 0; i < characterDataNodes.length; i++) {
+ var node = characterDataNodes[i];
+ deleteDataTests.push([node, 376, 2, node, 0, node, 1]);
+ deleteDataTests.push([node, 0, 2, node, 0, node, 0]);
+ deleteDataTests.push([node, 1, 2, node, 1, node, 1]);
+ deleteDataTests.push([node, node + ".length", 2, node, node + ".length", node, node + ".length"]);
+ deleteDataTests.push([node, 1, 2, node, 1, node, 3]);
+ deleteDataTests.push([node, 2, 2, node, 1, node, 3]);
+ deleteDataTests.push([node, 3, 2, node, 1, node, 3]);
+
+ deleteDataTests.push([node, 376, 0, node, 0, node, 1]);
+ deleteDataTests.push([node, 0, 0, node, 0, node, 0]);
+ deleteDataTests.push([node, 1, 0, node, 1, node, 1]);
+ deleteDataTests.push([node, node + ".length", 0, node, node + ".length", node, node + ".length"]);
+ deleteDataTests.push([node, 1, 0, node, 1, node, 3]);
+ deleteDataTests.push([node, 2, 0, node, 1, node, 3]);
+ deleteDataTests.push([node, 3, 0, node, 1, node, 3]);
+
+ deleteDataTests.push([node, 376, 631, node, 0, node, 1]);
+ deleteDataTests.push([node, 0, 631, node, 0, node, 0]);
+ deleteDataTests.push([node, 1, 631, node, 1, node, 1]);
+ deleteDataTests.push([node, node + ".length", 631, node, node + ".length", node, node + ".length"]);
+ deleteDataTests.push([node, 1, 631, node, 1, node, 3]);
+ deleteDataTests.push([node, 2, 631, node, 1, node, 3]);
+ deleteDataTests.push([node, 3, 631, node, 1, node, 3]);
+}
+
+deleteDataTests.push(
+ ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0]", 0],
+ ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 2, "paras[0]", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 2, "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 2, 2, "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 3, 2, "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 2, 2, "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 3, 2, "paras[0]", 0, "paras[0].firstChild", 3]
+);
+
+
+function testReplaceData(node, offset, count, data, startContainer, startOffset, endContainer, endOffset) {
+ return testReplaceDataAlgorithm(node, offset, count, data,
+ function() { node.replaceData(offset, count, data) },
+ startContainer, startOffset, endContainer, endOffset);
+}
+
+var replaceDataTests = [];
+for (var i = 0; i < characterDataNodes.length; i++) {
+ var node = characterDataNodes[i];
+ replaceDataTests.push([node, 376, 0, '"foo"', node, 0, node, 1]);
+ replaceDataTests.push([node, 0, 0, '"foo"', node, 0, node, 0]);
+ replaceDataTests.push([node, 1, 0, '"foo"', node, 1, node, 1]);
+ replaceDataTests.push([node, node + ".length", 0, '"foo"', node, node + ".length", node, node + ".length"]);
+ replaceDataTests.push([node, 1, 0, '"foo"', node, 1, node, 3]);
+ replaceDataTests.push([node, 2, 0, '"foo"', node, 1, node, 3]);
+ replaceDataTests.push([node, 3, 0, '"foo"', node, 1, node, 3]);
+
+ replaceDataTests.push([node, 376, 0, '""', node, 0, node, 1]);
+ replaceDataTests.push([node, 0, 0, '""', node, 0, node, 0]);
+ replaceDataTests.push([node, 1, 0, '""', node, 1, node, 1]);
+ replaceDataTests.push([node, node + ".length", 0, '""', node, node + ".length", node, node + ".length"]);
+ replaceDataTests.push([node, 1, 0, '""', node, 1, node, 3]);
+ replaceDataTests.push([node, 2, 0, '""', node, 1, node, 3]);
+ replaceDataTests.push([node, 3, 0, '""', node, 1, node, 3]);
+
+ replaceDataTests.push([node, 376, 1, '"foo"', node, 0, node, 1]);
+ replaceDataTests.push([node, 0, 1, '"foo"', node, 0, node, 0]);
+ replaceDataTests.push([node, 1, 1, '"foo"', node, 1, node, 1]);
+ replaceDataTests.push([node, node + ".length", 1, '"foo"', node, node + ".length", node, node + ".length"]);
+ replaceDataTests.push([node, 1, 1, '"foo"', node, 1, node, 3]);
+ replaceDataTests.push([node, 2, 1, '"foo"', node, 1, node, 3]);
+ replaceDataTests.push([node, 3, 1, '"foo"', node, 1, node, 3]);
+
+ replaceDataTests.push([node, 376, 1, '""', node, 0, node, 1]);
+ replaceDataTests.push([node, 0, 1, '""', node, 0, node, 0]);
+ replaceDataTests.push([node, 1, 1, '""', node, 1, node, 1]);
+ replaceDataTests.push([node, node + ".length", 1, '""', node, node + ".length", node, node + ".length"]);
+ replaceDataTests.push([node, 1, 1, '""', node, 1, node, 3]);
+ replaceDataTests.push([node, 2, 1, '""', node, 1, node, 3]);
+ replaceDataTests.push([node, 3, 1, '""', node, 1, node, 3]);
+
+ replaceDataTests.push([node, 376, 47, '"foo"', node, 0, node, 1]);
+ replaceDataTests.push([node, 0, 47, '"foo"', node, 0, node, 0]);
+ replaceDataTests.push([node, 1, 47, '"foo"', node, 1, node, 1]);
+ replaceDataTests.push([node, node + ".length", 47, '"foo"', node, node + ".length", node, node + ".length"]);
+ replaceDataTests.push([node, 1, 47, '"foo"', node, 1, node, 3]);
+ replaceDataTests.push([node, 2, 47, '"foo"', node, 1, node, 3]);
+ replaceDataTests.push([node, 3, 47, '"foo"', node, 1, node, 3]);
+
+ replaceDataTests.push([node, 376, 47, '""', node, 0, node, 1]);
+ replaceDataTests.push([node, 0, 47, '""', node, 0, node, 0]);
+ replaceDataTests.push([node, 1, 47, '""', node, 1, node, 1]);
+ replaceDataTests.push([node, node + ".length", 47, '""', node, node + ".length", node, node + ".length"]);
+ replaceDataTests.push([node, 1, 47, '""', node, 1, node, 3]);
+ replaceDataTests.push([node, 2, 47, '""', node, 1, node, 3]);
+ replaceDataTests.push([node, 3, 47, '""', node, 1, node, 3]);
+}
+
+replaceDataTests.push(
+ ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0]", 0],
+ ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 2, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 3, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 2, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 3, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+
+ ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0]", 0],
+ ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 2, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 3, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 2, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 3, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+
+ ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0]", 0],
+ ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 2, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 3, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
+ ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 2, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
+ ["paras[0].firstChild", 3, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
+);
+
+
+// There are lots of ways to set data, so we pass a callback that does the
+// actual setting.
+function testDataChange(node, attr, op, rval, startContainer, startOffset, endContainer, endOffset) {
+ return testReplaceDataAlgorithm(node, 0, node.length, op == "=" ? rval : node[attr] + rval,
+ function() {
+ if (op == "=") {
+ node[attr] = rval;
+ } else if (op == "+=") {
+ node[attr] += rval;
+ } else {
+ throw "Unknown op " + op;
+ }
+ },
+ startContainer, startOffset, endContainer, endOffset);
+}
+
+var dataChangeTests = [];
+var dataChangeTestAttrs = ["data", "textContent", "nodeValue"];
+for (var i = 0; i < characterDataNodes.length; i++) {
+ var node = characterDataNodes[i];
+ var dataChangeTestRanges = [
+ [node, 0, node, 0],
+ [node, 0, node, 1],
+ [node, 1, node, 1],
+ [node, 0, node, node + ".length"],
+ [node, 1, node, node + ".length"],
+ [node, node + ".length", node, node + ".length"],
+ ];
+
+ for (var j = 0; j < dataChangeTestRanges.length; j++) {
+ for (var k = 0; k < dataChangeTestAttrs.length; k++) {
+ dataChangeTests.push([
+ node,
+ '"' + dataChangeTestAttrs[k] + '"',
+ '"="',
+ '""',
+ ].concat(dataChangeTestRanges[j]));
+
+ dataChangeTests.push([
+ node,
+ '"' + dataChangeTestAttrs[k] + '"',
+ '"="',
+ '"foo"',
+ ].concat(dataChangeTestRanges[j]));
+
+ dataChangeTests.push([
+ node,
+ '"' + dataChangeTestAttrs[k] + '"',
+ '"="',
+ node + "." + dataChangeTestAttrs[k],
+ ].concat(dataChangeTestRanges[j]));
+
+ dataChangeTests.push([
+ node,
+ '"' + dataChangeTestAttrs[k] + '"',
+ '"+="',
+ '""',
+ ].concat(dataChangeTestRanges[j]));
+
+ dataChangeTests.push([
+ node,
+ '"' + dataChangeTestAttrs[k] + '"',
+ '"+="',
+ '"foo"',
+ ].concat(dataChangeTestRanges[j]));
+
+ dataChangeTests.push([
+ node,
+ '"' + dataChangeTestAttrs[k] + '"',
+ '"+="',
+ node + "." + dataChangeTestAttrs[k]
+ ].concat(dataChangeTestRanges[j]));
+ }
+ }
+}
+
+
+// Now we test node insertions and deletions, as opposed to just data changes.
+// To avoid loads of repetition, we define modifyForRemove() and
+// modifyForInsert().
+
+// If we were to remove removedNode from its parent, what would the boundary
+// point [node, offset] become? Returns [new node, new offset]. Must be
+// called BEFORE the node is actually removed, so its parent is not null. (If
+// the parent is null, it will do nothing.)
+function modifyForRemove(removedNode, point) {
+ var oldParent = removedNode.parentNode;
+ var oldIndex = indexOf(removedNode);
+ if (!oldParent) {
+ return point;
+ }
+
+ // "For each boundary point whose node is removed node or a descendant of
+ // it, set the boundary point to (old parent, old index)."
+ if (point[0] == removedNode || isDescendant(point[0], removedNode)) {
+ return [oldParent, oldIndex];
+ }
+
+ // "For each boundary point whose node is old parent and whose offset is
+ // greater than old index, subtract one from its offset."
+ if (point[0] == oldParent && point[1] > oldIndex) {
+ return [point[0], point[1] - 1];
+ }
+
+ return point;
+}
+
+// Update the given boundary point [node, offset] to account for the fact that
+// insertedNode was just inserted into its current position. This must be
+// called AFTER insertedNode was already inserted.
+function modifyForInsert(insertedNode, point) {
+ // "For each boundary point whose node is the new parent of the affected
+ // node and whose offset is greater than the new index of the affected
+ // node, add one to the boundary point's offset."
+ if (point[0] == insertedNode.parentNode && point[1] > indexOf(insertedNode)) {
+ return [point[0], point[1] + 1];
+ }
+
+ return point;
+}
+
+
+function testInsertBefore(newParent, affectedNode, refNode, startContainer, startOffset, endContainer, endOffset) {
+ var expectedStart = [startContainer, startOffset];
+ var expectedEnd = [endContainer, endOffset];
+
+ expectedStart = modifyForRemove(affectedNode, expectedStart);
+ expectedEnd = modifyForRemove(affectedNode, expectedEnd);
+
+ try {
+ newParent.insertBefore(affectedNode, refNode);
+ } catch (e) {
+ // For our purposes, assume that DOM Core is true -- i.e., ignore
+ // mutation events and similar.
+ return [startContainer, startOffset, endContainer, endOffset];
+ }
+
+ expectedStart = modifyForInsert(affectedNode, expectedStart);
+ expectedEnd = modifyForInsert(affectedNode, expectedEnd);
+
+ return expectedStart.concat(expectedEnd);
+}
+
+var insertBeforeTests = [
+ // Moving a node to its current position
+ ["testDiv", "paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 0],
+ ["testDiv", "paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 1],
+ ["testDiv", "paras[0]", "paras[1]", "paras[0]", 1, "paras[0]", 1],
+ ["testDiv", "paras[0]", "paras[1]", "testDiv", 0, "testDiv", 2],
+ ["testDiv", "paras[0]", "paras[1]", "testDiv", 1, "testDiv", 1],
+ ["testDiv", "paras[0]", "paras[1]", "testDiv", 1, "testDiv", 2],
+ ["testDiv", "paras[0]", "paras[1]", "testDiv", 2, "testDiv", 2],
+
+ // Stuff that actually moves something. Note that paras[0] and paras[1]
+ // are both children of testDiv.
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 1],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 2],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 1],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 2],
+ ["paras[0]", "paras[1]", "null", "paras[0]", 0, "paras[0]", 0],
+ ["paras[0]", "paras[1]", "null", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "paras[1]", "null", "paras[0]", 1, "paras[0]", 1],
+ ["paras[0]", "paras[1]", "null", "testDiv", 0, "testDiv", 1],
+ ["paras[0]", "paras[1]", "null", "testDiv", 0, "testDiv", 2],
+ ["paras[0]", "paras[1]", "null", "testDiv", 1, "testDiv", 1],
+ ["paras[0]", "paras[1]", "null", "testDiv", 1, "testDiv", 2],
+ ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 0],
+ ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 1],
+ ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 2],
+ ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 1, "foreignDoc", 1],
+ ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 0],
+ ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 1],
+ ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 2],
+ ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 1, "foreignDoc", 1],
+ ["foreignDoc", "detachedComment", "null", "foreignDoc", 0, "foreignDoc", 1],
+ ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
+ ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
+
+ // Stuff that throws exceptions
+ ["paras[0]", "paras[0]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "testDiv", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "document", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "foreignDoc", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "document.doctype", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+];
+
+
+function testReplaceChild(newParent, newChild, oldChild, startContainer, startOffset, endContainer, endOffset) {
+ var expectedStart = [startContainer, startOffset];
+ var expectedEnd = [endContainer, endOffset];
+
+ expectedStart = modifyForRemove(oldChild, expectedStart);
+ expectedEnd = modifyForRemove(oldChild, expectedEnd);
+
+ if (newChild != oldChild) {
+ // Don't do this twice, if they're the same!
+ expectedStart = modifyForRemove(newChild, expectedStart);
+ expectedEnd = modifyForRemove(newChild, expectedEnd);
+ }
+
+ try {
+ newParent.replaceChild(newChild, oldChild);
+ } catch (e) {
+ return [startContainer, startOffset, endContainer, endOffset];
+ }
+
+ expectedStart = modifyForInsert(newChild, expectedStart);
+ expectedEnd = modifyForInsert(newChild, expectedEnd);
+
+ return expectedStart.concat(expectedEnd);
+}
+
+var replaceChildTests = [
+ // Moving a node to its current position. Doesn't match most browsers'
+ // behavior, but we probably want to keep the spec the same anyway:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=647603
+ ["testDiv", "paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 0],
+ ["testDiv", "paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 1],
+ ["testDiv", "paras[0]", "paras[0]", "paras[0]", 1, "paras[0]", 1],
+ ["testDiv", "paras[0]", "paras[0]", "testDiv", 0, "testDiv", 2],
+ ["testDiv", "paras[0]", "paras[0]", "testDiv", 1, "testDiv", 1],
+ ["testDiv", "paras[0]", "paras[0]", "testDiv", 1, "testDiv", 2],
+ ["testDiv", "paras[0]", "paras[0]", "testDiv", 2, "testDiv", 2],
+
+ // Stuff that actually moves something.
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 1],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 2],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 1],
+ ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 2],
+ ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 0],
+ ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 1],
+ ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 2],
+ ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 1, "foreignDoc", 1],
+ ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 0],
+ ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 1],
+ ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 2],
+ ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 1, "foreignDoc", 1],
+ ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
+ ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
+
+ // Stuff that throws exceptions
+ ["paras[0]", "paras[0]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "testDiv", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "document", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "foreignDoc", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "document.doctype", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
+];
+
+
+function testAppendChild(newParent, affectedNode, startContainer, startOffset, endContainer, endOffset) {
+ var expectedStart = [startContainer, startOffset];
+ var expectedEnd = [endContainer, endOffset];
+
+ expectedStart = modifyForRemove(affectedNode, expectedStart);
+ expectedEnd = modifyForRemove(affectedNode, expectedEnd);
+
+ try {
+ newParent.appendChild(affectedNode);
+ } catch (e) {
+ return [startContainer, startOffset, endContainer, endOffset];
+ }
+
+ // These two lines will actually never do anything, if you think about it,
+ // but let's leave them in so correctness is more obvious.
+ expectedStart = modifyForInsert(affectedNode, expectedStart);
+ expectedEnd = modifyForInsert(affectedNode, expectedEnd);
+
+ return expectedStart.concat(expectedEnd);
+}
+
+var appendChildTests = [
+ // Moving a node to its current position
+ ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 0, "testDiv.lastChild", 0],
+ ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 0, "testDiv.lastChild", 1],
+ ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 1, "testDiv.lastChild", 1],
+ ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 2", "testDiv", "testDiv.childNodes.length"],
+ ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 2", "testDiv", "testDiv.childNodes.length - 1"],
+ ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 1", "testDiv", "testDiv.childNodes.length"],
+ ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 1", "testDiv", "testDiv.childNodes.length - 1"],
+ ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length", "testDiv", "testDiv.childNodes.length"],
+ ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 0, "detachedDiv.lastChild", 0],
+ ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 0, "detachedDiv.lastChild", 1],
+ ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 1, "detachedDiv.lastChild", 1],
+ ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 2", "detachedDiv", "detachedDiv.childNodes.length"],
+ ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 2", "detachedDiv", "detachedDiv.childNodes.length - 1"],
+ ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 1", "detachedDiv", "detachedDiv.childNodes.length"],
+ ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 1", "detachedDiv", "detachedDiv.childNodes.length - 1"],
+ ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length", "detachedDiv", "detachedDiv.childNodes.length"],
+
+ // Stuff that actually moves something
+ ["paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 0],
+ ["paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "paras[1]", "paras[0]", 1, "paras[0]", 1],
+ ["paras[0]", "paras[1]", "testDiv", 0, "testDiv", 1],
+ ["paras[0]", "paras[1]", "testDiv", 0, "testDiv", 2],
+ ["paras[0]", "paras[1]", "testDiv", 1, "testDiv", 1],
+ ["paras[0]", "paras[1]", "testDiv", 1, "testDiv", 2],
+ ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length - 1", "foreignDoc", "foreignDoc.childNodes.length"],
+ ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length - 1", "foreignDoc", "foreignDoc.childNodes.length - 1"],
+ ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length", "foreignDoc", "foreignDoc.childNodes.length"],
+ ["foreignDoc", "detachedComment", "detachedComment", 0, "detachedComment", 5],
+ ["paras[0]", "xmlTextNode", "paras[0]", 0, "paras[0]", 0],
+ ["paras[0]", "xmlTextNode", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "xmlTextNode", "paras[0]", 1, "paras[0]", 1],
+
+ // Stuff that throws exceptions
+ ["paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "testDiv", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "document", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "foreignDoc", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "document.doctype", "paras[0]", 0, "paras[0]", 1],
+];
+
+
+function testRemoveChild(affectedNode, startContainer, startOffset, endContainer, endOffset) {
+ var expectedStart = [startContainer, startOffset];
+ var expectedEnd = [endContainer, endOffset];
+
+ expectedStart = modifyForRemove(affectedNode, expectedStart);
+ expectedEnd = modifyForRemove(affectedNode, expectedEnd);
+
+ // We don't test cases where the parent is wrong, so this should never
+ // throw an exception.
+ affectedNode.parentNode.removeChild(affectedNode);
+
+ return expectedStart.concat(expectedEnd);
+}
+
+var removeChildTests = [
+ ["paras[0]", "paras[0]", 0, "paras[0]", 0],
+ ["paras[0]", "paras[0]", 0, "paras[0]", 1],
+ ["paras[0]", "paras[0]", 1, "paras[0]", 1],
+ ["paras[0]", "testDiv", 0, "testDiv", 0],
+ ["paras[0]", "testDiv", 0, "testDiv", 1],
+ ["paras[0]", "testDiv", 1, "testDiv", 1],
+ ["paras[0]", "testDiv", 0, "testDiv", 2],
+ ["paras[0]", "testDiv", 1, "testDiv", 2],
+ ["paras[0]", "testDiv", 2, "testDiv", 2],
+
+ ["foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", "foreignDoc.childNodes.length"],
+];
diff --git a/testing/web-platform/tests/dom/ranges/Range-selectNode.html b/testing/web-platform/tests/dom/ranges/Range-selectNode.html
new file mode 100644
index 0000000000..fe9b1f7860
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-selectNode.html
@@ -0,0 +1,99 @@
+<!doctype html>
+<title>Range.selectNode() and .selectNodeContents() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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";
+
+function testSelectNode(range, node) {
+ try {
+ range.collapsed;
+ } catch (e) {
+ // Range is detached
+ assert_throws_dom("INVALID_STATE_ERR", function () {
+ range.selectNode(node);
+ }, "selectNode() on a detached node must throw INVALID_STATE_ERR");
+ assert_throws_dom("INVALID_STATE_ERR", function () {
+ range.selectNodeContents(node);
+ }, "selectNodeContents() on a detached node must throw INVALID_STATE_ERR");
+ return;
+ }
+
+ if (!node.parentNode) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function() {
+ range.selectNode(node);
+ }, "selectNode() on a node with no parent must throw INVALID_NODE_TYPE_ERR");
+ } else {
+ var index = 0;
+ while (node.parentNode.childNodes[index] != node) {
+ index++;
+ }
+
+ range.selectNode(node);
+ assert_equals(range.startContainer, node.parentNode,
+ "After selectNode(), startContainer must equal parent node");
+ assert_equals(range.endContainer, node.parentNode,
+ "After selectNode(), endContainer must equal parent node");
+ assert_equals(range.startOffset, index,
+ "After selectNode(), startOffset must be index of node in parent (" + index + ")");
+ assert_equals(range.endOffset, index + 1,
+ "After selectNode(), endOffset must be one plus index of node in parent (" + (index + 1) + ")");
+ }
+
+ if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function () {
+ range.selectNodeContents(node);
+ }, "selectNodeContents() on a doctype must throw INVALID_NODE_TYPE_ERR");
+ } else {
+ range.selectNodeContents(node);
+ assert_equals(range.startContainer, node,
+ "After selectNodeContents(), startContainer must equal node");
+ assert_equals(range.endContainer, node,
+ "After selectNodeContents(), endContainer must equal node");
+ assert_equals(range.startOffset, 0,
+ "After selectNodeContents(), startOffset must equal 0");
+ var len = nodeLength(node);
+ assert_equals(range.endOffset, len,
+ "After selectNodeContents(), endOffset must equal node length (" + len + ")");
+ }
+}
+
+var range = document.createRange();
+var foreignRange = foreignDoc.createRange();
+var xmlRange = xmlDoc.createRange();
+var detachedRange = document.createRange();
+detachedRange.detach();
+var tests = [];
+function testTree(root, marker) {
+ if (root.nodeType == Node.ELEMENT_NODE && root.id == "log") {
+ // This is being modified during the tests, so let's not test it.
+ return;
+ }
+ tests.push([marker + ": " + root.nodeName.toLowerCase() + " node, current doc's range, type " + root.nodeType, range, root]);
+ tests.push([marker + ": " + root.nodeName.toLowerCase() + " node, foreign doc's range, type " + root.nodeType, foreignRange, root]);
+ tests.push([marker + ": " + root.nodeName.toLowerCase() + " node, XML doc's range, type " + root.nodeType, xmlRange, root]);
+ tests.push([marker + ": " + root.nodeName.toLowerCase() + " node, detached range, type " + root.nodeType, detachedRange, root]);
+ for (var i = 0; i < root.childNodes.length; i++) {
+ testTree(root.childNodes[i], marker + "[" + i + "]");
+ }
+}
+testTree(document, "current doc");
+testTree(foreignDoc, "foreign doc");
+testTree(detachedDiv, "detached div in current doc");
+
+var otherTests = ["xmlDoc", "xmlElement", "detachedTextNode",
+"foreignTextNode", "xmlTextNode", "processingInstruction", "comment",
+"foreignComment", "xmlComment", "docfrag", "foreignDocfrag", "xmlDocfrag"];
+
+for (var i = 0; i < otherTests.length; i++) {
+ testTree(window[otherTests[i]], otherTests[i]);
+}
+
+generate_tests(testSelectNode, tests);
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-set.html b/testing/web-platform/tests/dom/ranges/Range-set.html
new file mode 100644
index 0000000000..694fc60749
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-set.html
@@ -0,0 +1,221 @@
+<!doctype html>
+<title>Range setting tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<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";
+
+function testSetStart(range, node, offset) {
+ if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function() {
+ range.setStart(node, offset);
+ }, "setStart() to a doctype must throw INVALID_NODE_TYPE_ERR");
+ return;
+ }
+
+ if (offset < 0 || offset > nodeLength(node)) {
+ assert_throws_dom("INDEX_SIZE_ERR", function() {
+ range.setStart(node, offset);
+ }, "setStart() to a too-large offset must throw INDEX_SIZE_ERR");
+ return;
+ }
+
+ var newRange = range.cloneRange();
+ newRange.setStart(node, offset);
+
+ assert_equals(newRange.startContainer, node,
+ "setStart() must change startContainer to the new node");
+ assert_equals(newRange.startOffset, offset,
+ "setStart() must change startOffset to the new offset");
+
+ // FIXME: I'm assuming comparePoint() is correct, but the tests for that
+ // will depend on setStart()/setEnd().
+ if (furthestAncestor(node) != furthestAncestor(range.startContainer)
+ || range.comparePoint(node, offset) > 0) {
+ assert_equals(newRange.endContainer, node,
+ "setStart(node, offset) where node is after current end or in different document must set the end node to node too");
+ assert_equals(newRange.endOffset, offset,
+ "setStart(node, offset) where node is after current end or in different document must set the end offset to offset too");
+ } else {
+ assert_equals(newRange.endContainer, range.endContainer,
+ "setStart() must not change the end node if the new start is before the old end");
+ assert_equals(newRange.endOffset, range.endOffset,
+ "setStart() must not change the end offset if the new start is before the old end");
+ }
+}
+
+function testSetEnd(range, node, offset) {
+ if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function() {
+ range.setEnd(node, offset);
+ }, "setEnd() to a doctype must throw INVALID_NODE_TYPE_ERR");
+ return;
+ }
+
+ if (offset < 0 || offset > nodeLength(node)) {
+ assert_throws_dom("INDEX_SIZE_ERR", function() {
+ range.setEnd(node, offset);
+ }, "setEnd() to a too-large offset must throw INDEX_SIZE_ERR");
+ return;
+ }
+
+ var newRange = range.cloneRange();
+ newRange.setEnd(node, offset);
+
+ // FIXME: I'm assuming comparePoint() is correct, but the tests for that
+ // will depend on setStart()/setEnd().
+ if (furthestAncestor(node) != furthestAncestor(range.startContainer)
+ || range.comparePoint(node, offset) < 0) {
+ assert_equals(newRange.startContainer, node,
+ "setEnd(node, offset) where node is before current start or in different document must set the end node to node too");
+ assert_equals(newRange.startOffset, offset,
+ "setEnd(node, offset) where node is before current start or in different document must set the end offset to offset too");
+ } else {
+ assert_equals(newRange.startContainer, range.startContainer,
+ "setEnd() must not change the start node if the new end is after the old start");
+ assert_equals(newRange.startOffset, range.startOffset,
+ "setEnd() must not change the start offset if the new end is after the old start");
+ }
+
+ assert_equals(newRange.endContainer, node,
+ "setEnd() must change endContainer to the new node");
+ assert_equals(newRange.endOffset, offset,
+ "setEnd() must change endOffset to the new offset");
+}
+
+function testSetStartBefore(range, node) {
+ var parent = node.parentNode;
+ if (parent === null) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function () {
+ range.setStartBefore(node);
+ }, "setStartBefore() to a node with null parent must throw INVALID_NODE_TYPE_ERR");
+ return;
+ }
+
+ var idx = 0;
+ while (node.parentNode.childNodes[idx] != node) {
+ idx++;
+ }
+
+ testSetStart(range, node.parentNode, idx);
+}
+
+function testSetStartAfter(range, node) {
+ var parent = node.parentNode;
+ if (parent === null) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function () {
+ range.setStartAfter(node);
+ }, "setStartAfter() to a node with null parent must throw INVALID_NODE_TYPE_ERR");
+ return;
+ }
+
+ var idx = 0;
+ while (node.parentNode.childNodes[idx] != node) {
+ idx++;
+ }
+
+ testSetStart(range, node.parentNode, idx + 1);
+}
+
+function testSetEndBefore(range, node) {
+ var parent = node.parentNode;
+ if (parent === null) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function () {
+ range.setEndBefore(node);
+ }, "setEndBefore() to a node with null parent must throw INVALID_NODE_TYPE_ERR");
+ return;
+ }
+
+ var idx = 0;
+ while (node.parentNode.childNodes[idx] != node) {
+ idx++;
+ }
+
+ testSetEnd(range, node.parentNode, idx);
+}
+
+function testSetEndAfter(range, node) {
+ var parent = node.parentNode;
+ if (parent === null) {
+ assert_throws_dom("INVALID_NODE_TYPE_ERR", function () {
+ range.setEndAfter(node);
+ }, "setEndAfter() to a node with null parent must throw INVALID_NODE_TYPE_ERR");
+ return;
+ }
+
+ var idx = 0;
+ while (node.parentNode.childNodes[idx] != node) {
+ idx++;
+ }
+
+ testSetEnd(range, node.parentNode, idx + 1);
+}
+
+
+var startTests = [];
+var endTests = [];
+var startBeforeTests = [];
+var startAfterTests = [];
+var endBeforeTests = [];
+var endAfterTests = [];
+
+// Don't want to eval() each point a bazillion times
+var testPointsCached = testPoints.map(eval);
+var testNodesCached = testNodesShort.map(eval);
+
+for (var i = 0; i < testRangesShort.length; i++) {
+ var endpoints = eval(testRangesShort[i]);
+ var range;
+ test(function() {
+ range = ownerDocument(endpoints[0]).createRange();
+ range.setStart(endpoints[0], endpoints[1]);
+ range.setEnd(endpoints[2], endpoints[3]);
+ }, "Set up range " + i + " " + testRangesShort[i]);
+
+ for (var j = 0; j < testPoints.length; j++) {
+ startTests.push(["setStart() with range " + i + " " + testRangesShort[i] + ", point " + j + " " + testPoints[j],
+ range,
+ testPointsCached[j][0],
+ testPointsCached[j][1]
+ ]);
+ endTests.push(["setEnd() with range " + i + " " + testRangesShort[i] + ", point " + j + " " + testPoints[j],
+ range,
+ testPointsCached[j][0],
+ testPointsCached[j][1]
+ ]);
+ }
+
+ for (var j = 0; j < testNodesShort.length; j++) {
+ startBeforeTests.push(["setStartBefore() with range " + i + " " + testRangesShort[i] + ", node " + j + " " + testNodesShort[j],
+ range,
+ testNodesCached[j]
+ ]);
+ startAfterTests.push(["setStartAfter() with range " + i + " " + testRangesShort[i] + ", node " + j + " " + testNodesShort[j],
+ range,
+ testNodesCached[j]
+ ]);
+ endBeforeTests.push(["setEndBefore() with range " + i + " " + testRangesShort[i] + ", node " + j + " " + testNodesShort[j],
+ range,
+ testNodesCached[j]
+ ]);
+ endAfterTests.push(["setEndAfter() with range " + i + " " + testRangesShort[i] + ", node " + j + " " + testNodesShort[j],
+ range,
+ testNodesCached[j]
+ ]);
+ }
+}
+
+generate_tests(testSetStart, startTests);
+generate_tests(testSetEnd, endTests);
+generate_tests(testSetStartBefore, startBeforeTests);
+generate_tests(testSetStartAfter, startAfterTests);
+generate_tests(testSetEndBefore, endBeforeTests);
+generate_tests(testSetEndAfter, endAfterTests);
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-stringifier.html b/testing/web-platform/tests/dom/ranges/Range-stringifier.html
new file mode 100644
index 0000000000..330c7421ea
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-stringifier.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Range stringifier</title>
+<link rel="author" title="KiChjang" href="mailto:kungfukeith11@gmail.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=test>Test div</div>
+<div id=another>Another div</div>
+<div id=last>Last div</div>
+<div id=log></div>
+<script>
+test(function() {
+ var r = new Range();
+ var testDiv = document.getElementById("test");
+ test(function() {
+ r.selectNodeContents(testDiv);
+ assert_equals(r.collapsed, false);
+ assert_equals(r.toString(), testDiv.textContent);
+ }, "Node contents of a single div");
+
+ var textNode = testDiv.childNodes[0];
+ test(function() {
+ r.setStart(textNode, 5);
+ r.setEnd(textNode, 7);
+ assert_equals(r.collapsed, false);
+ assert_equals(r.toString(), "di");
+ }, "Text node with offsets");
+
+ var anotherDiv = document.getElementById("another");
+ test(function() {
+ r.setStart(testDiv, 0);
+ r.setEnd(anotherDiv, 0);
+ assert_equals(r.toString(), "Test div\n");
+ }, "Two nodes, each with a text node");
+
+ var lastDiv = document.getElementById("last");
+ var lastText = lastDiv.childNodes[0];
+ test(function() {
+ r.setStart(textNode, 5);
+ r.setEnd(lastText, 4);
+ assert_equals(r.toString(), "div\nAnother div\nLast");
+ }, "Three nodes with start offset and end offset on text nodes");
+});
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-surroundContents.html b/testing/web-platform/tests/dom/ranges/Range-surroundContents.html
new file mode 100644
index 0000000000..c9d47fcbad
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-surroundContents.html
@@ -0,0 +1,324 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Range.surroundContents() tests</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<meta name=timeout content=long>
+<p>To debug test failures, add a query parameter "subtest" with the test id (like
+"?subtest=5,16"). 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";
+
+testDiv.parentNode.removeChild(testDiv);
+
+function mySurroundContents(range, newParent) {
+ try {
+ // "If a non-Text node is partially contained in the context object,
+ // throw a "InvalidStateError" exception and terminate these steps."
+ var node = range.commonAncestorContainer;
+ var stop = nextNodeDescendants(node);
+ for (; node != stop; node = nextNode(node)) {
+ if (node.nodeType != Node.TEXT_NODE
+ && isPartiallyContained(node, range)) {
+ return "INVALID_STATE_ERR";
+ }
+ }
+
+ // "If newParent is a Document, DocumentType, or DocumentFragment node,
+ // throw an "InvalidNodeTypeError" exception and terminate these
+ // steps."
+ if (newParent.nodeType == Node.DOCUMENT_NODE
+ || newParent.nodeType == Node.DOCUMENT_TYPE_NODE
+ || newParent.nodeType == Node.DOCUMENT_FRAGMENT_NODE) {
+ return "INVALID_NODE_TYPE_ERR";
+ }
+
+ // "Call extractContents() on the context object, and let fragment be
+ // the result."
+ var fragment = myExtractContents(range);
+ if (typeof fragment == "string") {
+ return fragment;
+ }
+
+ // "While newParent has children, remove its first child."
+ while (newParent.childNodes.length) {
+ newParent.removeChild(newParent.firstChild);
+ }
+
+ // "Call insertNode(newParent) on the context object."
+ var ret = myInsertNode(range, newParent);
+ if (typeof ret == "string") {
+ return ret;
+ }
+
+ // "Call appendChild(fragment) on newParent."
+ newParent.appendChild(fragment);
+
+ // "Call selectNode(newParent) on the context object."
+ //
+ // We just reimplement this in-place.
+ if (!newParent.parentNode) {
+ return "INVALID_NODE_TYPE_ERR";
+ }
+ var index = indexOf(newParent);
+ range.setStart(newParent.parentNode, index);
+ range.setEnd(newParent.parentNode, index + 1);
+ } catch (e) {
+ return getDomExceptionName(e);
+ }
+}
+
+function restoreIframe(iframe, i, j) {
+ // Most of this function is designed to work around the fact that Opera
+ // doesn't let you add a doctype to a document that no longer has one, in
+ // any way I can figure out. I eventually compromised on something that
+ // will still let Opera pass most tests that don't actually involve
+ // doctypes.
+ while (iframe.contentDocument.firstChild
+ && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.firstChild);
+ }
+
+ while (iframe.contentDocument.lastChild
+ && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) {
+ iframe.contentDocument.removeChild(iframe.contentDocument.lastChild);
+ }
+
+ if (!iframe.contentDocument.firstChild) {
+ // This will throw an exception in Opera if we reach here, which is why
+ // I try to avoid it. It will never happen in a browser that obeys the
+ // spec, so it's really just insurance. I don't think it actually gets
+ // hit by anything.
+ iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", ""));
+ }
+ iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true));
+ iframe.contentWindow.setupRangeTests();
+ iframe.contentWindow.testRangeInput = testRangesShort[i];
+ iframe.contentWindow.testNodeInput = testNodesShort[j];
+ iframe.contentWindow.run();
+}
+
+function testSurroundContents(i, j) {
+ var actualRange;
+ var expectedRange;
+ var actualNode;
+ var expectedNode;
+ var actualRoots = [];
+ var expectedRoots = [];
+
+ domTests[i][j].step(function() {
+ restoreIframe(actualIframe, i, j);
+ restoreIframe(expectedIframe, i, j);
+
+ actualRange = actualIframe.contentWindow.testRange;
+ expectedRange = expectedIframe.contentWindow.testRange;
+ actualNode = actualIframe.contentWindow.testNode;
+ expectedNode = expectedIframe.contentWindow.testNode;
+
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual surroundContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated surroundContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_not_equals(actualRange, null,
+ "Range produced in actual iframe was null");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+ assert_not_equals(expectedRange, null,
+ "Range produced in expected iframe was null");
+ assert_equals(typeof actualNode, "object",
+ "typeof Node produced in actual iframe");
+ assert_not_equals(actualNode, null,
+ "Node produced in actual iframe was null");
+ assert_equals(typeof expectedNode, "object",
+ "typeof Node produced in expected iframe");
+ assert_not_equals(expectedNode, null,
+ "Node produced in expected iframe was null");
+
+ // We want to test that the trees containing the ranges are equal, and
+ // also the trees containing the moved nodes. These might not be the
+ // same, if we're inserting a node from a detached tree or a different
+ // document.
+ actualRoots.push(furthestAncestor(actualRange.startContainer));
+ expectedRoots.push(furthestAncestor(expectedRange.startContainer));
+
+ if (furthestAncestor(actualNode) != actualRoots[0]) {
+ actualRoots.push(furthestAncestor(actualNode));
+ }
+ if (furthestAncestor(expectedNode) != expectedRoots[0]) {
+ expectedRoots.push(furthestAncestor(expectedNode));
+ }
+
+ assert_equals(actualRoots.length, expectedRoots.length,
+ "Either the actual node and actual range are in the same tree but the expected are in different trees, or vice versa");
+
+ // This doctype stuff is to work around the fact that Opera 11.00 will
+ // move around doctypes within a document, even to totally invalid
+ // positions, but it won't allow a new doctype to be added to a
+ // document in any way I can figure out. So if we try moving a doctype
+ // to some invalid place, in Opera it will actually succeed, and then
+ // restoreIframe() will remove the doctype along with the root element,
+ // and then nothing can re-add the doctype. So instead, we catch it
+ // during the test itself and move it back to the right place while we
+ // still can.
+ //
+ // I spent *way* too much time debugging and working around this bug.
+ var actualDoctype = actualIframe.contentDocument.doctype;
+ var expectedDoctype = expectedIframe.contentDocument.doctype;
+
+ var result;
+ try {
+ result = mySurroundContents(expectedRange, expectedNode);
+ } catch (e) {
+ if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
+ expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
+ }
+ throw e;
+ }
+ if (typeof result == "string") {
+ assert_throws_dom(result, actualIframe.contentWindow.DOMException, function() {
+ try {
+ actualRange.surroundContents(actualNode);
+ } catch (e) {
+ if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
+ expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
+ }
+ if (actualDoctype != actualIframe.contentDocument.firstChild) {
+ actualIframe.contentDocument.insertBefore(actualDoctype, actualIframe.contentDocument.firstChild);
+ }
+ throw e;
+ }
+ }, "A " + result + " must be thrown in this case");
+ // Don't return, we still need to test DOM equality
+ } else {
+ try {
+ actualRange.surroundContents(actualNode);
+ } catch (e) {
+ if (expectedDoctype != expectedIframe.contentDocument.firstChild) {
+ expectedIframe.contentDocument.insertBefore(expectedDoctype, expectedIframe.contentDocument.firstChild);
+ }
+ if (actualDoctype != actualIframe.contentDocument.firstChild) {
+ actualIframe.contentDocument.insertBefore(actualDoctype, actualIframe.contentDocument.firstChild);
+ }
+ throw e;
+ }
+ }
+
+ for (var k = 0; k < actualRoots.length; k++) {
+ assertNodesEqual(actualRoots[k], expectedRoots[k], k ? "moved node's tree root" : "range's tree root");
+ }
+ });
+ domTests[i][j].done();
+
+ positionTests[i][j].step(function() {
+ assert_equals(actualIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for actual surroundContents()");
+ assert_equals(expectedIframe.contentWindow.unexpectedException, null,
+ "Unexpected exception thrown when setting up Range for simulated surroundContents()");
+ assert_equals(typeof actualRange, "object",
+ "typeof Range produced in actual iframe");
+ assert_not_equals(actualRange, null,
+ "Range produced in actual iframe was null");
+ assert_equals(typeof expectedRange, "object",
+ "typeof Range produced in expected iframe");
+ assert_not_equals(expectedRange, null,
+ "Range produced in expected iframe was null");
+ assert_equals(typeof actualNode, "object",
+ "typeof Node produced in actual iframe");
+ assert_not_equals(actualNode, null,
+ "Node produced in actual iframe was null");
+ assert_equals(typeof expectedNode, "object",
+ "typeof Node produced in expected iframe");
+ assert_not_equals(expectedNode, null,
+ "Node produced in expected iframe was null");
+
+ for (var k = 0; k < actualRoots.length; k++) {
+ assertNodesEqual(actualRoots[k], expectedRoots[k], k ? "moved node's tree root" : "range's tree root");
+ }
+
+ assert_equals(actualRange.startOffset, expectedRange.startOffset,
+ "Unexpected startOffset after surroundContents()");
+ assert_equals(actualRange.endOffset, expectedRange.endOffset,
+ "Unexpected endOffset after surroundContents()");
+ // How do we decide that the two nodes are equal, since they're in
+ // different trees? Since the DOMs are the same, it's enough to check
+ // that the index in the parent is the same all the way up the tree.
+ // But we can first cheat by just checking they're actually equal.
+ assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer),
+ "Unexpected startContainer after surroundContents(), expected " +
+ expectedRange.startContainer.nodeName.toLowerCase() + " but got " +
+ actualRange.startContainer.nodeName.toLowerCase());
+ var currentActual = actualRange.startContainer;
+ var currentExpected = expectedRange.startContainer;
+ var actual = "";
+ var expected = "";
+ while (currentActual && currentExpected) {
+ actual = indexOf(currentActual) + "-" + actual;
+ expected = indexOf(currentExpected) + "-" + expected;
+
+ currentActual = currentActual.parentNode;
+ currentExpected = currentExpected.parentNode;
+ }
+ actual = actual.substr(0, actual.length - 1);
+ expected = expected.substr(0, expected.length - 1);
+ assert_equals(actual, expected,
+ "startContainer superficially looks right but is actually the wrong node if you trace back its index in all its ancestors (I'm surprised this actually happened");
+ });
+ positionTests[i][j].done();
+}
+
+var iStart = 0;
+var iStop = testRangesShort.length;
+var jStart = 0;
+var jStop = testNodesShort.length;
+
+if (/subtest=[0-9]+,[0-9]+/.test(location.search)) {
+ var matches = /subtest=([0-9]+),([0-9]+)/.exec(location.search);
+ iStart = Number(matches[1]);
+ iStop = Number(matches[1]) + 1;
+ jStart = Number(matches[2]) + 0;
+ jStop = Number(matches[2]) + 1;
+}
+
+var domTests = [];
+var positionTests = [];
+for (var i = iStart; i < iStop; i++) {
+ domTests[i] = [];
+ positionTests[i] = [];
+ for (var j = jStart; j < jStop; j++) {
+ domTests[i][j] = async_test(i + "," + j + ": resulting DOM for range " + testRangesShort[i] + ", node " + testNodesShort[j]);
+ positionTests[i][j] = async_test(i + "," + j + ": resulting range position for range " + testRangesShort[i] + ", node " + testNodesShort[j]);
+ }
+}
+
+var actualIframe = document.createElement("iframe");
+actualIframe.style.display = "none";
+actualIframe.id = "actual";
+document.body.appendChild(actualIframe);
+
+var expectedIframe = document.createElement("iframe");
+expectedIframe.style.display = "none";
+expectedIframe.id = "expected";
+document.body.appendChild(expectedIframe);
+
+var referenceDoc = document.implementation.createHTMLDocument("");
+referenceDoc.removeChild(referenceDoc.documentElement);
+
+actualIframe.onload = function() {
+ expectedIframe.onload = function() {
+ for (var i = iStart; i < iStop; i++) {
+ for (var j = jStart; j < jStop; j++) {
+ testSurroundContents(i, j);
+ }
+ }
+ }
+ expectedIframe.src = "Range-test-iframe.html";
+ referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true));
+}
+actualIframe.src = "Range-test-iframe.html";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/Range-test-iframe.html b/testing/web-platform/tests/dom/ranges/Range-test-iframe.html
new file mode 100644
index 0000000000..f354ff758f
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/Range-test-iframe.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<title>Range test iframe</title>
+<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name>
+<meta name=timeout content=long>
+<body onload=run()>
+<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 the range and does nothing else. The data is returned via
+// window.testRange, and if an exception is thrown, it's put in
+// window.unexpectedException.
+window.unexpectedException = null;
+
+function run() {
+ try {
+ window.unexpectedException = null;
+
+ if (typeof window.testNodeInput != "undefined") {
+ window.testNode = eval(window.testNodeInput);
+ }
+
+ var rangeEndpoints;
+ if (typeof window.testRangeInput == "undefined") {
+ // Use the hash (old way of doing things, bad because it requires
+ // navigation)
+ if (location.hash == "") {
+ return;
+ }
+ rangeEndpoints = eval(location.hash.substr(1));
+ } else {
+ // Get the variable directly off the window, faster and can be done
+ // synchronously
+ rangeEndpoints = eval(window.testRangeInput);
+ }
+
+ var range;
+ if (rangeEndpoints == "detached") {
+ range = document.createRange();
+ range.detach();
+ } else {
+ range = ownerDocument(rangeEndpoints[0]).createRange();
+ range.setStart(rangeEndpoints[0], rangeEndpoints[1]);
+ range.setEnd(rangeEndpoints[2], rangeEndpoints[3]);
+ }
+
+ window.testRange = range;
+ } catch(e) {
+ window.unexpectedException = e;
+ }
+}
+
+testDiv.style.display = "none";
+</script>
diff --git a/testing/web-platform/tests/dom/ranges/StaticRange-constructor.html b/testing/web-platform/tests/dom/ranges/StaticRange-constructor.html
new file mode 100644
index 0000000000..6aae93f49b
--- /dev/null
+++ b/testing/web-platform/tests/dom/ranges/StaticRange-constructor.html
@@ -0,0 +1,200 @@
+<!doctype html>
+<title>StaticRange constructor test</title>
+<link rel='author' title='Sanket Joshi' href='mailto:sajos@microsoft.com'>
+<div id='log'></div>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+<div id='testDiv'>abc<span>def</span>ghi</div>
+<script>
+'use strict';
+
+const testDiv = document.getElementById('testDiv');
+const testTextNode = testDiv.firstChild;
+const testPINode = document.createProcessingInstruction('foo', 'abc');
+const testCommentNode = document.createComment('abc');
+document.body.append(testPINode, testCommentNode);
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 1, endContainer: testDiv, endOffset: 2});
+ assert_equals(staticRange.startContainer, testDiv, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 1, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testDiv, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 2, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with Element container');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testTextNode, startOffset: 1, endContainer: testTextNode, endOffset: 2});
+ assert_equals(staticRange.startContainer, testTextNode, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 1, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testTextNode, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 2, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with Text container');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 0, endContainer: testTextNode, endOffset: 1});
+ assert_equals(staticRange.startContainer, testDiv, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 0, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testTextNode, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 1, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with Element startContainer and Text endContainer');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testTextNode, startOffset: 0, endContainer: testDiv, endOffset: 3});
+ assert_equals(staticRange.startContainer, testTextNode, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 0, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testDiv, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 3, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with Text startContainer and Element endContainer');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testPINode, startOffset: 1, endContainer: testPINode, endOffset: 2});
+ assert_equals(staticRange.startContainer, testPINode, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 1, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testPINode, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 2, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with ProcessingInstruction container');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testCommentNode, startOffset: 1, endContainer: testCommentNode, endOffset: 2});
+ assert_equals(staticRange.startContainer, testCommentNode, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 1, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testCommentNode, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 2, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with Comment container');
+
+test(function() {
+ const xmlDoc = new DOMParser().parseFromString('<xml></xml>', 'application/xml');
+ const testCDATASection = xmlDoc.createCDATASection('abc');
+ const staticRange = new StaticRange({startContainer: testCDATASection, startOffset: 1, endContainer: testCDATASection, endOffset: 2});
+ assert_equals(staticRange.startContainer, testCDATASection, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 1, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testCDATASection, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 2, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with CDATASection container');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: document, startOffset: 0, endContainer: document, endOffset: 1});
+ assert_equals(staticRange.startContainer, document, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 0, 'valid startOffset');
+ assert_equals(staticRange.endContainer, document, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 1, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with Document container');
+
+test(function() {
+ const testDocFrag = document.createDocumentFragment();
+ testDocFrag.append('a','b','c');
+ const staticRange = new StaticRange({startContainer: testDocFrag, startOffset: 0, endContainer: testDocFrag, endOffset: 1});
+ assert_equals(staticRange.startContainer, testDocFrag, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 0, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testDocFrag, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 1, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with DocumentFragment container');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 0, endContainer: testDiv, endOffset: 0});
+ assert_equals(staticRange.startContainer, testDiv, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 0, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testDiv, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 0, 'valid endOffset');
+ assert_true(staticRange.collapsed, 'collapsed');
+}, 'Construct collapsed static range');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 1, endContainer: document.body, endOffset: 0});
+ assert_equals(staticRange.startContainer, testDiv, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 1, 'valid startOffset');
+ assert_equals(staticRange.endContainer, document.body, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 0, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct inverted static range');
+
+test(function() {
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 0, endContainer: testDiv, endOffset: 15});
+ assert_equals(staticRange.startContainer, testDiv, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 0, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testDiv, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 15, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with offset greater than length');
+
+test(function() {
+ const testNode = document.createTextNode('abc');
+ const staticRange = new StaticRange({startContainer: testNode, startOffset: 1, endContainer: testNode, endOffset: 2});
+ assert_equals(staticRange.startContainer, testNode, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 1, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testNode, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 2, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with standalone Node container');
+
+test(function() {
+ const testRoot = document.createElement('div');
+ testRoot.append('a','b');
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 1, endContainer: testRoot, endOffset: 2});
+ assert_equals(staticRange.startContainer, testDiv, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 1, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testRoot, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 2, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with endpoints in disconnected trees');
+
+test(function() {
+ const testDocNode = document.implementation.createDocument('about:blank', 'html', null);
+ const staticRange = new StaticRange({startContainer: document, startOffset: 0, endContainer: testDocNode.documentElement, endOffset: 0});
+ assert_equals(staticRange.startContainer, document, 'valid startContainer');
+ assert_equals(staticRange.startOffset, 0, 'valid startOffset');
+ assert_equals(staticRange.endContainer, testDocNode.documentElement, 'valid endContainer');
+ assert_equals(staticRange.endOffset, 0, 'valid endOffset');
+ assert_false(staticRange.collapsed, 'not collapsed');
+}, 'Construct static range with endpoints in disconnected documents');
+
+test(function() {
+ assert_throws_dom('INVALID_NODE_TYPE_ERR', function() {
+ const staticRange = new StaticRange({startContainer: document.doctype, startOffset: 0, endContainer: document.doctype, endOffset: 0});
+ }, 'throw a InvalidNodeTypeError when a DocumentType is passed as a startContainer or endContainer');
+
+ assert_throws_dom('INVALID_NODE_TYPE_ERR', function() {
+ const testAttrNode = testDiv.getAttributeNode('id');
+ const staticRange = new StaticRange({startContainer: testAttrNode, startOffset: 0, endContainer: testAttrNode, endOffset: 0});
+ }, 'throw a InvalidNodeTypeError when a Attr is passed as a startContainer or endContainer');
+}, 'Throw on DocumentType or Attr container');
+
+test(function () {
+ assert_throws_js(TypeError, function () {
+ const staticRange = new StaticRange();
+ }, 'throw a TypeError when no argument is passed');
+
+ assert_throws_js(TypeError, function () {
+ const staticRange = new StaticRange({startOffset: 0, endContainer: testDiv, endOffset: 0});
+ }, 'throw a TypeError when a startContainer is not passed');
+
+ assert_throws_js(TypeError, function () {
+ const staticRange = new StaticRange({startContainer: testDiv, endContainer: testDiv, endOffset: 0});
+ }, 'throw a TypeError when a startOffset is not passed');
+
+ assert_throws_js(TypeError, function () {
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 0, endOffset: 0});
+ }, 'throw a TypeError when an endContainer is not passed');
+
+ assert_throws_js(TypeError, function () {
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 0, endContainer: testDiv});
+ }, 'throw a TypeError when an endOffset is not passed');
+
+ assert_throws_js(TypeError, function () {
+ const staticRange = new StaticRange({startContainer: null, startOffset: 0, endContainer: testDiv, endOffset: 0});
+ }, 'throw a TypeError when a null startContainer is passed');
+
+ assert_throws_js(TypeError, function () {
+ const staticRange = new StaticRange({startContainer: testDiv, startOffset: 0, endContainer: null, endOffset: 0});
+ }, 'throw a TypeError when a null endContainer is passed');
+}, 'Throw on missing or invalid arguments');
+</script>