diff options
Diffstat (limited to 'testing/web-platform/tests/dom/ranges/Range-mutations.html')
-rw-r--r-- | testing/web-platform/tests/dom/ranges/Range-mutations.html | 950 |
1 files changed, 950 insertions, 0 deletions
diff --git a/testing/web-platform/tests/dom/ranges/Range-mutations.html b/testing/web-platform/tests/dom/ranges/Range-mutations.html new file mode 100644 index 000000000..ef99ca2ef --- /dev/null +++ b/testing/web-platform/tests/dom/ranges/Range-mutations.html @@ -0,0 +1,950 @@ +<!doctype html> +<title>Range mutation tests</title> +<link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name> +<meta name=timeout content=long> + +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=../common.js></script> +<script> +"use strict"; + +// These tests probably use too much abstraction and too little copy-paste. +// Reader beware. +// +// TODO: +// +// * Lots and lots and lots more different types of ranges +// * insertBefore() with DocumentFragments +// * Fill out other insert/remove tests +// * normalize() (https://www.w3.org/Bugs/Public/show_bug.cgi?id=13843) + +// Give a textual description of the range we're testing, for the test names. +function describeRange(startContainer, startOffset, endContainer, endOffset) { + if (startContainer == endContainer && startOffset == endOffset) { + return "range collapsed at (" + startContainer + ", " + startOffset + ")"; + } else if (startContainer == endContainer) { + return "range on " + startContainer + " from " + startOffset + " to " + endOffset; + } else { + return "range from (" + startContainer + ", " + startOffset + ") to (" + endContainer + ", " + endOffset + ")"; + } +} + +// Lists of the various types of nodes we'll want to use. We use strings that +// we can later eval(), so that we can produce legible test names. +var textNodes = [ + "paras[0].firstChild", + "paras[1].firstChild", + "foreignTextNode", + "xmlTextNode", + "detachedTextNode", + "detachedForeignTextNode", + "detachedXmlTextNode", +]; +var commentNodes = [ + "comment", + "foreignComment", + "xmlComment", + "detachedComment", + "detachedForeignComment", + "detachedXmlComment", +]; +var characterDataNodes = textNodes.concat(commentNodes); + +// This function is slightly scary, but it works well enough, so . . . +// sourceTests is an array of test data that will be altered in mysterious ways +// before being passed off to doTest, descFn is something that takes an element +// of sourceTests and produces the first part of a human-readable description +// of the test, testFn is the function that doTest will call to do the actual +// work and tell it what results to expect. +function doTests(sourceTests, descFn, testFn) { + var tests = []; + for (var i = 0; i < sourceTests.length; i++) { + var params = sourceTests[i]; + var len = params.length; + tests.push([ + descFn(params) + ", with unselected " + describeRange(params[len - 4], params[len - 3], params[len - 2], params[len - 1]), + // The closure here ensures that the params that testFn get are the + // current version of params, not the version from the last + // iteration of this loop. We test that none of the parameters + // evaluate to undefined to catch bugs in our eval'ing, like + // mistyping a property name. + function(params) { return function() { + var evaledParams = params.map(eval); + for (var i = 0; i < evaledParams.length; i++) { + assert_true(typeof evaledParams[i] != "undefined", + "Test bug: " + params[i] + " is undefined"); + } + return testFn.apply(null, evaledParams); + } }(params), + false, + params[len - 4], + params[len - 3], + params[len - 2], + params[len - 1] + ]); + tests.push([ + descFn(params) + ", with selected " + describeRange(params[len - 4], params[len - 3], params[len - 2], params[len - 1]), + function(params) { return function(selectedRange) { + var evaledParams = params.slice(0, len - 4).map(eval); + for (var i = 0; i < evaledParams.length; i++) { + assert_true(typeof evaledParams[i] != "undefined", + "Test bug: " + params[i] + " is undefined"); + } + // Override input range with the one that was actually selected when computing the expected result. + evaledParams = evaledParams.concat([selectedRange.startContainer, selectedRange.startOffset, selectedRange.endContainer, selectedRange.endOffset]); + return testFn.apply(null, evaledParams); + } }(params), + true, + params[len - 4], + params[len - 3], + params[len - 2], + params[len - 1] + ]); + } + generate_tests(doTest, tests); +} + +// Set up the range, call the callback function to do the DOM modification and +// tell us what to expect. The callback function needs to return a +// four-element array with the expected start/end containers/offsets, and +// receives no arguments. useSelection tells us whether the Range should be +// added to a Selection and the Selection tested to ensure that the mutation +// affects user selections as well as other ranges; every test is run with this +// both false and true, because when it's set to true WebKit and Opera fail all +// tests' sanity checks, which is unhelpful. The last four parameters just +// tell us what range to build. +function doTest(callback, useSelection, startContainer, startOffset, endContainer, endOffset) { + // Recreate all the test nodes in case they were altered by the last test + // run. + setupRangeTests(); + startContainer = eval(startContainer); + startOffset = eval(startOffset); + endContainer = eval(endContainer); + endOffset = eval(endOffset); + + var ownerDoc = startContainer.nodeType == Node.DOCUMENT_NODE + ? startContainer + : startContainer.ownerDocument; + var range = ownerDoc.createRange(); + range.setStart(startContainer, startOffset); + range.setEnd(endContainer, endOffset); + + if (useSelection) { + getSelection().removeAllRanges(); + getSelection().addRange(range); + + // Some browsers refuse to add a range unless it results in an actual visible selection. + if (!getSelection().rangeCount) + return; + + // Override range with the one that was actually selected as it differs in some browsers. + range = getSelection().getRangeAt(0); + } + + var expected = callback(range); + + assert_equals(range.startContainer, expected[0], + "Wrong start container"); + assert_equals(range.startOffset, expected[1], + "Wrong start offset"); + assert_equals(range.endContainer, expected[2], + "Wrong end container"); + assert_equals(range.endOffset, expected[3], + "Wrong end offset"); +} + + +// Now we get to the specific tests. + +function testSplitText(oldNode, offset, startContainer, startOffset, endContainer, endOffset) { + // Save these for later + var originalStartOffset = startOffset; + var originalEndOffset = endOffset; + var originalLength = oldNode.length; + + var newNode; + try { + newNode = oldNode.splitText(offset); + } catch (e) { + // Should only happen if offset is negative + return [startContainer, startOffset, endContainer, endOffset]; + } + + // First we adjust for replacing data: + // + // "Replace data with offset offset, count count, and data the empty + // string." + // + // That translates to offset = offset, count = originalLength - offset, + // data = "". node is oldNode. + // + // "For every boundary point whose node is node, and whose offset is + // greater than offset but less than or equal to offset plus count, set its + // offset to offset." + if (startContainer == oldNode + && startOffset > offset + && startOffset <= originalLength) { + startOffset = offset; + } + + if (endContainer == oldNode + && endOffset > offset + && endOffset <= originalLength) { + endOffset = offset; + } + + // "For every boundary point whose node is node, and whose offset is + // greater than offset plus count, add the length of data to its offset, + // then subtract count from it." + // + // Can't happen: offset plus count is originalLength. + + // Now we insert a node, if oldNode's parent isn't null: "For each boundary + // point whose node is the new parent of the affected node and whose offset + // is greater than the new index of the affected node, add one to the + // boundary point's offset." + if (startContainer == oldNode.parentNode + && startOffset > 1 + indexOf(oldNode)) { + startOffset++; + } + + if (endContainer == oldNode.parentNode + && endOffset > 1 + indexOf(oldNode)) { + endOffset++; + } + + // Finally, the splitText stuff itself: + // + // "If parent is not null, run these substeps: + // + // * "For each range whose start node is node and start offset is greater + // than offset, set its start node to new node and decrease its start + // offset by offset. + // + // * "For each range whose end node is node and end offset is greater + // than offset, set its end node to new node and decrease its end offset + // by offset. + // + // * "For each range whose start node is parent and start offset is equal + // to the index of node + 1, increase its start offset by one. + // + // * "For each range whose end node is parent and end offset is equal to + // the index of node + 1, increase its end offset by one." + if (oldNode.parentNode) { + if (startContainer == oldNode && originalStartOffset > offset) { + startContainer = newNode; + startOffset = originalStartOffset - offset; + } + + if (endContainer == oldNode && originalEndOffset > offset) { + endContainer = newNode; + endOffset = originalEndOffset - offset; + } + + if (startContainer == oldNode.parentNode + && startOffset == 1 + indexOf(oldNode)) { + startOffset++; + } + + if (endContainer == oldNode.parentNode + && endOffset == 1 + indexOf(oldNode)) { + endOffset++; + } + } + + return [startContainer, startOffset, endContainer, endOffset]; +} + +// The offset argument is unsigned, so per WebIDL -1 should wrap to 4294967295, +// which is probably longer than the length, so it should throw an exception. +// This is no different from the other cases where the offset is longer than +// the length, and the wrapping complicates my testing slightly, so I won't +// bother testing negative values here or in other cases. +var splitTextTests = []; +for (var i = 0; i < textNodes.length; i++) { + var node = textNodes[i]; + splitTextTests.push([node, 376, node, 0, node, 1]); + splitTextTests.push([node, 0, node, 0, node, 0]); + splitTextTests.push([node, 1, node, 1, node, 1]); + splitTextTests.push([node, node + ".length", node, node + ".length", node, node + ".length"]); + splitTextTests.push([node, 1, node, 1, node, 3]); + splitTextTests.push([node, 2, node, 1, node, 3]); + splitTextTests.push([node, 3, node, 1, node, 3]); +} + +splitTextTests.push( + ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0]", 0], + ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0]", 1], + ["paras[0].firstChild", 1, "paras[0]", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 2, "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 3, "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 2, "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 3, "paras[0]", 0, "paras[0].firstChild", 3] +); + + +function testReplaceDataAlgorithm(node, offset, count, data, callback, startContainer, startOffset, endContainer, endOffset) { + // Mutation works the same any time DOM Core's "replace data" algorithm is + // invoked. node, offset, count, data are as in that algorithm. The + // callback is what does the actual setting. Not to be confused with + // testReplaceData, which tests the replaceData() method. + + // Barring any provision to the contrary, the containers and offsets must + // not change. + var expectedStartContainer = startContainer; + var expectedStartOffset = startOffset; + var expectedEndContainer = endContainer; + var expectedEndOffset = endOffset; + + var originalParent = node.parentNode; + var originalData = node.data; + + var exceptionThrown = false; + try { + callback(); + } catch (e) { + // Should only happen if offset is greater than length + exceptionThrown = true; + } + + assert_equals(node.parentNode, originalParent, + "Sanity check failed: changing data changed the parent"); + + // "User agents must run the following steps whenever they replace data of + // a CharacterData node, as though they were written in the specification + // for that algorithm after all other steps. In particular, the steps must + // not be executed if the algorithm threw an exception." + if (exceptionThrown) { + assert_equals(node.data, originalData, + "Sanity check failed: exception thrown but data changed"); + } else { + assert_equals(node.data, + originalData.substr(0, offset) + data + originalData.substr(offset + count), + "Sanity check failed: data not changed as expected"); + } + + // "For every boundary point whose node is node, and whose offset is + // greater than offset but less than or equal to offset plus count, set + // its offset to offset." + if (!exceptionThrown + && startContainer == node + && startOffset > offset + && startOffset <= offset + count) { + expectedStartOffset = offset; + } + + if (!exceptionThrown + && endContainer == node + && endOffset > offset + && endOffset <= offset + count) { + expectedEndOffset = offset; + } + + // "For every boundary point whose node is node, and whose offset is + // greater than offset plus count, add the length of data to its offset, + // then subtract count from it." + if (!exceptionThrown + && startContainer == node + && startOffset > offset + count) { + expectedStartOffset += data.length - count; + } + + if (!exceptionThrown + && endContainer == node + && endOffset > offset + count) { + expectedEndOffset += data.length - count; + } + + return [expectedStartContainer, expectedStartOffset, expectedEndContainer, expectedEndOffset]; +} + +function testInsertData(node, offset, data, startContainer, startOffset, endContainer, endOffset) { + return testReplaceDataAlgorithm(node, offset, 0, data, + function() { node.insertData(offset, data) }, + startContainer, startOffset, endContainer, endOffset); +} + +var insertDataTests = []; +for (var i = 0; i < characterDataNodes.length; i++) { + var node = characterDataNodes[i]; + insertDataTests.push([node, 376, '"foo"', node, 0, node, 1]); + insertDataTests.push([node, 0, '"foo"', node, 0, node, 0]); + insertDataTests.push([node, 1, '"foo"', node, 1, node, 1]); + insertDataTests.push([node, node + ".length", '"foo"', node, node + ".length", node, node + ".length"]); + insertDataTests.push([node, 1, '"foo"', node, 1, node, 3]); + insertDataTests.push([node, 2, '"foo"', node, 1, node, 3]); + insertDataTests.push([node, 3, '"foo"', node, 1, node, 3]); + + insertDataTests.push([node, 376, '""', node, 0, node, 1]); + insertDataTests.push([node, 0, '""', node, 0, node, 0]); + insertDataTests.push([node, 1, '""', node, 1, node, 1]); + insertDataTests.push([node, node + ".length", '""', node, node + ".length", node, node + ".length"]); + insertDataTests.push([node, 1, '""', node, 1, node, 3]); + insertDataTests.push([node, 2, '""', node, 1, node, 3]); + insertDataTests.push([node, 3, '""', node, 1, node, 3]); +} + +insertDataTests.push( + ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0]", 0], + ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0]", 1], + ["paras[0].firstChild", 1, '"foo"', "paras[0]", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 2, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 3, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 2, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 3, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3] +); + + +function testAppendData(node, data, startContainer, startOffset, endContainer, endOffset) { + return testReplaceDataAlgorithm(node, node.length, 0, data, + function() { node.appendData(data) }, + startContainer, startOffset, endContainer, endOffset); +} + +var appendDataTests = []; +for (var i = 0; i < characterDataNodes.length; i++) { + var node = characterDataNodes[i]; + appendDataTests.push([node, '"foo"', node, 0, node, 1]); + appendDataTests.push([node, '"foo"', node, 0, node, 0]); + appendDataTests.push([node, '"foo"', node, 1, node, 1]); + appendDataTests.push([node, '"foo"', node, 0, node, node + ".length"]); + appendDataTests.push([node, '"foo"', node, 1, node, node + ".length"]); + appendDataTests.push([node, '"foo"', node, node + ".length", node, node + ".length"]); + appendDataTests.push([node, '"foo"', node, 1, node, 3]); + + appendDataTests.push([node, '""', node, 0, node, 1]); + appendDataTests.push([node, '""', node, 0, node, 0]); + appendDataTests.push([node, '""', node, 1, node, 1]); + appendDataTests.push([node, '""', node, 0, node, node + ".length"]); + appendDataTests.push([node, '""', node, 1, node, node + ".length"]); + appendDataTests.push([node, '""', node, node + ".length", node, node + ".length"]); + appendDataTests.push([node, '""', node, 1, node, 3]); +} + +appendDataTests.push( + ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0]", 0], + ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0]", 1], + ["paras[0].firstChild", '""', "paras[0]", 1, "paras[0]", 1], + ["paras[0].firstChild", '""', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0].firstChild", 3], + + ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0]", 0], + ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0]", 1], + ["paras[0].firstChild", '"foo"', "paras[0]", 1, "paras[0]", 1], + ["paras[0].firstChild", '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0].firstChild", 3] +); + + +function testDeleteData(node, offset, count, startContainer, startOffset, endContainer, endOffset) { + return testReplaceDataAlgorithm(node, offset, count, "", + function() { node.deleteData(offset, count) }, + startContainer, startOffset, endContainer, endOffset); +} + +var deleteDataTests = []; +for (var i = 0; i < characterDataNodes.length; i++) { + var node = characterDataNodes[i]; + deleteDataTests.push([node, 376, 2, node, 0, node, 1]); + deleteDataTests.push([node, 0, 2, node, 0, node, 0]); + deleteDataTests.push([node, 1, 2, node, 1, node, 1]); + deleteDataTests.push([node, node + ".length", 2, node, node + ".length", node, node + ".length"]); + deleteDataTests.push([node, 1, 2, node, 1, node, 3]); + deleteDataTests.push([node, 2, 2, node, 1, node, 3]); + deleteDataTests.push([node, 3, 2, node, 1, node, 3]); + + deleteDataTests.push([node, 376, 0, node, 0, node, 1]); + deleteDataTests.push([node, 0, 0, node, 0, node, 0]); + deleteDataTests.push([node, 1, 0, node, 1, node, 1]); + deleteDataTests.push([node, node + ".length", 0, node, node + ".length", node, node + ".length"]); + deleteDataTests.push([node, 1, 0, node, 1, node, 3]); + deleteDataTests.push([node, 2, 0, node, 1, node, 3]); + deleteDataTests.push([node, 3, 0, node, 1, node, 3]); + + deleteDataTests.push([node, 376, 631, node, 0, node, 1]); + deleteDataTests.push([node, 0, 631, node, 0, node, 0]); + deleteDataTests.push([node, 1, 631, node, 1, node, 1]); + deleteDataTests.push([node, node + ".length", 631, node, node + ".length", node, node + ".length"]); + deleteDataTests.push([node, 1, 631, node, 1, node, 3]); + deleteDataTests.push([node, 2, 631, node, 1, node, 3]); + deleteDataTests.push([node, 3, 631, node, 1, node, 3]); +} + +deleteDataTests.push( + ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0]", 0], + ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0]", 1], + ["paras[0].firstChild", 1, 2, "paras[0]", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, 2, "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 2, 2, "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 3, 2, "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 2, 2, "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 3, 2, "paras[0]", 0, "paras[0].firstChild", 3] +); + + +function testReplaceData(node, offset, count, data, startContainer, startOffset, endContainer, endOffset) { + return testReplaceDataAlgorithm(node, offset, count, data, + function() { node.replaceData(offset, count, data) }, + startContainer, startOffset, endContainer, endOffset); +} + +var replaceDataTests = []; +for (var i = 0; i < characterDataNodes.length; i++) { + var node = characterDataNodes[i]; + replaceDataTests.push([node, 376, 0, '"foo"', node, 0, node, 1]); + replaceDataTests.push([node, 0, 0, '"foo"', node, 0, node, 0]); + replaceDataTests.push([node, 1, 0, '"foo"', node, 1, node, 1]); + replaceDataTests.push([node, node + ".length", 0, '"foo"', node, node + ".length", node, node + ".length"]); + replaceDataTests.push([node, 1, 0, '"foo"', node, 1, node, 3]); + replaceDataTests.push([node, 2, 0, '"foo"', node, 1, node, 3]); + replaceDataTests.push([node, 3, 0, '"foo"', node, 1, node, 3]); + + replaceDataTests.push([node, 376, 0, '""', node, 0, node, 1]); + replaceDataTests.push([node, 0, 0, '""', node, 0, node, 0]); + replaceDataTests.push([node, 1, 0, '""', node, 1, node, 1]); + replaceDataTests.push([node, node + ".length", 0, '""', node, node + ".length", node, node + ".length"]); + replaceDataTests.push([node, 1, 0, '""', node, 1, node, 3]); + replaceDataTests.push([node, 2, 0, '""', node, 1, node, 3]); + replaceDataTests.push([node, 3, 0, '""', node, 1, node, 3]); + + replaceDataTests.push([node, 376, 1, '"foo"', node, 0, node, 1]); + replaceDataTests.push([node, 0, 1, '"foo"', node, 0, node, 0]); + replaceDataTests.push([node, 1, 1, '"foo"', node, 1, node, 1]); + replaceDataTests.push([node, node + ".length", 1, '"foo"', node, node + ".length", node, node + ".length"]); + replaceDataTests.push([node, 1, 1, '"foo"', node, 1, node, 3]); + replaceDataTests.push([node, 2, 1, '"foo"', node, 1, node, 3]); + replaceDataTests.push([node, 3, 1, '"foo"', node, 1, node, 3]); + + replaceDataTests.push([node, 376, 1, '""', node, 0, node, 1]); + replaceDataTests.push([node, 0, 1, '""', node, 0, node, 0]); + replaceDataTests.push([node, 1, 1, '""', node, 1, node, 1]); + replaceDataTests.push([node, node + ".length", 1, '""', node, node + ".length", node, node + ".length"]); + replaceDataTests.push([node, 1, 1, '""', node, 1, node, 3]); + replaceDataTests.push([node, 2, 1, '""', node, 1, node, 3]); + replaceDataTests.push([node, 3, 1, '""', node, 1, node, 3]); + + replaceDataTests.push([node, 376, 47, '"foo"', node, 0, node, 1]); + replaceDataTests.push([node, 0, 47, '"foo"', node, 0, node, 0]); + replaceDataTests.push([node, 1, 47, '"foo"', node, 1, node, 1]); + replaceDataTests.push([node, node + ".length", 47, '"foo"', node, node + ".length", node, node + ".length"]); + replaceDataTests.push([node, 1, 47, '"foo"', node, 1, node, 3]); + replaceDataTests.push([node, 2, 47, '"foo"', node, 1, node, 3]); + replaceDataTests.push([node, 3, 47, '"foo"', node, 1, node, 3]); + + replaceDataTests.push([node, 376, 47, '""', node, 0, node, 1]); + replaceDataTests.push([node, 0, 47, '""', node, 0, node, 0]); + replaceDataTests.push([node, 1, 47, '""', node, 1, node, 1]); + replaceDataTests.push([node, node + ".length", 47, '""', node, node + ".length", node, node + ".length"]); + replaceDataTests.push([node, 1, 47, '""', node, 1, node, 3]); + replaceDataTests.push([node, 2, 47, '""', node, 1, node, 3]); + replaceDataTests.push([node, 3, 47, '""', node, 1, node, 3]); +} + +replaceDataTests.push( + ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0]", 0], + ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0]", 1], + ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 2, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 3, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 2, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 3, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + + ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0]", 0], + ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0]", 1], + ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 2, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 3, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 2, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 3, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + + ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0]", 0], + ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0]", 1], + ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 2, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 3, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1], + ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 2, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3], + ["paras[0].firstChild", 3, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3] +); + + +// There are lots of ways to set data, so we pass a callback that does the +// actual setting. +function testDataChange(node, attr, op, rval, startContainer, startOffset, endContainer, endOffset) { + return testReplaceDataAlgorithm(node, 0, node.length, op == "=" ? rval : node[attr] + rval, + function() { + if (op == "=") { + node[attr] = rval; + } else if (op == "+=") { + node[attr] += rval; + } else { + throw "Unknown op " + op; + } + }, + startContainer, startOffset, endContainer, endOffset); +} + +var dataChangeTests = []; +var dataChangeTestAttrs = ["data", "textContent", "nodeValue"]; +for (var i = 0; i < characterDataNodes.length; i++) { + var node = characterDataNodes[i]; + var dataChangeTestRanges = [ + [node, 0, node, 0], + [node, 0, node, 1], + [node, 1, node, 1], + [node, 0, node, node + ".length"], + [node, 1, node, node + ".length"], + [node, node + ".length", node, node + ".length"], + ]; + + for (var j = 0; j < dataChangeTestRanges.length; j++) { + for (var k = 0; k < dataChangeTestAttrs.length; k++) { + dataChangeTests.push([ + node, + '"' + dataChangeTestAttrs[k] + '"', + '"="', + '""', + ].concat(dataChangeTestRanges[j])); + + dataChangeTests.push([ + node, + '"' + dataChangeTestAttrs[k] + '"', + '"="', + '"foo"', + ].concat(dataChangeTestRanges[j])); + + dataChangeTests.push([ + node, + '"' + dataChangeTestAttrs[k] + '"', + '"="', + node + "." + dataChangeTestAttrs[k], + ].concat(dataChangeTestRanges[j])); + + dataChangeTests.push([ + node, + '"' + dataChangeTestAttrs[k] + '"', + '"+="', + '""', + ].concat(dataChangeTestRanges[j])); + + dataChangeTests.push([ + node, + '"' + dataChangeTestAttrs[k] + '"', + '"+="', + '"foo"', + ].concat(dataChangeTestRanges[j])); + + dataChangeTests.push([ + node, + '"' + dataChangeTestAttrs[k] + '"', + '"+="', + node + "." + dataChangeTestAttrs[k] + ].concat(dataChangeTestRanges[j])); + } + } +} + + +// Now we test node insertions and deletions, as opposed to just data changes. +// To avoid loads of repetition, we define modifyForRemove() and +// modifyForInsert(). + +// If we were to remove removedNode from its parent, what would the boundary +// point [node, offset] become? Returns [new node, new offset]. Must be +// called BEFORE the node is actually removed, so its parent is not null. (If +// the parent is null, it will do nothing.) +function modifyForRemove(removedNode, point) { + var oldParent = removedNode.parentNode; + var oldIndex = indexOf(removedNode); + if (!oldParent) { + return point; + } + + // "For each boundary point whose node is removed node or a descendant of + // it, set the boundary point to (old parent, old index)." + if (point[0] == removedNode || isDescendant(point[0], removedNode)) { + return [oldParent, oldIndex]; + } + + // "For each boundary point whose node is old parent and whose offset is + // greater than old index, subtract one from its offset." + if (point[0] == oldParent && point[1] > oldIndex) { + return [point[0], point[1] - 1]; + } + + return point; +} + +// Update the given boundary point [node, offset] to account for the fact that +// insertedNode was just inserted into its current position. This must be +// called AFTER insertedNode was already inserted. +function modifyForInsert(insertedNode, point) { + // "For each boundary point whose node is the new parent of the affected + // node and whose offset is greater than the new index of the affected + // node, add one to the boundary point's offset." + if (point[0] == insertedNode.parentNode && point[1] > indexOf(insertedNode)) { + return [point[0], point[1] + 1]; + } + + return point; +} + + +function testInsertBefore(newParent, affectedNode, refNode, startContainer, startOffset, endContainer, endOffset) { + var expectedStart = [startContainer, startOffset]; + var expectedEnd = [endContainer, endOffset]; + + expectedStart = modifyForRemove(affectedNode, expectedStart); + expectedEnd = modifyForRemove(affectedNode, expectedEnd); + + try { + newParent.insertBefore(affectedNode, refNode); + } catch (e) { + // For our purposes, assume that DOM Core is true -- i.e., ignore + // mutation events and similar. + return [startContainer, startOffset, endContainer, endOffset]; + } + + expectedStart = modifyForInsert(affectedNode, expectedStart); + expectedEnd = modifyForInsert(affectedNode, expectedEnd); + + return expectedStart.concat(expectedEnd); +} + +var insertBeforeTests = [ + // Moving a node to its current position + ["testDiv", "paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 0], + ["testDiv", "paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 1], + ["testDiv", "paras[0]", "paras[1]", "paras[0]", 1, "paras[0]", 1], + ["testDiv", "paras[0]", "paras[1]", "testDiv", 0, "testDiv", 2], + ["testDiv", "paras[0]", "paras[1]", "testDiv", 1, "testDiv", 1], + ["testDiv", "paras[0]", "paras[1]", "testDiv", 1, "testDiv", 2], + ["testDiv", "paras[0]", "paras[1]", "testDiv", 2, "testDiv", 2], + + // Stuff that actually moves something. Note that paras[0] and paras[1] + // are both children of testDiv. + ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0], + ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1], + ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 1], + ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 2], + ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 1], + ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 2], + ["paras[0]", "paras[1]", "null", "paras[0]", 0, "paras[0]", 0], + ["paras[0]", "paras[1]", "null", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "paras[1]", "null", "paras[0]", 1, "paras[0]", 1], + ["paras[0]", "paras[1]", "null", "testDiv", 0, "testDiv", 1], + ["paras[0]", "paras[1]", "null", "testDiv", 0, "testDiv", 2], + ["paras[0]", "paras[1]", "null", "testDiv", 1, "testDiv", 1], + ["paras[0]", "paras[1]", "null", "testDiv", 1, "testDiv", 2], + ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 0], + ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 1], + ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 2], + ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 1, "foreignDoc", 1], + ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 0], + ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 1], + ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 2], + ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 1, "foreignDoc", 1], + ["foreignDoc", "detachedComment", "null", "foreignDoc", 0, "foreignDoc", 1], + ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0], + ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1], + + // Stuff that throws exceptions + ["paras[0]", "paras[0]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "testDiv", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "document", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "foreignDoc", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "document.doctype", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], +]; + + +function testReplaceChild(newParent, newChild, oldChild, startContainer, startOffset, endContainer, endOffset) { + var expectedStart = [startContainer, startOffset]; + var expectedEnd = [endContainer, endOffset]; + + expectedStart = modifyForRemove(oldChild, expectedStart); + expectedEnd = modifyForRemove(oldChild, expectedEnd); + + if (newChild != oldChild) { + // Don't do this twice, if they're the same! + expectedStart = modifyForRemove(newChild, expectedStart); + expectedEnd = modifyForRemove(newChild, expectedEnd); + } + + try { + newParent.replaceChild(newChild, oldChild); + } catch (e) { + return [startContainer, startOffset, endContainer, endOffset]; + } + + expectedStart = modifyForInsert(newChild, expectedStart); + expectedEnd = modifyForInsert(newChild, expectedEnd); + + return expectedStart.concat(expectedEnd); +} + +var replaceChildTests = [ + // Moving a node to its current position. Doesn't match most browsers' + // behavior, but we probably want to keep the spec the same anyway: + // https://bugzilla.mozilla.org/show_bug.cgi?id=647603 + ["testDiv", "paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 0], + ["testDiv", "paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 1], + ["testDiv", "paras[0]", "paras[0]", "paras[0]", 1, "paras[0]", 1], + ["testDiv", "paras[0]", "paras[0]", "testDiv", 0, "testDiv", 2], + ["testDiv", "paras[0]", "paras[0]", "testDiv", 1, "testDiv", 1], + ["testDiv", "paras[0]", "paras[0]", "testDiv", 1, "testDiv", 2], + ["testDiv", "paras[0]", "paras[0]", "testDiv", 2, "testDiv", 2], + + // Stuff that actually moves something. + ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0], + ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1], + ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 1], + ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 2], + ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 1], + ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 2], + ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 0], + ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 1], + ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 2], + ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 1, "foreignDoc", 1], + ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 0], + ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 1], + ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 2], + ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 1, "foreignDoc", 1], + ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0], + ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1], + + // Stuff that throws exceptions + ["paras[0]", "paras[0]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "testDiv", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "document", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "foreignDoc", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "document.doctype", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1], +]; + + +function testAppendChild(newParent, affectedNode, startContainer, startOffset, endContainer, endOffset) { + var expectedStart = [startContainer, startOffset]; + var expectedEnd = [endContainer, endOffset]; + + expectedStart = modifyForRemove(affectedNode, expectedStart); + expectedEnd = modifyForRemove(affectedNode, expectedEnd); + + try { + newParent.appendChild(affectedNode); + } catch (e) { + return [startContainer, startOffset, endContainer, endOffset]; + } + + // These two lines will actually never do anything, if you think about it, + // but let's leave them in so correctness is more obvious. + expectedStart = modifyForInsert(affectedNode, expectedStart); + expectedEnd = modifyForInsert(affectedNode, expectedEnd); + + return expectedStart.concat(expectedEnd); +} + +var appendChildTests = [ + // Moving a node to its current position + ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 0, "testDiv.lastChild", 0], + ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 0, "testDiv.lastChild", 1], + ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 1, "testDiv.lastChild", 1], + ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 2", "testDiv", "testDiv.childNodes.length"], + ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 2", "testDiv", "testDiv.childNodes.length - 1"], + ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 1", "testDiv", "testDiv.childNodes.length"], + ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 1", "testDiv", "testDiv.childNodes.length - 1"], + ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length", "testDiv", "testDiv.childNodes.length"], + ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 0, "detachedDiv.lastChild", 0], + ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 0, "detachedDiv.lastChild", 1], + ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 1, "detachedDiv.lastChild", 1], + ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 2", "detachedDiv", "detachedDiv.childNodes.length"], + ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 2", "detachedDiv", "detachedDiv.childNodes.length - 1"], + ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 1", "detachedDiv", "detachedDiv.childNodes.length"], + ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 1", "detachedDiv", "detachedDiv.childNodes.length - 1"], + ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length", "detachedDiv", "detachedDiv.childNodes.length"], + + // Stuff that actually moves something + ["paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 0], + ["paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "paras[1]", "paras[0]", 1, "paras[0]", 1], + ["paras[0]", "paras[1]", "testDiv", 0, "testDiv", 1], + ["paras[0]", "paras[1]", "testDiv", 0, "testDiv", 2], + ["paras[0]", "paras[1]", "testDiv", 1, "testDiv", 1], + ["paras[0]", "paras[1]", "testDiv", 1, "testDiv", 2], + ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length - 1", "foreignDoc", "foreignDoc.childNodes.length"], + ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length - 1", "foreignDoc", "foreignDoc.childNodes.length - 1"], + ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length", "foreignDoc", "foreignDoc.childNodes.length"], + ["foreignDoc", "detachedComment", "detachedComment", 0, "detachedComment", 5], + ["paras[0]", "xmlTextNode", "paras[0]", 0, "paras[0]", 0], + ["paras[0]", "xmlTextNode", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "xmlTextNode", "paras[0]", 1, "paras[0]", 1], + + // Stuff that throws exceptions + ["paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "testDiv", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "document", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "foreignDoc", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "document.doctype", "paras[0]", 0, "paras[0]", 1], +]; + + +function testRemoveChild(affectedNode, startContainer, startOffset, endContainer, endOffset) { + var expectedStart = [startContainer, startOffset]; + var expectedEnd = [endContainer, endOffset]; + + expectedStart = modifyForRemove(affectedNode, expectedStart); + expectedEnd = modifyForRemove(affectedNode, expectedEnd); + + // We don't test cases where the parent is wrong, so this should never + // throw an exception. + affectedNode.parentNode.removeChild(affectedNode); + + return expectedStart.concat(expectedEnd); +} + +var removeChildTests = [ + ["paras[0]", "paras[0]", 0, "paras[0]", 0], + ["paras[0]", "paras[0]", 0, "paras[0]", 1], + ["paras[0]", "paras[0]", 1, "paras[0]", 1], + ["paras[0]", "testDiv", 0, "testDiv", 0], + ["paras[0]", "testDiv", 0, "testDiv", 1], + ["paras[0]", "testDiv", 1, "testDiv", 1], + ["paras[0]", "testDiv", 0, "testDiv", 2], + ["paras[0]", "testDiv", 1, "testDiv", 2], + ["paras[0]", "testDiv", 2, "testDiv", 2], + + ["foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", "foreignDoc.childNodes.length"], +]; + + +// Finally run everything. All grouped together at the end so that I can +// easily comment out some of them, so I don't have to wait for all test types +// to debug only some of them. +doTests(splitTextTests, function(params) { return params[0] + ".splitText(" + params[1] + ")" }, testSplitText); +doTests(insertDataTests, function(params) { return params[0] + ".insertData(" + params[1] + ", " + params[2] + ")" }, testInsertData); +doTests(appendDataTests, function(params) { return params[0] + ".appendData(" + params[1] + ")" }, testAppendData); +doTests(deleteDataTests, function(params) { return params[0] + ".deleteData(" + params[1] + ", " + params[2] + ")" }, testDeleteData); +doTests(replaceDataTests, function(params) { return params[0] + ".replaceData(" + params[1] + ", " + params[2] + ", " + params[3] + ")" }, testReplaceData); +doTests(dataChangeTests, function(params) { return params[0] + "." + eval(params[1]) + " " + eval(params[2]) + ' ' + params[3] }, testDataChange); +doTests(insertBeforeTests, function(params) { return params[0] + ".insertBefore(" + params[1] + ", " + params[2] + ")" }, testInsertBefore); +doTests(replaceChildTests, function(params) { return params[0] + ".replaceChild(" + params[1] + ", " + params[2] + ")" }, testReplaceChild); +doTests(appendChildTests, function(params) { return params[0] + ".appendChild(" + params[1] + ")" }, testAppendChild); +doTests(removeChildTests, function(params) { return params[0] + ".parentNode.removeChild(" + params[0] + ")" }, testRemoveChild); + + +testDiv.style.display = "none"; +</script> |