diff options
Diffstat (limited to 'testing/web-platform/tests/dom/ranges/Range-cloneContents.html')
-rw-r--r-- | testing/web-platform/tests/dom/ranges/Range-cloneContents.html | 457 |
1 files changed, 457 insertions, 0 deletions
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> |