summaryrefslogtreecommitdiffstats
path: root/dom/base/test/unit/test_range.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/base/test/unit/test_range.js')
-rw-r--r--dom/base/test/unit/test_range.js465
1 files changed, 465 insertions, 0 deletions
diff --git a/dom/base/test/unit/test_range.js b/dom/base/test/unit/test_range.js
new file mode 100644
index 0000000000..8b9f5c0b8b
--- /dev/null
+++ b/dom/base/test/unit/test_range.js
@@ -0,0 +1,465 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const UNORDERED_TYPE = 8; // XPathResult.ANY_UNORDERED_NODE_TYPE
+
+/**
+ * Determine if the data node has only ignorable white-space.
+ *
+ * @return NodeFilter.FILTER_SKIP if it does.
+ * @return NodeFilter.FILTER_ACCEPT otherwise.
+ */
+function isWhitespace(aNode) {
+ return /\S/.test(aNode.nodeValue)
+ ? NodeFilter.FILTER_SKIP
+ : NodeFilter.FILTER_ACCEPT;
+}
+
+/**
+ * Create a DocumentFragment with cloned children equaling a node's children.
+ *
+ * @param aNode The node to copy from.
+ *
+ * @return DocumentFragment node.
+ */
+function getFragment(aNode) {
+ var frag = aNode.ownerDocument.createDocumentFragment();
+ for (var i = 0; i < aNode.childNodes.length; i++) {
+ frag.appendChild(aNode.childNodes.item(i).cloneNode(true));
+ }
+ return frag;
+}
+
+// Goodies from head_content.js
+const parser = getParser();
+
+/**
+ * Translate an XPath to a DOM node. This method uses a document
+ * fragment as context node.
+ *
+ * @param aContextNode The context node to apply the XPath to.
+ * @param aPath The XPath to use.
+ *
+ * @return Node The target node retrieved from the XPath.
+ */
+function evalXPathInDocumentFragment(aContextNode, aPath) {
+ Assert.equal(ChromeUtils.getClassName(aContextNode), "DocumentFragment");
+ Assert.ok(aContextNode.childNodes.length);
+ if (aPath == ".") {
+ return aContextNode;
+ }
+
+ // Separate the fragment's xpath lookup from the rest.
+ var firstSlash = aPath.indexOf("/");
+ if (firstSlash == -1) {
+ firstSlash = aPath.length;
+ }
+ var prefix = aPath.substr(0, firstSlash);
+ var realPath = aPath.substr(firstSlash + 1);
+ if (!realPath) {
+ realPath = ".";
+ }
+
+ // Set up a special node filter to look among the fragment's child nodes.
+ var childIndex = 1;
+ var bracketIndex = prefix.indexOf("[");
+ if (bracketIndex != -1) {
+ childIndex = Number(
+ prefix.substring(bracketIndex + 1, prefix.indexOf("]"))
+ );
+ Assert.ok(childIndex > 0);
+ prefix = prefix.substr(0, bracketIndex);
+ }
+
+ var targetType = NodeFilter.SHOW_ELEMENT;
+ var targetNodeName = prefix;
+ if (prefix.indexOf("processing-instruction(") == 0) {
+ targetType = NodeFilter.SHOW_PROCESSING_INSTRUCTION;
+ targetNodeName = prefix.substring(
+ prefix.indexOf("(") + 2,
+ prefix.indexOf(")") - 1
+ );
+ }
+ switch (prefix) {
+ case "text()":
+ targetType = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION;
+ targetNodeName = null;
+ break;
+ case "comment()":
+ targetType = NodeFilter.SHOW_COMMENT;
+ targetNodeName = null;
+ break;
+ case "node()":
+ targetType = NodeFilter.SHOW_ALL;
+ targetNodeName = null;
+ }
+
+ var filter = {
+ count: 0,
+
+ // NodeFilter
+ acceptNode: function acceptNode(aNode) {
+ if (aNode.parentNode != aContextNode) {
+ // Don't bother looking at kids either.
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ if (targetNodeName && targetNodeName != aNode.nodeName) {
+ return NodeFilter.FILTER_SKIP;
+ }
+
+ this.count++;
+ if (this.count != childIndex) {
+ return NodeFilter.FILTER_SKIP;
+ }
+
+ return NodeFilter.FILTER_ACCEPT;
+ },
+ };
+
+ // Look for the node matching the step from the document fragment.
+ var walker = aContextNode.ownerDocument.createTreeWalker(
+ aContextNode,
+ targetType,
+ filter
+ );
+ var targetNode = walker.nextNode();
+ Assert.notEqual(targetNode, null);
+
+ // Apply our remaining xpath to the found node.
+ var expr = aContextNode.ownerDocument.createExpression(realPath, null);
+ var result = expr.evaluate(targetNode, UNORDERED_TYPE, null);
+ return result.singleNodeValue;
+}
+
+/**
+ * Get a DOM range corresponding to the test's source node.
+ *
+ * @param aSourceNode <source/> element with range information.
+ * @param aFragment DocumentFragment generated with getFragment().
+ *
+ * @return Range object.
+ */
+function getRange(aSourceNode, aFragment) {
+ Assert.ok(Element.isInstance(aSourceNode));
+ Assert.equal(ChromeUtils.getClassName(aFragment), "DocumentFragment");
+ var doc = aSourceNode.ownerDocument;
+
+ var containerPath = aSourceNode.getAttribute("startContainer");
+ var startContainer = evalXPathInDocumentFragment(aFragment, containerPath);
+ var startOffset = Number(aSourceNode.getAttribute("startOffset"));
+
+ containerPath = aSourceNode.getAttribute("endContainer");
+ var endContainer = evalXPathInDocumentFragment(aFragment, containerPath);
+ var endOffset = Number(aSourceNode.getAttribute("endOffset"));
+
+ var range = doc.createRange();
+ range.setStart(startContainer, startOffset);
+ range.setEnd(endContainer, endOffset);
+ return range;
+}
+
+/**
+ * Get the document for a given path, and clean it up for our tests.
+ *
+ * @param aPath The path to the local document.
+ */
+function getParsedDocument(aPath) {
+ return do_parse_document(aPath, "application/xml").then(
+ processParsedDocument
+ );
+}
+
+function processParsedDocument(doc) {
+ Assert.ok(doc.documentElement.localName != "parsererror");
+ Assert.equal(ChromeUtils.getClassName(doc), "XMLDocument");
+
+ // Clean out whitespace.
+ var walker = doc.createTreeWalker(
+ doc,
+ NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION,
+ isWhitespace
+ );
+ while (walker.nextNode()) {
+ var parent = walker.currentNode.parentNode;
+ parent.removeChild(walker.currentNode);
+ walker.currentNode = parent;
+ }
+
+ // Clean out mandatory splits between nodes.
+ var splits = doc.getElementsByTagName("split");
+ var i;
+ for (i = splits.length - 1; i >= 0; i--) {
+ let node = splits.item(i);
+ node.remove();
+ }
+ splits = null;
+
+ // Replace empty CDATA sections.
+ var emptyData = doc.getElementsByTagName("empty-cdata");
+ for (i = emptyData.length - 1; i >= 0; i--) {
+ let node = emptyData.item(i);
+ var cdata = doc.createCDATASection("");
+ node.parentNode.replaceChild(cdata, node);
+ }
+
+ return doc;
+}
+
+/**
+ * Run the extraction tests.
+ */
+function run_extract_test() {
+ var filePath = "test_delete_range.xml";
+ getParsedDocument(filePath).then(do_extract_test);
+}
+
+function do_extract_test(doc) {
+ var tests = doc.getElementsByTagName("test");
+
+ // Run our deletion, extraction tests.
+ for (var i = 0; i < tests.length; i++) {
+ dump("Configuring for test " + i + "\n");
+ var currentTest = tests.item(i);
+
+ // Validate the test is properly formatted for what this harness expects.
+ var baseSource = currentTest.firstChild;
+ Assert.equal(baseSource.nodeName, "source");
+ var baseResult = baseSource.nextSibling;
+ Assert.equal(baseResult.nodeName, "result");
+ var baseExtract = baseResult.nextSibling;
+ Assert.equal(baseExtract.nodeName, "extract");
+ Assert.equal(baseExtract.nextSibling, null);
+
+ /* We do all our tests on DOM document fragments, derived from the test
+ element's children. This lets us rip the various fragments to shreds,
+ while preserving the original elements so we can make more copies of
+ them.
+
+ After the range's extraction or deletion is done, we use
+ Node.isEqualNode() between the altered source fragment and the
+ result fragment. We also run isEqualNode() between the extracted
+ fragment and the fragment from the baseExtract node. If they are not
+ equal, we have failed a test.
+
+ We also have to ensure the original nodes on the end points of the
+ range are still in the source fragment. This is bug 332148. The nodes
+ may not be replaced with equal but separate nodes. The range extraction
+ may alter these nodes - in the case of text containers, they will - but
+ the nodes must stay there, to preserve references such as user data,
+ event listeners, etc.
+
+ First, an extraction test.
+ */
+
+ var resultFrag = getFragment(baseResult);
+ var extractFrag = getFragment(baseExtract);
+
+ dump("Extract contents test " + i + "\n\n");
+ var baseFrag = getFragment(baseSource);
+ var baseRange = getRange(baseSource, baseFrag);
+ var startContainer = baseRange.startContainer;
+ var endContainer = baseRange.endContainer;
+
+ var cutFragment = baseRange.extractContents();
+ dump("cutFragment: " + cutFragment + "\n");
+ if (cutFragment) {
+ Assert.ok(extractFrag.isEqualNode(cutFragment));
+ } else {
+ Assert.equal(extractFrag.firstChild, null);
+ }
+ Assert.ok(baseFrag.isEqualNode(resultFrag));
+
+ dump("Ensure the original nodes weren't extracted - test " + i + "\n\n");
+ var walker = doc.createTreeWalker(baseFrag, NodeFilter.SHOW_ALL, null);
+ var foundStart = false;
+ var foundEnd = false;
+ do {
+ if (walker.currentNode == startContainer) {
+ foundStart = true;
+ }
+
+ if (walker.currentNode == endContainer) {
+ // An end container node should not come before the start container node.
+ Assert.ok(foundStart);
+ foundEnd = true;
+ break;
+ }
+ } while (walker.nextNode());
+ Assert.ok(foundEnd);
+
+ /* Now, we reset our test for the deleteContents case. This one differs
+ from the extractContents case only in that there is no extracted document
+ fragment to compare against. So we merely compare the starting fragment,
+ minus the extracted content, against the result fragment.
+ */
+ dump("Delete contents test " + i + "\n\n");
+ baseFrag = getFragment(baseSource);
+ baseRange = getRange(baseSource, baseFrag);
+ startContainer = baseRange.startContainer;
+ endContainer = baseRange.endContainer;
+ baseRange.deleteContents();
+ Assert.ok(baseFrag.isEqualNode(resultFrag));
+
+ dump("Ensure the original nodes weren't deleted - test " + i + "\n\n");
+ walker = doc.createTreeWalker(baseFrag, NodeFilter.SHOW_ALL, null);
+ foundStart = false;
+ foundEnd = false;
+ do {
+ if (walker.currentNode == startContainer) {
+ foundStart = true;
+ }
+
+ if (walker.currentNode == endContainer) {
+ // An end container node should not come before the start container node.
+ Assert.ok(foundStart);
+ foundEnd = true;
+ break;
+ }
+ } while (walker.nextNode());
+ Assert.ok(foundEnd);
+
+ // Clean up after ourselves.
+ walker = null;
+ }
+}
+
+/**
+ * Miscellaneous tests not covered above.
+ */
+function run_miscellaneous_tests() {
+ var filePath = "test_delete_range.xml";
+ getParsedDocument(filePath).then(do_miscellaneous_tests);
+}
+
+function isText(node) {
+ return (
+ node.nodeType == node.TEXT_NODE || node.nodeType == node.CDATA_SECTION_NODE
+ );
+}
+
+function do_miscellaneous_tests(doc) {
+ var tests = doc.getElementsByTagName("test");
+
+ // Let's try some invalid inputs to our DOM range and see what happens.
+ var currentTest = tests.item(0);
+ var baseSource = currentTest.firstChild;
+
+ var baseFrag = getFragment(baseSource);
+
+ var baseRange = getRange(baseSource, baseFrag);
+ var startContainer = baseRange.startContainer;
+ var endContainer = baseRange.endContainer;
+ var startOffset = baseRange.startOffset;
+ var endOffset = baseRange.endOffset;
+
+ // Text range manipulation.
+ if (
+ endOffset > startOffset &&
+ startContainer == endContainer &&
+ isText(startContainer)
+ ) {
+ // Invalid start node
+ try {
+ baseRange.setStart(null, 0);
+ do_throw("Should have thrown NOT_OBJECT_ERR!");
+ } catch (e) {
+ Assert.equal(e.constructor.name, "TypeError");
+ }
+
+ // Invalid start node
+ try {
+ baseRange.setStart({}, 0);
+ do_throw("Should have thrown SecurityError!");
+ } catch (e) {
+ Assert.equal(e.constructor.name, "TypeError");
+ }
+
+ // Invalid index
+ try {
+ baseRange.setStart(startContainer, -1);
+ do_throw("Should have thrown IndexSizeError!");
+ } catch (e) {
+ Assert.equal(e.name, "IndexSizeError");
+ }
+
+ // Invalid index
+ var newOffset = isText(startContainer)
+ ? startContainer.nodeValue.length + 1
+ : startContainer.childNodes.length + 1;
+ try {
+ baseRange.setStart(startContainer, newOffset);
+ do_throw("Should have thrown IndexSizeError!");
+ } catch (e) {
+ Assert.equal(e.name, "IndexSizeError");
+ }
+
+ newOffset--;
+ // Valid index
+ baseRange.setStart(startContainer, newOffset);
+ Assert.equal(baseRange.startContainer, baseRange.endContainer);
+ Assert.equal(baseRange.startOffset, newOffset);
+ Assert.ok(baseRange.collapsed);
+
+ // Valid index
+ baseRange.setEnd(startContainer, 0);
+ Assert.equal(baseRange.startContainer, baseRange.endContainer);
+ Assert.equal(baseRange.startOffset, 0);
+ Assert.ok(baseRange.collapsed);
+ } else {
+ do_throw(
+ "The first test should be a text-only range test. Test is invalid."
+ );
+ }
+
+ /* See what happens when a range has a startContainer in one fragment, and an
+ endContainer in another. According to the DOM spec, section 2.4, the range
+ should collapse to the new container and offset. */
+ baseRange = getRange(baseSource, baseFrag);
+ startContainer = baseRange.startContainer;
+ startOffset = baseRange.startOffset;
+ endContainer = baseRange.endContainer;
+ endOffset = baseRange.endOffset;
+
+ dump("External fragment test\n\n");
+
+ var externalTest = tests.item(1);
+ var externalSource = externalTest.firstChild;
+ var externalFrag = getFragment(externalSource);
+ var externalRange = getRange(externalSource, externalFrag);
+
+ baseRange.setEnd(externalRange.endContainer, 0);
+ Assert.equal(baseRange.startContainer, externalRange.endContainer);
+ Assert.equal(baseRange.startOffset, 0);
+ Assert.ok(baseRange.collapsed);
+
+ /*
+ // XXX ajvincent if rv == WRONG_DOCUMENT_ERR, return false?
+ do_check_false(baseRange.isPointInRange(startContainer, startOffset));
+ do_check_false(baseRange.isPointInRange(startContainer, startOffset + 1));
+ do_check_false(baseRange.isPointInRange(endContainer, endOffset));
+ */
+
+ // Requested by smaug: A range involving a comment as a document child.
+ doc = parser.parseFromString("<!-- foo --><foo/>", "application/xml");
+ Assert.equal(ChromeUtils.getClassName(doc), "XMLDocument");
+ Assert.equal(doc.childNodes.length, 2);
+ baseRange = doc.createRange();
+ baseRange.setStart(doc.firstChild, 1);
+ baseRange.setEnd(doc.firstChild, 2);
+ var frag = baseRange.extractContents();
+ Assert.equal(frag.childNodes.length, 1);
+ Assert.ok(ChromeUtils.getClassName(frag.firstChild) == "Comment");
+ Assert.equal(frag.firstChild.nodeType, frag.COMMENT_NODE);
+ Assert.equal(frag.firstChild.nodeValue, "f");
+
+ /* smaug also requested attribute tests. Sadly, those are not yet supported
+ in ranges - see https://bugzilla.mozilla.org/show_bug.cgi?id=302775.
+ */
+}
+
+function run_test() {
+ run_extract_test();
+ run_miscellaneous_tests();
+}