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