diff options
Diffstat (limited to 'testing/web-platform/tests/selection')
16 files changed, 2113 insertions, 0 deletions
diff --git a/testing/web-platform/tests/selection/Document-open.html b/testing/web-platform/tests/selection/Document-open.html new file mode 100644 index 000000000..9d170914e --- /dev/null +++ b/testing/web-platform/tests/selection/Document-open.html @@ -0,0 +1,44 @@ +<!doctype html> +<title>Selection Document.open() tests</title> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +"use strict"; + +// This tests the HTML spec requirement "Replace the Document's singleton +// objects with new instances of those objects. (This includes in particular +// the Window, Location, History, ApplicationCache, and Navigator, objects, the +// various BarProp objects, the two Storage objects, the various HTMLCollection +// objects, and objects defined by other specifications, like Selection and the +// document's UndoManager. It also includes all the Web IDL prototypes in the +// JavaScript binding, including the Document object's prototype.)" in the +// document.open() algorithm. + +var iframe = document.createElement("iframe"); +var t = async_test("Selection must be replaced with a new object after document.open()"); +iframe.onload = function() { + t.step(function() { + var originalSelection = iframe.contentWindow.getSelection(); + assert_equals(originalSelection.rangeCount, 0, + "Sanity check: rangeCount must be initially 0"); + iframe.contentDocument.body.appendChild( + iframe.contentDocument.createTextNode("foo")); + var range = iframe.contentDocument.createRange(); + range.selectNodeContents(iframe.contentDocument.body); + iframe.contentWindow.getSelection().addRange(range); + assert_equals(originalSelection.rangeCount, 1, + "Sanity check: rangeCount must be 1 after adding a range"); + + iframe.contentDocument.open(); + + assert_not_equals(iframe.contentWindow.getSelection(), originalSelection, + "After document.open(), the Selection object must no longer be the same"); + assert_equals(iframe.contentWindow.getSelection().rangeCount, 0, + "After document.open(), rangeCount must be 0 again"); + }); + t.done(); + document.body.removeChild(iframe); +}; +document.body.appendChild(iframe); +</script> diff --git a/testing/web-platform/tests/selection/OWNERS b/testing/web-platform/tests/selection/OWNERS new file mode 100644 index 000000000..ce908c45b --- /dev/null +++ b/testing/web-platform/tests/selection/OWNERS @@ -0,0 +1 @@ +@ayg diff --git a/testing/web-platform/tests/selection/addRange.html b/testing/web-platform/tests/selection/addRange.html new file mode 100644 index 000000000..5fe840942 --- /dev/null +++ b/testing/web-platform/tests/selection/addRange.html @@ -0,0 +1,178 @@ +<!doctype html> +<title>Selection.addRange() tests</title> +<div id=log></div> +<meta name="timeout" content="long"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=common.js></script> +<script> +"use strict"; + +function testAddRange(exception, range, endpoints, qualifier, testName) { + test(function() { + assert_equals(exception, null, "Test setup must not throw exceptions"); + + selection.addRange(range); + + assert_equals(range.startContainer, endpoints[0], + "addRange() must not modify the startContainer of the Range it's given"); + assert_equals(range.startOffset, endpoints[1], + "addRange() must not modify the startOffset of the Range it's given"); + assert_equals(range.endContainer, endpoints[2], + "addRange() must not modify the endContainer of the Range it's given"); + assert_equals(range.endOffset, endpoints[3], + "addRange() must not modify the endOffset of the Range it's given"); + }, testName + ": " + qualifier + " addRange() must not throw exceptions or modify the range it's given"); + + test(function() { + assert_equals(exception, null, "Test setup must not throw exceptions"); + + assert_equals(selection.rangeCount, 1, "rangeCount must be 1"); + }, testName + ": " + qualifier + " addRange() must result in rangeCount being 1"); + + // From here on out we check selection.getRangeAt(selection.rangeCount - 1) + // so as not to double-fail Gecko. + + test(function() { + assert_equals(exception, null, "Test setup must not throw exceptions"); + assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0"); + + var newRange = selection.getRangeAt(selection.rangeCount - 1); + + assert_not_equals(newRange, null, + "getRangeAt(rangeCount - 1) must not return null"); + assert_equals(typeof newRange, "object", + "getRangeAt(rangeCount - 1) must return an object"); + + assert_equals(newRange.startContainer, range.startContainer, + "startContainer of the Selection's last Range must match the added Range"); + assert_equals(newRange.startOffset, range.startOffset, + "startOffset of the Selection's last Range must match the added Range"); + assert_equals(newRange.endContainer, range.endContainer, + "endContainer of the Selection's last Range must match the added Range"); + assert_equals(newRange.endOffset, range.endOffset, + "endOffset of the Selection's last Range must match the added Range"); + }, testName + ": " + qualifier + " addRange() must result in the selection's last range having the specified endpoints"); + + test(function() { + assert_equals(exception, null, "Test setup must not throw exceptions"); + assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0"); + + assert_equals(selection.getRangeAt(selection.rangeCount - 1), range, + "getRangeAt(rangeCount - 1) must return the same object we added"); + }, testName + ": " + qualifier + " addRange() must result in the selection's last range being the same object we added"); + + // Let's not test many different modifications -- one should be enough. + test(function() { + assert_equals(exception, null, "Test setup must not throw exceptions"); + assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0"); + + if (range.startContainer == paras[0].firstChild + && range.startOffset == 0 + && range.endContainer == paras[0].firstChild + && range.endOffset == 2) { + // Just in case . . . + range.setStart(paras[0].firstChild, 1); + } else { + range.setStart(paras[0].firstChild, 0); + range.setEnd(paras[0].firstChild, 2); + } + + var newRange = selection.getRangeAt(selection.rangeCount - 1); + + assert_equals(newRange.startContainer, range.startContainer, + "After mutating the " + qualifier + " added Range, startContainer of the Selection's last Range must match the added Range"); + assert_equals(newRange.startOffset, range.startOffset, + "After mutating the " + qualifier + " added Range, startOffset of the Selection's last Range must match the added Range"); + assert_equals(newRange.endContainer, range.endContainer, + "After mutating the " + qualifier + " added Range, endContainer of the Selection's last Range must match the added Range"); + assert_equals(newRange.endOffset, range.endOffset, + "After mutating the " + qualifier + " added Range, endOffset of the Selection's last Range must match the added Range"); + }, testName + ": modifying the " + qualifier + " added range must modify the Selection's last Range"); + + // Now test the other way too. + test(function() { + assert_equals(exception, null, "Test setup must not throw exceptions"); + assert_not_equals(selection.rangeCount, 0, "Cannot proceed with tests if rangeCount is 0"); + + var newRange = selection.getRangeAt(selection.rangeCount - 1); + + if (newRange.startContainer == paras[0].firstChild + && newRange.startOffset == 4 + && newRange.endContainer == paras[0].firstChild + && newRange.endOffset == 6) { + newRange.setStart(paras[0].firstChild, 5); + } else { + newRange.setStart(paras[0].firstChild, 4); + newRange.setStart(paras[0].firstChild, 6); + } + + assert_equals(newRange.startContainer, range.startContainer, + "After " + qualifier + " addRange(), after mutating the Selection's last Range, startContainer of the Selection's last Range must match the added Range"); + assert_equals(newRange.startOffset, range.startOffset, + "After " + qualifier + " addRange(), after mutating the Selection's last Range, startOffset of the Selection's last Range must match the added Range"); + assert_equals(newRange.endContainer, range.endContainer, + "After " + qualifier + " addRange(), after mutating the Selection's last Range, endContainer of the Selection's last Range must match the added Range"); + assert_equals(newRange.endOffset, range.endOffset, + "After " + qualifier + " addRange(), after mutating the Selection's last Range, endOffset of the Selection's last Range must match the added Range"); + }, testName + ": modifying the Selection's last Range must modify the " + qualifier + " added Range"); +} + +// Do only n evals, not n^2 +var testRangesEvaled = testRanges.map(eval); + +for (var i = 0; i < testRanges.length; i++) { + for (var j = 0; j < testRanges.length; j++) { + var testName = "Range " + i + " " + testRanges[i] + + " followed by Range " + j + " " + testRanges[j]; + + var exception = null; + try { + selection.removeAllRanges(); + + var endpoints1 = testRangesEvaled[i]; + var range1 = ownerDocument(endpoints1[0]).createRange(); + range1.setStart(endpoints1[0], endpoints1[1]); + range1.setEnd(endpoints1[2], endpoints1[3]); + + if (range1.startContainer !== endpoints1[0]) { + throw "Sanity check: the first Range we created must have the desired startContainer"; + } + if (range1.startOffset !== endpoints1[1]) { + throw "Sanity check: the first Range we created must have the desired startOffset"; + } + if (range1.endContainer !== endpoints1[2]) { + throw "Sanity check: the first Range we created must have the desired endContainer"; + } + if (range1.endOffset !== endpoints1[3]) { + throw "Sanity check: the first Range we created must have the desired endOffset"; + } + + var endpoints2 = testRangesEvaled[j]; + var range2 = ownerDocument(endpoints2[0]).createRange(); + range2.setStart(endpoints2[0], endpoints2[1]); + range2.setEnd(endpoints2[2], endpoints2[3]); + + if (range2.startContainer !== endpoints2[0]) { + throw "Sanity check: the second Range we created must have the desired startContainer"; + } + if (range2.startOffset !== endpoints2[1]) { + throw "Sanity check: the second Range we created must have the desired startOffset"; + } + if (range2.endContainer !== endpoints2[2]) { + throw "Sanity check: the second Range we created must have the desired endContainer"; + } + if (range2.endOffset !== endpoints2[3]) { + throw "Sanity check: the second Range we created must have the desired endOffset"; + } + } catch (e) { + exception = e; + } + + testAddRange(exception, range1, endpoints1, "first", testName); + testAddRange(exception, range2, endpoints2, "second", testName); + } +} + +testDiv.style.display = "none"; +</script> diff --git a/testing/web-platform/tests/selection/collapse.html b/testing/web-platform/tests/selection/collapse.html new file mode 100644 index 000000000..530158a05 --- /dev/null +++ b/testing/web-platform/tests/selection/collapse.html @@ -0,0 +1,88 @@ +<!doctype html> +<title>Selection.collapse() tests</title> +<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"; + +function testCollapse(range, point) { + selection.removeAllRanges(); + var addedRange; + if (range) { + addedRange = range.cloneRange(); + selection.addRange(addedRange); + } + + if (point[0].nodeType == Node.DOCUMENT_TYPE_NODE) { + assert_throws("INVALID_NODE_TYPE_ERR", function() { + selection.collapse(point[0], point[1]); + }, "Must throw INVALID_NODE_TYPE_ERR when collapse()ing if the node is a DocumentType"); + return; + } + + if (point[1] < 0 || point[1] > getNodeLength(point[0])) { + assert_throws("INDEX_SIZE_ERR", function() { + selection.collapse(point[0], point[1]); + }, "Must throw INDEX_SIZE_ERR when collapse()ing if the offset is negative or greater than the node's length"); + return; + } + + selection.collapse(point[0], point[1]); + + assert_equals(selection.rangeCount, 1, + "selection.rangeCount must equal 1 after collapse()"); + assert_equals(selection.focusNode, point[0], + "focusNode must equal the node we collapse()d to"); + assert_equals(selection.focusOffset, point[1], + "focusOffset must equal the offset we collapse()d to"); + assert_equals(selection.focusNode, selection.anchorNode, + "focusNode and anchorNode must be equal after collapse()"); + assert_equals(selection.focusOffset, selection.anchorOffset, + "focusOffset and anchorOffset must be equal after collapse()"); + if (range) { + assert_equals(addedRange.startContainer, range.startContainer, + "collapse() must not change the startContainer of a preexisting Range"); + assert_equals(addedRange.endContainer, range.endContainer, + "collapse() must not change the endContainer of a preexisting Range"); + assert_equals(addedRange.startOffset, range.startOffset, + "collapse() must not change the startOffset of a preexisting Range"); + assert_equals(addedRange.endOffset, range.endOffset, + "collapse() must not change the endOffset of a preexisting Range"); + } +} + +// Also test a selection with no ranges +testRanges.unshift("[]"); + +// Don't want to eval() each point a bazillion times +var testPointsCached = []; +for (var i = 0; i < testPoints.length; i++) { + testPointsCached.push(eval(testPoints[i])); +} + +var tests = []; +for (var i = 0; i < testRanges.length; i++) { + var endpoints = eval(testRanges[i]); + var range; + test(function() { + if (endpoints.length) { + range = ownerDocument(endpoints[0]).createRange(); + range.setStart(endpoints[0], endpoints[1]); + range.setEnd(endpoints[2], endpoints[3]); + } else { + // Empty selection + range = null; + } + }, "Set up range " + i + " " + testRanges[i]); + for (var j = 0; j < testPoints.length; j++) { + tests.push(["Range " + i + " " + testRanges[i] + ", point " + j + " " + testPoints[j], range, testPointsCached[j]]); + } +} + +generate_tests(testCollapse, tests); + +testDiv.style.display = "none"; +</script> diff --git a/testing/web-platform/tests/selection/collapseToStartEnd.html b/testing/web-platform/tests/selection/collapseToStartEnd.html new file mode 100644 index 000000000..37c57fa78 --- /dev/null +++ b/testing/web-platform/tests/selection/collapseToStartEnd.html @@ -0,0 +1,121 @@ +<!doctype html> +<title>Selection.collapseTo(Start|End)() tests</title> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=common.js></script> +<script> +"use strict"; + +// Also test a selection with no ranges +testRanges.unshift("[]"); + +for (var i = 0; i < testRanges.length; i++) { + test(function() { + selection.removeAllRanges(); + var endpoints = eval(testRanges[i]); + if (!endpoints.length) { + assert_throws("INVALID_STATE_ERR", function() { + selection.collapseToStart(); + }, "Must throw InvalidStateErr if the selection's range is null"); + return; + } + + var addedRange = ownerDocument(endpoints[0]).createRange(); + addedRange.setStart(endpoints[0], endpoints[1]); + addedRange.setEnd(endpoints[2], endpoints[3]); + selection.addRange(addedRange); + + // We don't penalize browsers here for mishandling addRange() and + // adding a different range than we specified. They fail addRange() + // tests for that, and don't have to fail collapseToStart/End() tests + // too. They do fail if they throw unexpectedly, though. I also fail + // them if there's no range at all, because otherwise they could pass + // all tests if addRange() always does nothing and collapseToStart() + // always throws. + assert_equals(selection.rangeCount, 1, + "Sanity check: rangeCount must equal 1 after addRange()"); + + var expectedEndpoint = [ + selection.getRangeAt(0).startContainer, + selection.getRangeAt(0).startOffset + ]; + + selection.collapseToStart(); + + assert_equals(selection.rangeCount, 1, + "selection.rangeCount must equal 1"); + assert_equals(selection.focusNode, expectedEndpoint[0], + "focusNode must equal the original start node"); + assert_equals(selection.focusOffset, expectedEndpoint[1], + "focusOffset must equal the original start offset"); + assert_equals(selection.anchorNode, expectedEndpoint[0], + "anchorNode must equal the original start node"); + assert_equals(selection.anchorOffset, expectedEndpoint[1], + "anchorOffset must equal the original start offset"); + assert_equals(addedRange.startContainer, endpoints[0], + "collapseToStart() must not change the startContainer of the selection's original range"); + assert_equals(addedRange.startOffset, endpoints[1], + "collapseToStart() must not change the startOffset of the selection's original range"); + assert_equals(addedRange.endContainer, endpoints[2], + "collapseToStart() must not change the endContainer of the selection's original range"); + assert_equals(addedRange.endOffset, endpoints[3], + "collapseToStart() must not change the endOffset of the selection's original range"); + }, "Range " + i + " " + testRanges[i] + " collapseToStart()"); + + // Copy-paste of above + test(function() { + selection.removeAllRanges(); + var endpoints = eval(testRanges[i]); + if (!endpoints.length) { + assert_throws("INVALID_STATE_ERR", function() { + selection.collapseToEnd(); + }, "Must throw InvalidStateErr if the selection's range is null"); + return; + } + + var addedRange = ownerDocument(endpoints[0]).createRange(); + addedRange.setStart(endpoints[0], endpoints[1]); + addedRange.setEnd(endpoints[2], endpoints[3]); + selection.addRange(addedRange); + + // We don't penalize browsers here for mishandling addRange() and + // adding a different range than we specified. They fail addRange() + // tests for that, and don't have to fail collapseToStart/End() tests + // too. They do fail if they throw unexpectedly, though. I also fail + // them if there's no range at all, because otherwise they could pass + // all tests if addRange() always does nothing and collapseToStart() + // always throws. + assert_equals(selection.rangeCount, 1, + "Sanity check: rangeCount must equal 1 after addRange()"); + + var expectedEndpoint = [ + selection.getRangeAt(0).endContainer, + selection.getRangeAt(0).endOffset + ]; + + selection.collapseToEnd(); + + assert_equals(selection.rangeCount, 1, + "selection.rangeCount must equal 1"); + assert_equals(selection.focusNode, expectedEndpoint[0], + "focusNode must equal the original end node"); + assert_equals(selection.focusOffset, expectedEndpoint[1], + "focusOffset must equal the original end offset"); + assert_equals(selection.anchorNode, expectedEndpoint[0], + "anchorNode must equal the original end node"); + assert_equals(selection.anchorOffset, expectedEndpoint[1], + "anchorOffset must equal the original end offset"); + assert_equals(addedRange.startContainer, endpoints[0], + "collapseToEnd() must not change the startContainer of the selection's original range"); + assert_equals(addedRange.startOffset, endpoints[1], + "collapseToEnd() must not change the startOffset of the selection's original range"); + assert_equals(addedRange.endContainer, endpoints[2], + "collapseToEnd() must not change the endContainer of the selection's original range"); + assert_equals(addedRange.endOffset, endpoints[3], + "collapseToEnd() must not change the endOffset of the selection's original range"); + }, "Range " + i + " " + testRanges[i] + " collapseToEnd()"); +} + +testDiv.style.display = "none"; +</script> diff --git a/testing/web-platform/tests/selection/common.js b/testing/web-platform/tests/selection/common.js new file mode 100644 index 000000000..c0b622f63 --- /dev/null +++ b/testing/web-platform/tests/selection/common.js @@ -0,0 +1,952 @@ +"use strict"; +// TODO: iframes, contenteditable/designMode + +// Everything is done in functions in this test harness, so we have to declare +// all the variables before use to make sure they can be reused. +var selection; +var testDiv, paras, detachedDiv, detachedPara1, detachedPara2, + foreignDoc, foreignPara1, foreignPara2, xmlDoc, xmlElement, + detachedXmlElement, detachedTextNode, foreignTextNode, + detachedForeignTextNode, xmlTextNode, detachedXmlTextNode, + processingInstruction, detachedProcessingInstruction, comment, + detachedComment, foreignComment, detachedForeignComment, xmlComment, + detachedXmlComment, docfrag, foreignDocfrag, xmlDocfrag, doctype, + foreignDoctype, xmlDoctype; +var testRanges, testPoints, testNodes; + +function setupRangeTests() { + selection = getSelection(); + testDiv = document.querySelector("#test"); + if (testDiv) { + testDiv.parentNode.removeChild(testDiv); + } + testDiv = document.createElement("div"); + testDiv.id = "test"; + document.body.insertBefore(testDiv, document.body.firstChild); + // Test some diacritics, to make sure browsers are using code units here + // and not something like grapheme clusters. + testDiv.innerHTML = "<p id=a>Äb̈c̈d̈ëf̈g̈ḧ\n" + + "<p id=b style=display:none>Ijklmnop\n" + + "<p id=c>Qrstuvwx" + + "<p id=d style=display:none>Yzabcdef" + + "<p id=e style=display:none>Ghijklmn"; + paras = testDiv.querySelectorAll("p"); + + detachedDiv = document.createElement("div"); + detachedPara1 = document.createElement("p"); + detachedPara1.appendChild(document.createTextNode("Opqrstuv")); + detachedPara2 = document.createElement("p"); + detachedPara2.appendChild(document.createTextNode("Wxyzabcd")); + detachedDiv.appendChild(detachedPara1); + detachedDiv.appendChild(detachedPara2); + + // Opera doesn't automatically create a doctype for a new HTML document, + // contrary to spec. It also doesn't let you add doctypes to documents + // after the fact through any means I've tried. So foreignDoc in Opera + // will have no doctype, foreignDoctype will be null, and Opera will fail + // some tests somewhat mysteriously as a result. + foreignDoc = document.implementation.createHTMLDocument(""); + foreignPara1 = foreignDoc.createElement("p"); + foreignPara1.appendChild(foreignDoc.createTextNode("Efghijkl")); + foreignPara2 = foreignDoc.createElement("p"); + foreignPara2.appendChild(foreignDoc.createTextNode("Mnopqrst")); + foreignDoc.body.appendChild(foreignPara1); + foreignDoc.body.appendChild(foreignPara2); + + // Now we get to do really silly stuff, which nobody in the universe is + // ever going to actually do, but the spec defines behavior, so too bad. + // Testing is fun! + xmlDoctype = document.implementation.createDocumentType("qorflesnorf", "abcde", "x\"'y"); + xmlDoc = document.implementation.createDocument(null, null, xmlDoctype); + detachedXmlElement = xmlDoc.createElement("everyone-hates-hyphenated-element-names"); + detachedTextNode = document.createTextNode("Uvwxyzab"); + detachedForeignTextNode = foreignDoc.createTextNode("Cdefghij"); + detachedXmlTextNode = xmlDoc.createTextNode("Klmnopqr"); + // PIs only exist in XML documents, so don't bother with document or + // foreignDoc. + detachedProcessingInstruction = xmlDoc.createProcessingInstruction("whippoorwill", "chirp chirp chirp"); + detachedComment = document.createComment("Stuvwxyz"); + // Hurrah, we finally got to "z" at the end! + detachedForeignComment = foreignDoc.createComment("אריה יהודה"); + detachedXmlComment = xmlDoc.createComment("בן חיים אליעזר"); + + // We should also test with document fragments that actually contain stuff + // . . . but, maybe later. + docfrag = document.createDocumentFragment(); + foreignDocfrag = foreignDoc.createDocumentFragment(); + xmlDocfrag = xmlDoc.createDocumentFragment(); + + xmlElement = xmlDoc.createElement("igiveuponcreativenames"); + xmlTextNode = xmlDoc.createTextNode("do re mi fa so la ti"); + xmlElement.appendChild(xmlTextNode); + processingInstruction = xmlDoc.createProcessingInstruction("somePI", 'Did you know that ":syn sync fromstart" is very useful when using vim to edit large amounts of JavaScript embedded in HTML?'); + xmlDoc.appendChild(xmlElement); + xmlDoc.appendChild(processingInstruction); + xmlComment = xmlDoc.createComment("I maliciously created a comment that will break incautious XML serializers, but Firefox threw an exception, so all I got was this lousy T-shirt"); + xmlDoc.appendChild(xmlComment); + + comment = document.createComment("Alphabet soup?"); + testDiv.appendChild(comment); + + foreignComment = foreignDoc.createComment('"Commenter" and "commentator" mean different things. I\'ve seen non-native speakers trip up on this.'); + foreignDoc.appendChild(foreignComment); + foreignTextNode = foreignDoc.createTextNode("I admit that I harbor doubts about whether we really need so many things to test, but it's too late to stop now."); + foreignDoc.body.appendChild(foreignTextNode); + + doctype = document.doctype; + foreignDoctype = foreignDoc.doctype; + + testRanges = [ + // Various ranges within the text node children of different + // paragraphs. All should be valid. + "[paras[0].firstChild, 0, paras[0].firstChild, 0]", + "[paras[0].firstChild, 0, paras[0].firstChild, 1]", + "[paras[0].firstChild, 2, paras[0].firstChild, 8]", + "[paras[0].firstChild, 2, paras[0].firstChild, 9]", + "[paras[1].firstChild, 0, paras[1].firstChild, 0]", + "[paras[1].firstChild, 0, paras[1].firstChild, 1]", + "[paras[1].firstChild, 2, paras[1].firstChild, 8]", + "[paras[1].firstChild, 2, paras[1].firstChild, 9]", + "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]", + "[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]", + "[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]", + "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]", + "[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]", + "[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]", + // Now try testing some elements, not just text nodes. + "[document.documentElement, 0, document.documentElement, 1]", + "[document.documentElement, 0, document.documentElement, 2]", + "[document.documentElement, 1, document.documentElement, 2]", + "[document.head, 1, document.head, 1]", + "[document.body, 0, document.body, 1]", + "[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]", + "[foreignDoc.head, 1, foreignDoc.head, 1]", + "[foreignDoc.body, 0, foreignDoc.body, 0]", + "[paras[0], 0, paras[0], 0]", + "[paras[0], 0, paras[0], 1]", + "[detachedPara1, 0, detachedPara1, 0]", + "[detachedPara1, 0, detachedPara1, 1]", + // Now try some ranges that span elements. + "[paras[0].firstChild, 0, paras[1].firstChild, 0]", + "[paras[0].firstChild, 0, paras[1].firstChild, 8]", + "[paras[0].firstChild, 3, paras[3], 1]", + // How about something that spans a node and its descendant? + "[paras[0], 0, paras[0].firstChild, 7]", + "[testDiv, 2, paras[4], 1]", + "[testDiv, 1, paras[2].firstChild, 5]", + "[document.documentElement, 1, document.body, 0]", + "[foreignDoc.documentElement, 1, foreignDoc.body, 0]", + // Then a few more interesting things just for good measure. + "[document, 0, document, 1]", + "[document, 0, document, 2]", + "[document, 1, document, 2]", + "[testDiv, 0, comment, 5]", + "[paras[2].firstChild, 4, comment, 2]", + "[paras[3], 1, comment, 8]", + "[foreignDoc, 0, foreignDoc, 0]", + "[foreignDoc, 1, foreignComment, 2]", + "[foreignDoc.body, 0, foreignTextNode, 36]", + "[xmlDoc, 0, xmlDoc, 0]", + // Opera 11 crashes if you extractContents() a range that ends at offset + // zero in a comment. Comment out this line to run the tests successfully. + "[xmlDoc, 1, xmlComment, 0]", + "[detachedTextNode, 0, detachedTextNode, 8]", + "[detachedForeignTextNode, 7, detachedForeignTextNode, 7]", + "[detachedForeignTextNode, 0, detachedForeignTextNode, 8]", + "[detachedXmlTextNode, 7, detachedXmlTextNode, 7]", + "[detachedXmlTextNode, 0, detachedXmlTextNode, 8]", + "[detachedComment, 3, detachedComment, 4]", + "[detachedComment, 5, detachedComment, 5]", + "[detachedForeignComment, 0, detachedForeignComment, 1]", + "[detachedForeignComment, 4, detachedForeignComment, 4]", + "[detachedXmlComment, 2, detachedXmlComment, 6]", + "[docfrag, 0, docfrag, 0]", + "[foreignDocfrag, 0, foreignDocfrag, 0]", + "[xmlDocfrag, 0, xmlDocfrag, 0]", + ]; + + testPoints = [ + // Various positions within the page, some invalid. Remember that + // paras[0] is visible, and paras[1] is display: none. + "[paras[0].firstChild, -1]", + "[paras[0].firstChild, 0]", + "[paras[0].firstChild, 1]", + "[paras[0].firstChild, 2]", + "[paras[0].firstChild, 8]", + "[paras[0].firstChild, 9]", + "[paras[0].firstChild, 10]", + "[paras[0].firstChild, 65535]", + "[paras[1].firstChild, -1]", + "[paras[1].firstChild, 0]", + "[paras[1].firstChild, 1]", + "[paras[1].firstChild, 2]", + "[paras[1].firstChild, 8]", + "[paras[1].firstChild, 9]", + "[paras[1].firstChild, 10]", + "[paras[1].firstChild, 65535]", + "[detachedPara1.firstChild, 0]", + "[detachedPara1.firstChild, 1]", + "[detachedPara1.firstChild, 8]", + "[detachedPara1.firstChild, 9]", + "[foreignPara1.firstChild, 0]", + "[foreignPara1.firstChild, 1]", + "[foreignPara1.firstChild, 8]", + "[foreignPara1.firstChild, 9]", + // Now try testing some elements, not just text nodes. + "[document.documentElement, -1]", + "[document.documentElement, 0]", + "[document.documentElement, 1]", + "[document.documentElement, 2]", + "[document.documentElement, 7]", + "[document.head, 1]", + "[document.body, 3]", + "[foreignDoc.documentElement, 0]", + "[foreignDoc.documentElement, 1]", + "[foreignDoc.head, 0]", + "[foreignDoc.body, 1]", + "[paras[0], 0]", + "[paras[0], 1]", + "[paras[0], 2]", + "[paras[1], 0]", + "[paras[1], 1]", + "[paras[1], 2]", + "[detachedPara1, 0]", + "[detachedPara1, 1]", + "[testDiv, 0]", + "[testDiv, 3]", + // Then a few more interesting things just for good measure. + "[document, -1]", + "[document, 0]", + "[document, 1]", + "[document, 2]", + "[document, 3]", + "[comment, -1]", + "[comment, 0]", + "[comment, 4]", + "[comment, 96]", + "[foreignDoc, 0]", + "[foreignDoc, 1]", + "[foreignComment, 2]", + "[foreignTextNode, 0]", + "[foreignTextNode, 36]", + "[xmlDoc, -1]", + "[xmlDoc, 0]", + "[xmlDoc, 1]", + "[xmlDoc, 5]", + "[xmlComment, 0]", + "[xmlComment, 4]", + "[processingInstruction, 0]", + "[processingInstruction, 5]", + "[processingInstruction, 9]", + "[detachedTextNode, 0]", + "[detachedTextNode, 8]", + "[detachedForeignTextNode, 0]", + "[detachedForeignTextNode, 8]", + "[detachedXmlTextNode, 0]", + "[detachedXmlTextNode, 8]", + "[detachedProcessingInstruction, 12]", + "[detachedComment, 3]", + "[detachedComment, 5]", + "[detachedForeignComment, 0]", + "[detachedForeignComment, 4]", + "[detachedXmlComment, 2]", + "[docfrag, 0]", + "[foreignDocfrag, 0]", + "[xmlDocfrag, 0]", + "[doctype, 0]", + "[doctype, -17]", + "[doctype, 1]", + "[foreignDoctype, 0]", + "[xmlDoctype, 0]", + ]; + + testNodes = [ + "paras[0]", + "paras[0].firstChild", + "paras[1]", + "paras[1].firstChild", + "foreignPara1", + "foreignPara1.firstChild", + "detachedPara1", + "detachedPara1.firstChild", + "detachedPara1", + "detachedPara1.firstChild", + "testDiv", + "document", + "detachedDiv", + "detachedPara2", + "foreignDoc", + "foreignPara2", + "xmlDoc", + "xmlElement", + "detachedXmlElement", + "detachedTextNode", + "foreignTextNode", + "detachedForeignTextNode", + "xmlTextNode", + "detachedXmlTextNode", + "processingInstruction", + "detachedProcessingInstruction", + "comment", + "detachedComment", + "foreignComment", + "detachedForeignComment", + "xmlComment", + "detachedXmlComment", + "docfrag", + "foreignDocfrag", + "xmlDocfrag", + "doctype", + "foreignDoctype", + "xmlDoctype", + ]; +} +if ("setup" in window) { + setup(setupRangeTests); +} else { + // Presumably we're running from within an iframe or something + setupRangeTests(); +} + +/** + * Return the length of a node as specified in DOM Range. + */ +function getNodeLength(node) { + if (node.nodeType == Node.DOCUMENT_TYPE_NODE) { + return 0; + } + if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.PROCESSING_INSTRUCTION_NODE || node.nodeType == Node.COMMENT_NODE) { + return node.length; + } + return node.childNodes.length; +} + +/** + * Returns the furthest ancestor of a Node as defined by the spec. + */ +function furthestAncestor(node) { + var root = node; + while (root.parentNode != null) { + root = root.parentNode; + } + return root; +} + +/** + * "The ancestor containers of a Node are the Node itself and all its + * ancestors." + * + * Is node1 an ancestor container of node2? + */ +function isAncestorContainer(node1, node2) { + return node1 == node2 || + (node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS); +} + +/** + * Returns the first Node that's after node in tree order, or null if node is + * the last Node. + */ +function nextNode(node) { + if (node.hasChildNodes()) { + return node.firstChild; + } + return nextNodeDescendants(node); +} + +/** + * Returns the last Node that's before node in tree order, or null if node is + * the first Node. + */ +function previousNode(node) { + if (node.previousSibling) { + node = node.previousSibling; + while (node.hasChildNodes()) { + node = node.lastChild; + } + return node; + } + return node.parentNode; +} + +/** + * Returns the next Node that's after node and all its descendants in tree + * order, or null if node is the last Node or an ancestor of it. + */ +function nextNodeDescendants(node) { + while (node && !node.nextSibling) { + node = node.parentNode; + } + if (!node) { + return null; + } + return node.nextSibling; +} + +/** + * Returns the ownerDocument of the Node, or the Node itself if it's a + * Document. + */ +function ownerDocument(node) { + return node.nodeType == Node.DOCUMENT_NODE + ? node + : node.ownerDocument; +} + +/** + * Returns true if ancestor is an ancestor of descendant, false otherwise. + */ +function isAncestor(ancestor, descendant) { + if (!ancestor || !descendant) { + return false; + } + while (descendant && descendant != ancestor) { + descendant = descendant.parentNode; + } + return descendant == ancestor; +} + +/** + * Returns true if descendant is a descendant of ancestor, false otherwise. + */ +function isDescendant(descendant, ancestor) { + return isAncestor(ancestor, descendant); +} + +/** + * The position of two boundary points relative to one another, as defined by + * the spec. + */ +function getPosition(nodeA, offsetA, nodeB, offsetB) { + // "If node A is the same as node B, return equal if offset A equals offset + // B, before if offset A is less than offset B, and after if offset A is + // greater than offset B." + if (nodeA == nodeB) { + if (offsetA == offsetB) { + return "equal"; + } + if (offsetA < offsetB) { + return "before"; + } + if (offsetA > offsetB) { + return "after"; + } + } + + // "If node A is after node B in tree order, compute the position of (node + // B, offset B) relative to (node A, offset A). If it is before, return + // after. If it is after, return before." + if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) { + var pos = getPosition(nodeB, offsetB, nodeA, offsetA); + if (pos == "before") { + return "after"; + } + if (pos == "after") { + return "before"; + } + } + + // "If node A is an ancestor of node B:" + if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) { + // "Let child equal node B." + var child = nodeB; + + // "While child is not a child of node A, set child to its parent." + while (child.parentNode != nodeA) { + child = child.parentNode; + } + + // "If the index of child is less than offset A, return after." + if (indexOf(child) < offsetA) { + return "after"; + } + } + + // "Return before." + return "before"; +} + +/** + * "contained" as defined by DOM Range: "A Node node is contained in a range + * range if node's furthest ancestor is the same as range's root, and (node, 0) + * is after range's start, and (node, length of node) is before range's end." + */ +function isContained(node, range) { + var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); + var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset); + + return furthestAncestor(node) == furthestAncestor(range.startContainer) + && pos1 == "after" + && pos2 == "before"; +} + +/** + * "partially contained" as defined by DOM Range: "A Node is partially + * contained in a range if it is an ancestor container of the range's start but + * not its end, or vice versa." + */ +function isPartiallyContained(node, range) { + var cond1 = isAncestorContainer(node, range.startContainer); + var cond2 = isAncestorContainer(node, range.endContainer); + return (cond1 && !cond2) || (cond2 && !cond1); +} + +/** + * Index of a node as defined by the spec. + */ +function indexOf(node) { + if (!node.parentNode) { + // No preceding sibling nodes, right? + return 0; + } + var i = 0; + while (node != node.parentNode.childNodes[i]) { + i++; + } + return i; +} + +/** + * extractContents() implementation, following the spec. If an exception is + * supposed to be thrown, will return a string with the name (e.g., + * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also + * return an arbitrary human-readable string if a condition is hit that implies + * a spec bug. + */ +function myExtractContents(range) { + // "If the context object's detached flag is set, raise an + // INVALID_STATE_ERR exception and abort these steps." + try { + range.collapsed; + } catch (e) { + return "INVALID_STATE_ERR"; + } + + // "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 or Comment node:" + if (range.startContainer == range.endContainer + && (range.startContainer.nodeType == Node.TEXT_NODE + || range.startContainer.nodeType == Node.COMMENT_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); + + // "Call deleteData(original start offset, original end offset − + // original start offset) on original start node." + originalStartNode.deleteData(originalStartOffset, + originalEndOffset - originalStartOffset); + + // "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 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, newOffset; + if (isAncestorContainer(originalStartNode, originalEndNode)) { + 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 + && !isAncestorContainer(referenceNode.parentNode, originalEndNode)) { + 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 first partially contained child is a Text or Comment node:" + if (firstPartiallyContainedChild + && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE + || firstPartiallyContainedChild.nodeType == Node.COMMENT_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, + getNodeLength(originalStartNode) - originalStartOffset); + + // "Append clone as the last child of frag." + frag.appendChild(clone); + + // "Call deleteData() on original start node, with original start + // offset as the first argument and (length of original start node − + // original start offset) as the second." + originalStartNode.deleteData(originalStartOffset, + getNodeLength(originalStartNode) - originalStartOffset); + // "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, + getNodeLength(firstPartiallyContainedChild)); + + // "Let subfrag be the result of calling extractContents() on + // subrange." + var subfrag = myExtractContents(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, append contained child + // as the last child of frag." + for (var i = 0; i < containedChildren.length; i++) { + frag.appendChild(containedChildren[i]); + } + + // "If last partially contained child is a Text or Comment node:" + if (lastPartiallyContainedChild + && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE + || lastPartiallyContainedChild.nodeType == Node.COMMENT_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); + + // "Call deleteData(0, original end offset) on original end node." + originalEndNode.deleteData(0, originalEndOffset); + // "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 extractContents() on + // subrange." + var subfrag = myExtractContents(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]); + } + } + + // "Set the context object's start and end to (new node, new offset)." + range.setStart(newNode, newOffset); + range.setEnd(newNode, newOffset); + + // "Return frag." + return frag; +} + +/** + * insertNode() implementation, following the spec. If an exception is + * supposed to be thrown, will return a string with the name (e.g., + * "HIERARCHY_REQUEST_ERR") instead of a document fragment. It might also + * return an arbitrary human-readable string if a condition is hit that implies + * a spec bug. + */ +function myInsertNode(range, newNode) { + // "If the context object's detached flag is set, raise an + // INVALID_STATE_ERR exception and abort these steps." + // + // Assume that if accessing collapsed throws, it's detached. + try { + range.collapsed; + } catch (e) { + return "INVALID_STATE_ERR"; + } + + // "If the context object's start node is a Text or Comment node and its + // parent is null, raise an HIERARCHY_REQUEST_ERR exception and abort these + // steps." + if ((range.startContainer.nodeType == Node.TEXT_NODE + || range.startContainer.nodeType == Node.COMMENT_NODE) + && !range.startContainer.parentNode) { + return "HIERARCHY_REQUEST_ERR"; + } + + // "If the context object's start node is a Text node, run splitText() on + // it with the context object's start offset as its argument, and let + // reference node be the result." + var referenceNode; + if (range.startContainer.nodeType == Node.TEXT_NODE) { + // We aren't testing how ranges vary under mutations, and browsers vary + // in how they mutate for splitText, so let's just force the correct + // way. + var start = [range.startContainer, range.startOffset]; + var end = [range.endContainer, range.endOffset]; + + referenceNode = range.startContainer.splitText(range.startOffset); + + if (start[0] == end[0] + && end[1] > start[1]) { + end[0] = referenceNode; + end[1] -= start[1]; + } else if (end[0] == start[0].parentNode + && end[1] > indexOf(referenceNode)) { + end[1]++; + } + range.setStart(start[0], start[1]); + range.setEnd(end[0], end[1]); + // "Otherwise, if the context object's start node is a Comment, let + // reference node be the context object's start node." + } else if (range.startContainer.nodeType == Node.COMMENT_NODE) { + referenceNode = range.startContainer; + // "Otherwise, let reference node be the child of the context object's + // start node with index equal to the context object's start offset, or + // null if there is no such child." + } else { + referenceNode = range.startContainer.childNodes[range.startOffset]; + if (typeof referenceNode == "undefined") { + referenceNode = null; + } + } + + // "If reference node is null, let parent node be the context object's + // start node." + var parentNode; + if (!referenceNode) { + parentNode = range.startContainer; + // "Otherwise, let parent node be the parent of reference node." + } else { + parentNode = referenceNode.parentNode; + } + + // "Call insertBefore(newNode, reference node) on parent node, re-raising + // any exceptions that call raised." + try { + parentNode.insertBefore(newNode, referenceNode); + } catch (e) { + return getDomExceptionName(e); + } +} + +/** + * Asserts that two nodes are equal, in the sense of isEqualNode(). If they + * aren't, tries to print a relatively informative reason why not. TODO: Move + * this to testharness.js? + */ +function assertNodesEqual(actual, expected, msg) { + if (!actual.isEqualNode(expected)) { + msg = "Actual and expected mismatch for " + msg + ". "; + + while (actual && expected) { + assert_true(actual.nodeType === expected.nodeType + && actual.nodeName === expected.nodeName + && actual.nodeValue === expected.nodeValue + && actual.childNodes.length === expected.childNodes.length, + "First differing node: expected " + format_value(expected) + + ", got " + format_value(actual)); + actual = nextNode(actual); + expected = nextNode(expected); + } + + assert_unreached("DOMs were not equal but we couldn't figure out why"); + } +} + +/** + * Given a DOMException, return the name (e.g., "HIERARCHY_REQUEST_ERR"). In + * theory this should be just e.name, but in practice it's not. So I could + * legitimately just return e.name, but then every engine but WebKit would fail + * every test, since no one seems to care much for standardizing DOMExceptions. + * Instead I mangle it to account for browser bugs, so as not to fail + * insertNode() tests (for instance) for insertBefore() bugs. Of course, a + * standards-compliant browser will work right in any event. + * + * If the exception has no string property called "name" or "message", we just + * re-throw it. + */ +function getDomExceptionName(e) { + if (typeof e.name == "string" + && /^[A-Z_]+_ERR$/.test(e.name)) { + // Either following the standard, or prefixing NS_ERROR_DOM (I'm + // looking at you, Gecko). + return e.name.replace(/^NS_ERROR_DOM_/, ""); + } + + if (typeof e.message == "string" + && /^[A-Z_]+_ERR$/.test(e.message)) { + // Opera + return e.message; + } + + if (typeof e.message == "string" + && /^DOM Exception:/.test(e.message)) { + // IE + return /[A-Z_]+_ERR/.exec(e.message)[0]; + } + + throw e; +} + +/** + * Given an array of endpoint data [start container, start offset, end + * container, end offset], returns a Range with those endpoints. + */ +function rangeFromEndpoints(endpoints) { + // If we just use document instead of the ownerDocument of endpoints[0], + // WebKit will throw on setStart/setEnd. This is a WebKit bug, but it's in + // range, not selection, so we don't want to fail anything for it. + var range = ownerDocument(endpoints[0]).createRange(); + range.setStart(endpoints[0], endpoints[1]); + range.setEnd(endpoints[2], endpoints[3]); + return range; +} + +/** + * Given an array of endpoint data [start container, start offset, end + * container, end offset], sets the selection to have those endpoints. Uses + * addRange, so the range will be forwards. Accepts an empty array for + * endpoints, in which case the selection will just be emptied. + */ +function setSelectionForwards(endpoints) { + selection.removeAllRanges(); + if (endpoints.length) { + selection.addRange(rangeFromEndpoints(endpoints)); + } +} + +/** + * Given an array of endpoint data [start container, start offset, end + * container, end offset], sets the selection to have those endpoints, with the + * direction backwards. Uses extend, so it will throw in IE. Accepts an empty + * array for endpoints, in which case the selection will just be emptied. + */ +function setSelectionBackwards(endpoints) { + selection.removeAllRanges(); + if (endpoints.length) { + selection.collapse(endpoints[2], endpoints[3]); + selection.extend(endpoints[0], endpoints[1]); + } +} diff --git a/testing/web-platform/tests/selection/deleteFromDocument.html b/testing/web-platform/tests/selection/deleteFromDocument.html new file mode 100644 index 000000000..b7e036009 --- /dev/null +++ b/testing/web-platform/tests/selection/deleteFromDocument.html @@ -0,0 +1,97 @@ +<!doctype html> +<title>Selection.deleteFromDocument() tests</title> +<link rel=author title="Aryeh Gregor" href=ayg@aryeh.name> +<p>To debug test failures, add a query parameter with the test id (like +"?5"). 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"; + +// We need to use explicit_done, because in Chrome 16 dev and Opera 12.00, the +// second iframe doesn't block the load event -- even though it is added before +// the load event. +setup({explicit_done: true}); + +// Specified by WebIDL +test(function() { + assert_equals(Selection.prototype.deleteFromDocument.length, 0, + "Selection.prototype.deleteFromDocument.length must equal 0"); +}, "Selection.prototype.deleteFromDocument.length must equal 0"); + +testDiv.parentNode.removeChild(testDiv); + +// Test an empty selection too +testRanges.unshift("empty"); + +var actualIframe = document.createElement("iframe"); + +var expectedIframe = document.createElement("iframe"); + +var referenceDoc = document.implementation.createHTMLDocument(""); +referenceDoc.removeChild(referenceDoc.documentElement); + +actualIframe.onload = function() { + expectedIframe.onload = function() { + for (var i = 0; i < testRanges.length; i++) { + if (location.search && i != Number(location.search)) { + continue; + } + + test(function() { + initializeIframe(actualIframe, testRanges[i]); + initializeIframe(expectedIframe, testRanges[i]); + + var actualRange = actualIframe.contentWindow.testRange; + var expectedRange = expectedIframe.contentWindow.testRange; + + assert_equals(actualIframe.contentWindow.unexpectedException, null, + "Unexpected exception thrown when setting up Range for actual deleteFromDocument"); + assert_equals(expectedIframe.contentWindow.unexpectedException, null, + "Unexpected exception thrown when setting up Range for simulated deleteFromDocument"); + + actualIframe.contentWindow.getSelection().removeAllRanges(); + if (testRanges[i] != "empty") { + assert_equals(typeof actualRange, "object", + "Range produced in actual iframe must be an object"); + assert_equals(typeof expectedRange, "object", + "Range produced in expected iframe must be an object"); + assert_true(actualRange instanceof actualIframe.contentWindow.Range, + "Range produced in actual iframe must be instanceof Range"); + assert_true(expectedRange instanceof expectedIframe.contentWindow.Range, + "Range produced in expected iframe must be instanceof Range"); + actualIframe.contentWindow.getSelection().addRange(actualIframe.contentWindow.testRange); + expectedIframe.contentWindow.testRange.deleteContents(); + } + actualIframe.contentWindow.getSelection().deleteFromDocument(); + + assertNodesEqual(actualIframe.contentDocument, expectedIframe.contentDocument, "DOM contents"); + }, "Range " + i + ": " + testRanges[i]); + } + actualIframe.style.display = "none"; + expectedIframe.style.display = "none"; + done(); + }; + expectedIframe.src = "test-iframe.html"; + document.body.appendChild(expectedIframe); + referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true)); +}; +actualIframe.src = "test-iframe.html"; +document.body.appendChild(actualIframe); + +function initializeIframe(iframe, endpoints) { + while (iframe.contentDocument.firstChild) { + iframe.contentDocument.removeChild(iframe.contentDocument.lastChild); + } + iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", "")); + iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true)); + iframe.contentWindow.setupRangeTests(); + if (endpoints != "empty") { + iframe.contentWindow.testRangeInput = endpoints; + iframe.contentWindow.run(); + } +} +</script> diff --git a/testing/web-platform/tests/selection/dir-manual.html b/testing/web-platform/tests/selection/dir-manual.html new file mode 100644 index 000000000..39cf65552 --- /dev/null +++ b/testing/web-platform/tests/selection/dir-manual.html @@ -0,0 +1,106 @@ +<!doctype html> +<title>Selection direction tests</title> +<meta charset=utf-8> +<div id=test> + <p>This is a manual test, since there's no way to synthesize keyboard or + mouse input. Click after the letter "c" in the following paragraph and + drag backwards so that both the "b" and the "c" are highlighted, then click + the "Test" button: + + <p>abcd <button onclick=testDirection()>Test</button> + + <p>efghi +</div> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +setup({explicit_done: true}); + +function testDirection() { + var testDiv = document.getElementById("test"); + var p = testDiv.getElementsByTagName("p")[1].firstChild; + var selection = getSelection(); + var range = selection.getRangeAt(0); + test(function() { + assert_equals(range.toString(), "bc"); + }, "The expected range is selected"); + test(function() { + assert_equals(selection.anchorNode, p); + assert_equals(selection.focusNode, p); + }, "Expected node is initially selected"); + test(function() { + assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [1, 3]); + }, "Expected offsets are initially selected (maybe not in order)"); + test(function() { + assert_equals(selection.anchorOffset, 3); + assert_equals(selection.focusOffset, 1); + }, "Offsets are backwards for initial selection"), + test(function() { + assert_equals(selection.anchorNode, range.endContainer); + assert_equals(selection.anchorOffset, range.endOffset); + assert_equals(selection.focusNode, range.startContainer); + assert_equals(selection.focusOffset, range.startOffset); + }, "Offsets match the range for initial selection"); + + // Per spec, the direction of the selection remains even if you zap a range + // and add a new one. + test(function() { + selection.removeRange(range); + range = document.createRange(); + p = testDiv.getElementsByTagName("p")[0].firstChild; + range.setStart(p, 0); + range.setEnd(p, 4); + assert_equals(range.toString(), "This"); + selection.addRange(range); + }, "removeRange()/addRange() successful"); + test(function() { + assert_equals(selection.anchorNode, p); + assert_equals(selection.focusNode, p); + }, "Expected node is selected after remove/addRange()"); + test(function() { + assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [0, 4]); + }, "Expected offsets are selected after remove/addRange() (maybe not in order)"); + test(function() { + assert_equals(selection.anchorOffset, 4); + assert_equals(selection.focusOffset, 0); + }, "Offsets are backwards after remove/addRange()"), + test(function() { + assert_equals(selection.anchorNode, range.endContainer); + assert_equals(selection.anchorOffset, range.endOffset); + assert_equals(selection.focusNode, range.startContainer); + assert_equals(selection.focusOffset, range.startOffset); + }, "Offsets match the range after remove/addRange()"); + + // But if you call removeAllRanges(), the direction should reset to + // forwards. + test(function() { + selection.removeAllRanges(); + range = document.createRange(); + p = testDiv.getElementsByTagName("p")[2].firstChild; + range.setStart(p, 2); + range.setEnd(p, 5); + assert_equals(range.toString(), "ghi"); + selection.addRange(range); + }, "removeAllRanges() successful"); + test(function() { + assert_equals(selection.anchorNode, p); + assert_equals(selection.focusNode, p); + }, "Expected node is selected after removeAllRanges()"); + test(function() { + assert_array_equals([selection.anchorOffset, selection.focusOffset].sort(), [2, 5]); + }, "Expected offsets are selected after removeAllRanges() (maybe not in order)"); + test(function() { + assert_equals(selection.anchorOffset, 2); + assert_equals(selection.focusOffset, 5); + }, "Offsets are forwards after removeAllRanges()"); + test(function() { + assert_equals(selection.anchorNode, range.startContainer); + assert_equals(selection.anchorOffset, range.startOffset); + assert_equals(selection.focusNode, range.endContainer); + assert_equals(selection.focusOffset, range.endOffset); + }, "Offsets match the range after removeAllRanges()"); + + done(); +} +</script> diff --git a/testing/web-platform/tests/selection/extend.html b/testing/web-platform/tests/selection/extend.html new file mode 100644 index 000000000..e00f8c401 --- /dev/null +++ b/testing/web-platform/tests/selection/extend.html @@ -0,0 +1,149 @@ +<!doctype html> +<title>Selection extend() tests</title> +<meta charset=utf-8> +<meta name=timeout content=long> +<body> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=common.js></script> +<div id=log></div> +<script> +"use strict"; + +// Also test a selection with no ranges +testRanges.unshift("[]"); + +/** + * We test Selections that go both forwards and backwards here. In the latter + * case we need to use extend() to force it to go backwards, which is fair + * enough, since that's what we're testing. We test collapsed selections only + * once. + */ +for (var i = 0; i < testRanges.length; i++) { + var endpoints = eval(testRanges[i]); + for (var j = 0; j < testPoints.length; j++) { + if (endpoints[0] == endpoints[2] + && endpoints[1] == endpoints[3]) { + // Test collapsed selections only once + test(function() { + setSelectionForwards(endpoints); + testExtend(endpoints, eval(testPoints[j])); + }, "extend() with range " + i + " " + testRanges[i] + + " and point " + j + " " + testPoints[j]); + } else { + test(function() { + setSelectionForwards(endpoints); + testExtend(endpoints, eval(testPoints[j])); + }, "extend() forwards with range " + i + " " + testRanges[i] + + " and point " + j + " " + testPoints[j]); + + test(function() { + setSelectionBackwards(endpoints); + testExtend(endpoints, eval(testPoints[j])); + }, "extend() backwards with range " + i + " " + testRanges[i] + + " and point " + j + " " + testPoints[j]); + } + } +} + +function testExtend(endpoints, target) { + assert_equals(getSelection().rangeCount, endpoints.length/4, + "Sanity check: rangeCount must be correct"); + + var node = target[0]; + var offset = target[1]; + + // "If the context object's range is null, throw an InvalidStateError + // exception and abort these steps." + if (getSelection().rangeCount == 0) { + assert_throws("INVALID_STATE_ERR", function() { + selection.extend(node, offset); + }, "extend() when rangeCount is 0 must throw InvalidStateError"); + return; + } + + assert_equals(getSelection().getRangeAt(0).startContainer, endpoints[0], + "Sanity check: startContainer must be correct"); + assert_equals(getSelection().getRangeAt(0).startOffset, endpoints[1], + "Sanity check: startOffset must be correct"); + assert_equals(getSelection().getRangeAt(0).endContainer, endpoints[2], + "Sanity check: endContainer must be correct"); + assert_equals(getSelection().getRangeAt(0).endOffset, endpoints[3], + "Sanity check: endOffset must be correct"); + + // "Let anchor and focus be the context object's anchor and focus, and let + // new focus be the boundary point (node, offset)." + var anchorNode = getSelection().anchorNode; + var anchorOffset = getSelection().anchorOffset; + var focusNode = getSelection().focusNode; + var focusOffset = getSelection().focusOffset; + + // "Let new range be a new range." + // + // We'll always be setting either new range's start or its end to new + // focus, so we'll always throw at some point. Test that now. + // + // From DOM4's "set the start or end of a range": "If node is a doctype, + // throw an "InvalidNodeTypeError" exception and terminate these steps." + if (node.nodeType == Node.DOCUMENT_TYPE_NODE) { + assert_throws("INVALID_NODE_TYPE_ERR", function() { + selection.extend(node, offset); + }, "extend() to a doctype must throw InvalidNodeTypeError"); + return; + } + + // From DOM4's "set the start or end of a range": "If offset is greater + // than node's length, throw an "IndexSizeError" exception and terminate + // these steps." + // + // FIXME: We should be casting offset to an unsigned int per WebIDL. Until + // we do, we need the offset < 0 check too. + if (offset < 0 || offset > getNodeLength(node)) { + assert_throws("INDEX_SIZE_ERR", function() { + selection.extend(node, offset); + }, "extend() to an offset that's greater than node length (" + getNodeLength(node) + ") must throw IndexSizeError"); + return; + } + + // Now back to the editing spec. + var originalRange = getSelection().getRangeAt(0); + + // "If node's root is not the same as the context object's range's root, + // set new range's start and end to (node, offset)." + // + // "Otherwise, if anchor is before or equal to new focus, set new range's + // start to anchor, then set its end to new focus." + // + // "Otherwise, set new range's start to new focus, then set its end to + // anchor." + // + // "Set the context object's range to new range." + // + // "If new focus is before anchor, set the context object's direction to + // backwards. Otherwise, set it to forwards." + // + // The upshot of all these is summed up by just testing the anchor and + // offset. + getSelection().extend(node, offset); + + if (furthestAncestor(anchorNode) == furthestAncestor(node)) { + assert_equals(getSelection().anchorNode, anchorNode, + "anchorNode must not change if the node passed to extend() has the same root as the original range"); + assert_equals(getSelection().anchorOffset, anchorOffset, + "anchorOffset must not change if the node passed to extend() has the same root as the original range"); + } else { + assert_equals(getSelection().anchorNode, node, + "anchorNode must be the node passed to extend() if it has a different root from the original range"); + assert_equals(getSelection().anchorOffset, offset, + "anchorOffset must be the offset passed to extend() if the node has a different root from the original range"); + } + assert_equals(getSelection().focusNode, node, + "focusNode must be the node passed to extend()"); + assert_equals(getSelection().focusOffset, offset, + "focusOffset must be the offset passed to extend()"); + assert_not_equals(getSelection().getRangeAt(0), originalRange, + "extend() must replace any existing range with a new one, not mutate the existing one"); +} + +testDiv.style.display = "none"; +</script> diff --git a/testing/web-platform/tests/selection/getRangeAt.html b/testing/web-platform/tests/selection/getRangeAt.html new file mode 100644 index 000000000..3c6d7976f --- /dev/null +++ b/testing/web-platform/tests/selection/getRangeAt.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<title>The getRangeAt method</title> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +test(function() { + var sel = getSelection(); + var range = document.createRange(); + sel.addRange(range); + assert_throws("INDEX_SIZE_ERR", function() { sel.getRangeAt(-1); }) + assert_throws("INDEX_SIZE_ERR", function() { sel.getRangeAt(1); }) +}); +</script> diff --git a/testing/web-platform/tests/selection/getSelection.html b/testing/web-platform/tests/selection/getSelection.html new file mode 100644 index 000000000..ea119f2fc --- /dev/null +++ b/testing/web-platform/tests/selection/getSelection.html @@ -0,0 +1,160 @@ +<!doctype html> +<title>getSelection() tests</title> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +"use strict"; + +// TODO: Figure out more places where defaultView is or is not guaranteed to be +// null, and test whether getSelection() is null. +// +// TODO: Figure out a good way to test display: none iframes. + +test(function() { + // Sanity checks like this are to flag known browser bugs with clearer + // error messages, instead of throwing inscrutable exceptions. + assert_true("Selection" in window, + "Sanity check: window must have Selection property"); + + assert_true(window.getSelection() instanceof Selection); +}, "window.getSelection() instanceof Selection"); + +test(function() { + assert_equals(window.getSelection(), window.getSelection()); +}, "window.getSelection() === window.getSelection()"); + +test(function() { + assert_true("Selection" in window, + "Sanity check: window must have Selection property"); + // This sanity check (which occurs a number of times below, too) is because + // document.getSelection() is supposed to return null if defaultView is + // null, so we need to figure out whether defaultView is null or not before + // we can make correct assertions about getSelection(). + assert_not_equals(document.defaultView, null, + "Sanity check: document.defaultView must not be null"); + + assert_equals(typeof document.getSelection(), "object", + "document.getSelection() must be an object"); + assert_true(document.getSelection() instanceof Selection); +}, "document.getSelection() instanceof Selection"); + +test(function() { + assert_not_equals(document.defaultView, null, + "Sanity check: document.defaultView must not be null"); + assert_equals(document.getSelection(), document.getSelection()); +}, "document.getSelection() === document.getSelection()"); + +test(function() { + assert_not_equals(document.defaultView, null, + "Sanity check: document.defaultView must not be null"); + assert_equals(window.getSelection(), document.getSelection()); +}, "window.getSelection() === document.getSelection()"); + +// "Each selection is associated with a single range, which may be null and is +// initially null." +// +// "The rangeCount attribute must return 0 if the context object's range is +// null, otherwise 1." +test(function() { + assert_equals(window.getSelection().rangeCount, 0, + "window.getSelection().rangeCount must initially be 0"); + assert_equals(typeof document.getSelection(), "object", + "Sanity check: document.getSelection() must be an object"); + assert_equals(document.getSelection().rangeCount, 0, + "document.getSelection().rangeCount must initially be 0"); +}, "Selection's range must initially be null"); + +test(function() { + var doc = document.implementation.createHTMLDocument(""); + assert_equals(doc.defaultView, null, + "Sanity check: defaultView of created HTML document must be null"); + assert_equals(doc.getSelection(), null); +}, "getSelection() on HTML document with null defaultView must be null"); + +test(function() { + var xmlDoc = document.implementation.createDocument(null, "", null); + + assert_true("getSelection" in xmlDoc, "XML document must have getSelection()"); + + assert_equals(xmlDoc.defaultView, null, + "Sanity check: defaultView of created XML document must be null"); + assert_equals(xmlDoc.getSelection(), null); +}, "getSelection() on XML document with null defaultView must be null"); + + +// Run a bunch of iframe tests, once immediately after the iframe is appended +// to the document and once onload. This makes a difference, because browsers +// differ (at the time of this writing) in whether they load about:blank in +// iframes synchronously or not. Per the HTML spec, there must be a browsing +// context associated with the iframe as soon as it's appended to the document, +// so there should be a selection too. +var iframe = document.createElement("iframe"); +add_completion_callback(function() { + document.body.removeChild(iframe); +}); + +var testDescs = []; +var testFuncs = []; +testDescs.push("window.getSelection() instanceof Selection in an iframe"); +testFuncs.push(function() { + assert_true("Selection" in iframe.contentWindow, + "Sanity check: window must have Selection property"); + assert_not_equals(iframe.contentWindow.document.defaultView, null, + "Sanity check: document.defaultView must not be null"); + assert_not_equals(iframe.contentWindow.getSelection(), null, + "window.getSelection() must not be null"); + assert_true(iframe.contentWindow.getSelection() instanceof iframe.contentWindow.Selection); +}); + +testDescs.push("document.getSelection() instanceof Selection in an iframe"); +testFuncs.push(function() { + assert_true("Selection" in iframe.contentWindow, + "Sanity check: window must have Selection property"); + assert_not_equals(iframe.contentDocument.defaultView, null, + "Sanity check: document.defaultView must not be null"); + assert_not_equals(iframe.contentDocument.getSelection(), null, + "document.getSelection() must not be null"); + assert_equals(typeof iframe.contentDocument.getSelection(), "object", + "document.getSelection() must be an object"); + assert_true(iframe.contentDocument.getSelection() instanceof iframe.contentWindow.Selection); +}); + +testDescs.push("window.getSelection() === document.getSelection() in an iframe"); +testFuncs.push(function() { + assert_not_equals(iframe.contentDocument.defaultView, null, + "Sanity check: document.defaultView must not be null"); + assert_equals(iframe.contentWindow.getSelection(), iframe.contentDocument.getSelection()); +}); + +testDescs.push("getSelection() inside and outside iframe must return different objects"); +testFuncs.push(function() { + assert_not_equals(iframe.contentWindow.getSelection(), getSelection()); +}); + +testDescs.push("getSelection() on HTML document with null defaultView must be null inside an iframe"); +testFuncs.push(function() { + var doc = iframe.contentDocument.implementation.createHTMLDocument(""); + assert_equals(doc.defaultView, null, + "Sanity check: defaultView of created HTML document must be null"); + assert_equals(doc.getSelection(), null); +}); + +var asyncTests = []; +testDescs.forEach(function(desc) { + asyncTests.push(async_test(desc + " onload")); +}); + +iframe.onload = function() { + asyncTests.forEach(function(t, i) { + t.step(testFuncs[i]); + t.done(); + }); +}; + +document.body.appendChild(iframe); + +testDescs.forEach(function(desc, i) { + test(testFuncs[i], desc + " immediately after appendChild"); +}); +</script> diff --git a/testing/web-platform/tests/selection/interfaces.html b/testing/web-platform/tests/selection/interfaces.html new file mode 100644 index 000000000..7174686a4 --- /dev/null +++ b/testing/web-platform/tests/selection/interfaces.html @@ -0,0 +1,41 @@ +<!doctype html> +<title>Selection interface tests</title> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/resources/WebIDLParser.js></script> +<script src=/resources/idlharness.js></script> +<script type=text/plain> +interface Selection { + readonly attribute Node? anchorNode; + readonly attribute unsigned long anchorOffset; + readonly attribute Node? focusNode; + readonly attribute unsigned long focusOffset; + + readonly attribute boolean isCollapsed; + void collapse(Node node, unsigned long offset); + void collapseToStart(); + void collapseToEnd(); + + void extend(Node node, unsigned long offset); + + void selectAllChildren(Node node); + void deleteFromDocument(); + + readonly attribute unsigned long rangeCount; + Range getRangeAt(unsigned long index); + void addRange(Range range); + void removeRange(Range range); + void removeAllRanges(); + + stringifier; +}; +</script> +<script> +"use strict"; + +var idlArray = new IdlArray(); +idlArray.add_idls(document.querySelector("script[type=text\\/plain]").textContent); +idlArray.add_objects({Selection: ['getSelection()']}); +idlArray.test(); +</script> diff --git a/testing/web-platform/tests/selection/isCollapsed.html b/testing/web-platform/tests/selection/isCollapsed.html new file mode 100644 index 000000000..113a16db8 --- /dev/null +++ b/testing/web-platform/tests/selection/isCollapsed.html @@ -0,0 +1,31 @@ +<!doctype html> +<title>Selection.isCollapsed tests</title> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=common.js></script> +<script> +"use strict"; + +test(function() { + selection.removeAllRanges(); + assert_true(selection.isCollapsed, "isCollapsed must be true if both anchor and focus are null"); +}, "Empty selection"); + +for (var i = 0; i < testRanges.length; i++) { + test(function() { + selection.removeAllRanges(); + var endpoints = eval(testRanges[i]); + var range = ownerDocument(endpoints[0]).createRange(); + range.setStart(endpoints[0], endpoints[1]); + range.setEnd(endpoints[2], endpoints[3]); + selection.addRange(range); + + assert_equals(selection.isCollapsed, + endpoints[0] === endpoints[2] && endpoints[1] === endpoints[3], + "Value of isCollapsed"); + }, "Range " + i + " " + testRanges[i]); +} + +testDiv.style.display = "none"; +</script> diff --git a/testing/web-platform/tests/selection/removeAllRanges.html b/testing/web-platform/tests/selection/removeAllRanges.html new file mode 100644 index 000000000..cc6b69256 --- /dev/null +++ b/testing/web-platform/tests/selection/removeAllRanges.html @@ -0,0 +1,45 @@ +<!doctype html> +<title>Selection.removeAllRanges() tests</title> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=common.js></script> +<script> +"use strict"; + +// Also test a selection with no ranges +testRanges.unshift("[]"); + +var range = rangeFromEndpoints([paras[0].firstChild, 0, paras[0].firstChild, 1]); + +for (var i = 0; i < testRanges.length; i++) { + test(function() { + setSelectionForwards(eval(testRanges[i])); + selection.removeAllRanges(); + assert_equals(selection.rangeCount, 0, + "After removeAllRanges(), rangeCount must be 0"); + // Test that it's forwards + selection.addRange(range); + assert_equals(selection.anchorOffset, selection.getRangeAt(0).startOffset, + "After removeAllRanges(), addRange() must be forwards, so anchorOffset must equal startOffset rather than endOffset"); + assert_equals(selection.focusOffset, selection.getRangeAt(0).endOffset, + "After removeAllRanges(), addRange() must be forwards, so focusOffset must equal endOffset rather than startOffset"); + }, "Range " + i + " " + testRanges[i] + " forwards"); + + // Copy-pasted from above + test(function() { + setSelectionBackwards(eval(testRanges[i])); + selection.removeAllRanges(); + assert_equals(selection.rangeCount, 0, + "After removeAllRanges(), rangeCount must be 0"); + // Test that it's forwards + selection.addRange(range); + assert_equals(selection.anchorOffset, selection.getRangeAt(0).startOffset, + "After removeAllRanges(), addRange() must be forwards, so anchorOffset must equal startOffset rather than endOffset"); + assert_equals(selection.focusOffset, selection.getRangeAt(0).endOffset, + "After removeAllRanges(), addRange() must be forwards, so focusOffset must equal endOffset rather than startOffset"); + }, "Range " + i + " " + testRanges[i] + " backwards"); +} + +testDiv.style.display = "none"; +</script> diff --git a/testing/web-platform/tests/selection/selectAllChildren.html b/testing/web-platform/tests/selection/selectAllChildren.html new file mode 100644 index 000000000..904e0cb2b --- /dev/null +++ b/testing/web-platform/tests/selection/selectAllChildren.html @@ -0,0 +1,53 @@ +<!doctype html> +<title>Selection.selectAllChildren tests</title> +<div id=log></div> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=common.js></script> +<script> +"use strict"; + +testRanges.unshift("[]"); + +for (var i = 0; i < testRanges.length; i++) { + var endpoints = eval(testRanges[i]); + + for (var j = 0; j < testNodes.length; j++) { + var node = eval(testNodes[j]); + + test(function() { + setSelectionForwards(endpoints); + var originalRange = getSelection().rangeCount + ? getSelection().getRangeAt(0) + : null; + + if (node.nodeType == Node.DOCUMENT_TYPE_NODE) { + assert_throws("INVALID_NODE_TYPE_ERR", function() { + selection.selectAllChildren(node); + }, "selectAllChildren() on a DocumentType must throw InvalidNodeTypeError"); + return; + } + + selection.selectAllChildren(node); + // This implicitly tests that the selection is forwards, by using + // anchorOffset/focusOffset instead of getRangeAt. + assert_equals(selection.rangeCount, 1, + "After selectAllChildren, rangeCount must be 1"); + assert_equals(selection.anchorNode, node, + "After selectAllChildren, anchorNode must be the given node"); + assert_equals(selection.anchorOffset, 0, + "After selectAllChildren, anchorOffset must be 0"); + assert_equals(selection.focusNode, node, + "After selectAllChildren, focusNode must be the given node"); + assert_equals(selection.focusOffset, node.childNodes.length, + "After selectAllChildren, focusOffset must be the given node's number of children"); + if (originalRange) { + assert_not_equals(getSelection().getRangeAt(0), originalRange, + "selectAllChildren must replace any existing range, not mutate it"); + } + }, "Range " + i + " " + testRanges[i] + ", node " + j + " " + testNodes[j]); + } +} + +testDiv.style.display = "none"; +</script> diff --git a/testing/web-platform/tests/selection/test-iframe.html b/testing/web-platform/tests/selection/test-iframe.html new file mode 100644 index 000000000..42b982324 --- /dev/null +++ b/testing/web-platform/tests/selection/test-iframe.html @@ -0,0 +1,33 @@ +<!doctype html> +<title>Selection test iframe</title> +<link rel=author title="Aryeh Gregor" href=ayg@aryeh.name> +<body> +<script src=common.js></script> +<script> +"use strict"; + +// This script only exists because we want to evaluate the range endpoints +// in each iframe using that iframe's local variables set up by common.js. It +// just creates a range with the endpoints given by +// eval(window.testRangeInput), and assigns the result to window.testRange. If +// there's an exception, it's assigned to window.unexpectedException. +// Everything else is to be done by the script that created the iframe. +window.unexpectedException = null; + +function run() { + window.unexpectedException = null; + try { + window.testRange = rangeFromEndpoints(eval(window.testRangeInput)); + } catch(e) { + window.unexpectedException = e; + } +} + +// Remove the scripts so they don't run repeatedly when the iframe is +// reinitialized +[].forEach.call(document.querySelectorAll("script"), function(script) { + script.parentNode.removeChild(script); +}); + +testDiv.style.display = "none"; +</script> |