| <!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> |