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: }