mirror of
https://github.com/servo/servo.git
synced 2025-06-24 09:04:33 +01:00
457 lines
18 KiB
HTML
457 lines
18 KiB
HTML
<!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>
|