diff options
Diffstat (limited to 'testing/web-platform/tests/dom/ranges')
23 files changed, 3814 insertions, 0 deletions
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 000000000..ced47edc5 --- /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 000000000..bf75c9204 --- /dev/null +++ b/testing/web-platform/tests/dom/ranges/Range-cloneContents.html @@ -0,0 +1,457 @@ +<!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(expectedFrag, 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 000000000..6c736df29 --- /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 000000000..78e1d9fb4 --- /dev/null +++ b/testing/web-platform/tests/dom/ranges/Range-collapse.html @@ -0,0 +1,75 @@ +<!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) { + var range; + if (rangeEndpoints == "detached") { + range = document.createRange(); + range.detach(); // should be a no-op and therefore the following should not throw + range.collapse(toStart); + assert_equals(true, range.collapsed); + } + + // Have to account for Ranges involving Documents! + var ownerDoc = rangeEndpoints[0].nodeType == Node.DOCUMENT_NODE + ? rangeEndpoints[0] + : rangeEndpoints[0].ownerDocument; + 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 000000000..f0a3e451c --- /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 000000000..7882ccc31 --- /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 000000000..48413ecd8 --- /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("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("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 000000000..356c8d351 --- /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(new TypeError(), function() { r.comparePoint(null, 0) }) +}) +test(function() { + var doc = document.implementation.createHTMLDocument("tralala") + var r = document.createRange(); + assert_throws("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 000000000..95264796b --- /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("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("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("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 000000000..e8cfbef75 --- /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 000000000..40dc40012 --- /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 000000000..ac35d7136 --- /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 000000000..098837f0c --- /dev/null +++ b/testing/web-platform/tests/dom/ranges/Range-extractContents.html @@ -0,0 +1,248 @@ +<!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(expectedFrag, 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 000000000..4c4073b21 --- /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_false(actualRange === null, + "Range produced in actual iframe was null"); + assert_equals(typeof expectedRange, "object", + "typeof Range produced in expected iframe"); + assert_false(expectedRange === null, + "Range produced in expected iframe was null"); + assert_equals(typeof actualNode, "object", + "typeof Node produced in actual iframe"); + assert_false(actualNode === null, + "Node produced in actual iframe was null"); + assert_equals(typeof expectedNode, "object", + "typeof Node produced in expected iframe"); + assert_false(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(result, 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_false(actualRange === null, + "Range produced in actual iframe was null"); + assert_equals(typeof expectedRange, "object", + "typeof Range produced in expected iframe"); + assert_false(expectedRange === null, + "Range produced in expected iframe was null"); + assert_equals(typeof actualNode, "object", + "typeof Node produced in actual iframe"); + assert_false(actualNode === null, + "Node produced in actual iframe was null"); + assert_equals(typeof expectedNode, "object", + "typeof Node produced in expected iframe"); + assert_false(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-binding.html b/testing/web-platform/tests/dom/ranges/Range-intersectsNode-binding.html new file mode 100644 index 000000000..729388428 --- /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(new TypeError(), function() { r.intersectsNode(); }); + assert_throws(new TypeError(), function() { r.intersectsNode(null); }); + assert_throws(new TypeError(), function() { r.intersectsNode(undefined); }); + assert_throws(new TypeError(), function() { r.intersectsNode(42); }); + assert_throws(new TypeError(), function() { r.intersectsNode("foo"); }); + assert_throws(new TypeError(), function() { r.intersectsNode({}); }); + r.detach(); + assert_throws(new TypeError(), function() { r.intersectsNode(); }); + assert_throws(new TypeError(), function() { r.intersectsNode(null); }); + assert_throws(new TypeError(), function() { r.intersectsNode(undefined); }); + assert_throws(new TypeError(), function() { r.intersectsNode(42); }); + assert_throws(new TypeError(), function() { r.intersectsNode("foo"); }); + assert_throws(new 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.html b/testing/web-platform/tests/dom/ranges/Range-intersectsNode.html new file mode 100644 index 000000000..97e10f6f0 --- /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 000000000..fa87442b8 --- /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("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("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.html b/testing/web-platform/tests/dom/ranges/Range-mutations.html new file mode 100644 index 000000000..ef99ca2ef --- /dev/null +++ b/testing/web-platform/tests/dom/ranges/Range-mutations.html @@ -0,0 +1,950 @@ +<!doctype html> +<title>Range mutation 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"; + +// 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_true(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_true(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"], +]; + + +// Finally run everything. All grouped together at the end so that I can +// easily comment out some of them, so I don't have to wait for all test types +// to debug only some of them. +doTests(splitTextTests, function(params) { return params[0] + ".splitText(" + params[1] + ")" }, testSplitText); +doTests(insertDataTests, function(params) { return params[0] + ".insertData(" + params[1] + ", " + params[2] + ")" }, testInsertData); +doTests(appendDataTests, function(params) { return params[0] + ".appendData(" + params[1] + ")" }, testAppendData); +doTests(deleteDataTests, function(params) { return params[0] + ".deleteData(" + params[1] + ", " + params[2] + ")" }, testDeleteData); +doTests(replaceDataTests, function(params) { return params[0] + ".replaceData(" + params[1] + ", " + params[2] + ", " + params[3] + ")" }, testReplaceData); +doTests(dataChangeTests, function(params) { return params[0] + "." + eval(params[1]) + " " + eval(params[2]) + ' ' + params[3] }, testDataChange); +doTests(insertBeforeTests, function(params) { return params[0] + ".insertBefore(" + params[1] + ", " + params[2] + ")" }, testInsertBefore); +doTests(replaceChildTests, function(params) { return params[0] + ".replaceChild(" + params[1] + ", " + params[2] + ")" }, testReplaceChild); +doTests(appendChildTests, function(params) { return params[0] + ".appendChild(" + params[1] + ")" }, testAppendChild); +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-selectNode.html b/testing/web-platform/tests/dom/ranges/Range-selectNode.html new file mode 100644 index 000000000..2d9ec6b9a --- /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("INVALID_STATE_ERR", function () { + range.selectNode(node); + }, "selectNode() on a detached node must throw INVALID_STATE_ERR"); + assert_throws("INVALID_STATE_ERR", function () { + range.selectNodeContents(node); + }, "selectNodeContents() on a detached node must throw INVALID_STATE_ERR"); + return; + } + + if (!node.parentNode) { + assert_throws("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("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); + } +} +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(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 000000000..5b43c04f4 --- /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("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("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("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("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("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("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("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("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 000000000..330c7421e --- /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 000000000..e8cc11b24 --- /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_false(actualRange === null, + "Range produced in actual iframe was null"); + assert_equals(typeof expectedRange, "object", + "typeof Range produced in expected iframe"); + assert_false(expectedRange === null, + "Range produced in expected iframe was null"); + assert_equals(typeof actualNode, "object", + "typeof Node produced in actual iframe"); + assert_false(actualNode === null, + "Node produced in actual iframe was null"); + assert_equals(typeof expectedNode, "object", + "typeof Node produced in expected iframe"); + assert_false(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(result, 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_false(actualRange === null, + "Range produced in actual iframe was null"); + assert_equals(typeof expectedRange, "object", + "typeof Range produced in expected iframe"); + assert_false(expectedRange === null, + "Range produced in expected iframe was null"); + assert_equals(typeof actualNode, "object", + "typeof Node produced in actual iframe"); + assert_false(actualNode === null, + "Node produced in actual iframe was null"); + assert_equals(typeof expectedNode, "object", + "typeof Node produced in expected iframe"); + assert_false(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 000000000..f354ff758 --- /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> |