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