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