1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/content/test/unit/test_range.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,457 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const C_i = Components.interfaces; 1.9 + 1.10 +const UNORDERED_TYPE = C_i.nsIDOMXPathResult.ANY_UNORDERED_NODE_TYPE; 1.11 + 1.12 +/** 1.13 + * Determine if the data node has only ignorable white-space. 1.14 + * 1.15 + * @return nsIDOMNodeFilter.FILTER_SKIP if it does. 1.16 + * @return nsIDOMNodeFilter.FILTER_ACCEPT otherwise. 1.17 + */ 1.18 +function isWhitespace(aNode) { 1.19 + return ((/\S/).test(aNode.nodeValue)) ? 1.20 + C_i.nsIDOMNodeFilter.FILTER_SKIP : 1.21 + C_i.nsIDOMNodeFilter.FILTER_ACCEPT; 1.22 +} 1.23 + 1.24 +/** 1.25 + * Create a DocumentFragment with cloned children equaling a node's children. 1.26 + * 1.27 + * @param aNode The node to copy from. 1.28 + * 1.29 + * @return DocumentFragment node. 1.30 + */ 1.31 +function getFragment(aNode) { 1.32 + var frag = aNode.ownerDocument.createDocumentFragment(); 1.33 + for (var i = 0; i < aNode.childNodes.length; i++) { 1.34 + frag.appendChild(aNode.childNodes.item(i).cloneNode(true)); 1.35 + } 1.36 + return frag; 1.37 +} 1.38 + 1.39 +// Goodies from head_content.js 1.40 +const serializer = new DOMSerializer(); 1.41 +const parser = new DOMParser(); 1.42 + 1.43 +/** 1.44 + * Dump the contents of a document fragment to the console. 1.45 + * 1.46 + * @param aFragment The fragment to serialize. 1.47 + */ 1.48 +function dumpFragment(aFragment) { 1.49 + dump(serializer.serializeToString(aFragment) + "\n\n"); 1.50 +} 1.51 + 1.52 +/** 1.53 + * Translate an XPath to a DOM node. This method uses a document 1.54 + * fragment as context node. 1.55 + * 1.56 + * @param aContextNode The context node to apply the XPath to. 1.57 + * @param aPath The XPath to use. 1.58 + * 1.59 + * @return nsIDOMNode The target node retrieved from the XPath. 1.60 + */ 1.61 +function evalXPathInDocumentFragment(aContextNode, aPath) { 1.62 + do_check_true(aContextNode instanceof C_i.nsIDOMDocumentFragment); 1.63 + do_check_true(aContextNode.childNodes.length > 0); 1.64 + if (aPath == ".") { 1.65 + return aContextNode; 1.66 + } 1.67 + 1.68 + // Separate the fragment's xpath lookup from the rest. 1.69 + var firstSlash = aPath.indexOf("/"); 1.70 + if (firstSlash == -1) { 1.71 + firstSlash = aPath.length; 1.72 + } 1.73 + var prefix = aPath.substr(0, firstSlash); 1.74 + var realPath = aPath.substr(firstSlash + 1); 1.75 + if (!realPath) { 1.76 + realPath = "."; 1.77 + } 1.78 + 1.79 + // Set up a special node filter to look among the fragment's child nodes. 1.80 + var childIndex = 1; 1.81 + var bracketIndex = prefix.indexOf("["); 1.82 + if (bracketIndex != -1) { 1.83 + childIndex = Number(prefix.substring(bracketIndex + 1, prefix.indexOf("]"))); 1.84 + do_check_true(childIndex > 0); 1.85 + prefix = prefix.substr(0, bracketIndex); 1.86 + } 1.87 + 1.88 + var targetType = C_i.nsIDOMNodeFilter.SHOW_ELEMENT; 1.89 + var targetNodeName = prefix; 1.90 + if (prefix.indexOf("processing-instruction(") == 0) { 1.91 + targetType = C_i.nsIDOMNodeFilter.SHOW_PROCESSING_INSTRUCTION; 1.92 + targetNodeName = prefix.substring(prefix.indexOf("(") + 2, prefix.indexOf(")") - 1); 1.93 + } 1.94 + switch (prefix) { 1.95 + case "text()": 1.96 + targetType = C_i.nsIDOMNodeFilter.SHOW_TEXT | 1.97 + C_i.nsIDOMNodeFilter.SHOW_CDATA_SECTION; 1.98 + targetNodeName = null; 1.99 + break; 1.100 + case "comment()": 1.101 + targetType = C_i.nsIDOMNodeFilter.SHOW_COMMENT; 1.102 + targetNodeName = null; 1.103 + break; 1.104 + case "node()": 1.105 + targetType = C_i.nsIDOMNodeFilter.SHOW_ALL; 1.106 + targetNodeName = null; 1.107 + } 1.108 + 1.109 + var filter = { 1.110 + count: 0, 1.111 + 1.112 + // nsIDOMNodeFilter 1.113 + acceptNode: function acceptNode(aNode) { 1.114 + if (aNode.parentNode != aContextNode) { 1.115 + // Don't bother looking at kids either. 1.116 + return C_i.nsIDOMNodeFilter.FILTER_REJECT; 1.117 + } 1.118 + 1.119 + if (targetNodeName && targetNodeName != aNode.nodeName) { 1.120 + return C_i.nsIDOMNodeFilter.FILTER_SKIP; 1.121 + } 1.122 + 1.123 + this.count++; 1.124 + if (this.count != childIndex) { 1.125 + return C_i.nsIDOMNodeFilter.FILTER_SKIP; 1.126 + } 1.127 + 1.128 + return C_i.nsIDOMNodeFilter.FILTER_ACCEPT; 1.129 + } 1.130 + }; 1.131 + 1.132 + // Look for the node matching the step from the document fragment. 1.133 + var walker = aContextNode.ownerDocument.createTreeWalker( 1.134 + aContextNode, 1.135 + targetType, 1.136 + filter); 1.137 + var targetNode = walker.nextNode(); 1.138 + do_check_neq(targetNode, null); 1.139 + 1.140 + // Apply our remaining xpath to the found node. 1.141 + var expr = aContextNode.ownerDocument.createExpression(realPath, null); 1.142 + var result = expr.evaluate(targetNode, UNORDERED_TYPE, null); 1.143 + do_check_true(result instanceof C_i.nsIDOMXPathResult); 1.144 + return result.singleNodeValue; 1.145 +} 1.146 + 1.147 +/** 1.148 + * Get a DOM range corresponding to the test's source node. 1.149 + * 1.150 + * @param aSourceNode <source/> element with range information. 1.151 + * @param aFragment DocumentFragment generated with getFragment(). 1.152 + * 1.153 + * @return Range object. 1.154 + */ 1.155 +function getRange(aSourceNode, aFragment) { 1.156 + do_check_true(aSourceNode instanceof C_i.nsIDOMElement); 1.157 + do_check_true(aFragment instanceof C_i.nsIDOMDocumentFragment); 1.158 + var doc = aSourceNode.ownerDocument; 1.159 + 1.160 + var containerPath = aSourceNode.getAttribute("startContainer"); 1.161 + var startContainer = evalXPathInDocumentFragment(aFragment, containerPath); 1.162 + var startOffset = Number(aSourceNode.getAttribute("startOffset")); 1.163 + 1.164 + containerPath = aSourceNode.getAttribute("endContainer"); 1.165 + var endContainer = evalXPathInDocumentFragment(aFragment, containerPath); 1.166 + var endOffset = Number(aSourceNode.getAttribute("endOffset")); 1.167 + 1.168 + var range = doc.createRange(); 1.169 + range.setStart(startContainer, startOffset); 1.170 + range.setEnd(endContainer, endOffset); 1.171 + return range; 1.172 +} 1.173 + 1.174 +/** 1.175 + * Get the document for a given path, and clean it up for our tests. 1.176 + * 1.177 + * @param aPath The path to the local document. 1.178 + */ 1.179 +function getParsedDocument(aPath) { 1.180 + var doc = do_parse_document(aPath, "application/xml"); 1.181 + do_check_true(doc.documentElement.localName != "parsererror"); 1.182 + do_check_true(doc instanceof C_i.nsIDOMXPathEvaluator); 1.183 + do_check_true(doc instanceof C_i.nsIDOMDocument); 1.184 + 1.185 + // Clean out whitespace. 1.186 + var walker = doc.createTreeWalker(doc, 1.187 + C_i.nsIDOMNodeFilter.SHOW_TEXT | 1.188 + C_i.nsIDOMNodeFilter.SHOW_CDATA_SECTION, 1.189 + isWhitespace); 1.190 + while (walker.nextNode()) { 1.191 + var parent = walker.currentNode.parentNode; 1.192 + parent.removeChild(walker.currentNode); 1.193 + walker.currentNode = parent; 1.194 + } 1.195 + 1.196 + // Clean out mandatory splits between nodes. 1.197 + var splits = doc.getElementsByTagName("split"); 1.198 + var i; 1.199 + for (i = splits.length - 1; i >= 0; i--) { 1.200 + var node = splits.item(i); 1.201 + node.parentNode.removeChild(node); 1.202 + } 1.203 + splits = null; 1.204 + 1.205 + // Replace empty CDATA sections. 1.206 + var emptyData = doc.getElementsByTagName("empty-cdata"); 1.207 + for (i = emptyData.length - 1; i >= 0; i--) { 1.208 + var node = emptyData.item(i); 1.209 + var cdata = doc.createCDATASection(""); 1.210 + node.parentNode.replaceChild(cdata, node); 1.211 + } 1.212 + 1.213 + return doc; 1.214 +} 1.215 + 1.216 +/** 1.217 + * Run the extraction tests. 1.218 + */ 1.219 +function run_extract_test() { 1.220 + var filePath = "test_delete_range.xml"; 1.221 + var doc = getParsedDocument(filePath); 1.222 + var tests = doc.getElementsByTagName("test"); 1.223 + 1.224 + // Run our deletion, extraction tests. 1.225 + for (var i = 0; i < tests.length; i++) { 1.226 + dump("Configuring for test " + i + "\n"); 1.227 + var currentTest = tests.item(i); 1.228 + 1.229 + // Validate the test is properly formatted for what this harness expects. 1.230 + var baseSource = currentTest.firstChild; 1.231 + do_check_eq(baseSource.nodeName, "source"); 1.232 + var baseResult = baseSource.nextSibling; 1.233 + do_check_eq(baseResult.nodeName, "result"); 1.234 + var baseExtract = baseResult.nextSibling; 1.235 + do_check_eq(baseExtract.nodeName, "extract"); 1.236 + do_check_eq(baseExtract.nextSibling, null); 1.237 + 1.238 + /* We do all our tests on DOM document fragments, derived from the test 1.239 + element's children. This lets us rip the various fragments to shreds, 1.240 + while preserving the original elements so we can make more copies of 1.241 + them. 1.242 + 1.243 + After the range's extraction or deletion is done, we use 1.244 + nsIDOMNode.isEqualNode() between the altered source fragment and the 1.245 + result fragment. We also run isEqualNode() between the extracted 1.246 + fragment and the fragment from the baseExtract node. If they are not 1.247 + equal, we have failed a test. 1.248 + 1.249 + We also have to ensure the original nodes on the end points of the 1.250 + range are still in the source fragment. This is bug 332148. The nodes 1.251 + may not be replaced with equal but separate nodes. The range extraction 1.252 + may alter these nodes - in the case of text containers, they will - but 1.253 + the nodes must stay there, to preserve references such as user data, 1.254 + event listeners, etc. 1.255 + 1.256 + First, an extraction test. 1.257 + */ 1.258 + 1.259 + var resultFrag = getFragment(baseResult); 1.260 + var extractFrag = getFragment(baseExtract); 1.261 + 1.262 + dump("Extract contents test " + i + "\n\n"); 1.263 + var baseFrag = getFragment(baseSource); 1.264 + var baseRange = getRange(baseSource, baseFrag); 1.265 + var startContainer = baseRange.startContainer; 1.266 + var endContainer = baseRange.endContainer; 1.267 + 1.268 + var cutFragment = baseRange.extractContents(); 1.269 + dump("cutFragment: " + cutFragment + "\n"); 1.270 + if (cutFragment) { 1.271 + do_check_true(extractFrag.isEqualNode(cutFragment)); 1.272 + } else { 1.273 + do_check_eq(extractFrag.firstChild, null); 1.274 + } 1.275 + do_check_true(baseFrag.isEqualNode(resultFrag)); 1.276 + 1.277 + dump("Ensure the original nodes weren't extracted - test " + i + "\n\n"); 1.278 + var walker = doc.createTreeWalker(baseFrag, 1.279 + C_i.nsIDOMNodeFilter.SHOW_ALL, 1.280 + null); 1.281 + var foundStart = false; 1.282 + var foundEnd = false; 1.283 + do { 1.284 + if (walker.currentNode == startContainer) { 1.285 + foundStart = true; 1.286 + } 1.287 + 1.288 + if (walker.currentNode == endContainer) { 1.289 + // An end container node should not come before the start container node. 1.290 + do_check_true(foundStart); 1.291 + foundEnd = true; 1.292 + break; 1.293 + } 1.294 + } while (walker.nextNode()) 1.295 + do_check_true(foundEnd); 1.296 + 1.297 + /* Now, we reset our test for the deleteContents case. This one differs 1.298 + from the extractContents case only in that there is no extracted document 1.299 + fragment to compare against. So we merely compare the starting fragment, 1.300 + minus the extracted content, against the result fragment. 1.301 + */ 1.302 + dump("Delete contents test " + i + "\n\n"); 1.303 + baseFrag = getFragment(baseSource); 1.304 + baseRange = getRange(baseSource, baseFrag); 1.305 + var startContainer = baseRange.startContainer; 1.306 + var endContainer = baseRange.endContainer; 1.307 + baseRange.deleteContents(); 1.308 + do_check_true(baseFrag.isEqualNode(resultFrag)); 1.309 + 1.310 + dump("Ensure the original nodes weren't deleted - test " + i + "\n\n"); 1.311 + walker = doc.createTreeWalker(baseFrag, 1.312 + C_i.nsIDOMNodeFilter.SHOW_ALL, 1.313 + null); 1.314 + foundStart = false; 1.315 + foundEnd = false; 1.316 + do { 1.317 + if (walker.currentNode == startContainer) { 1.318 + foundStart = true; 1.319 + } 1.320 + 1.321 + if (walker.currentNode == endContainer) { 1.322 + // An end container node should not come before the start container node. 1.323 + do_check_true(foundStart); 1.324 + foundEnd = true; 1.325 + break; 1.326 + } 1.327 + } while (walker.nextNode()) 1.328 + do_check_true(foundEnd); 1.329 + 1.330 + // Clean up after ourselves. 1.331 + walker = null; 1.332 + } 1.333 +} 1.334 + 1.335 +/** 1.336 + * Miscellaneous tests not covered above. 1.337 + */ 1.338 +function run_miscellaneous_tests() { 1.339 + var filePath = "test_delete_range.xml"; 1.340 + var doc = getParsedDocument(filePath); 1.341 + var tests = doc.getElementsByTagName("test"); 1.342 + 1.343 + // Let's try some invalid inputs to our DOM range and see what happens. 1.344 + var currentTest = tests.item(0); 1.345 + var baseSource = currentTest.firstChild; 1.346 + var baseResult = baseSource.nextSibling; 1.347 + var baseExtract = baseResult.nextSibling; 1.348 + 1.349 + var baseFrag = getFragment(baseSource); 1.350 + 1.351 + var baseRange = getRange(baseSource, baseFrag); 1.352 + var startContainer = baseRange.startContainer; 1.353 + var endContainer = baseRange.endContainer; 1.354 + var startOffset = baseRange.startOffset; 1.355 + var endOffset = baseRange.endOffset; 1.356 + 1.357 + // Text range manipulation. 1.358 + if ((endOffset > startOffset) && 1.359 + (startContainer == endContainer) && 1.360 + (startContainer instanceof C_i.nsIDOMText)) { 1.361 + // Invalid start node 1.362 + try { 1.363 + baseRange.setStart(null, 0); 1.364 + do_throw("Should have thrown NOT_OBJECT_ERR!"); 1.365 + } catch (e) { 1.366 + do_check_eq(e.constructor.name, "TypeError"); 1.367 + } 1.368 + 1.369 + // Invalid start node 1.370 + try { 1.371 + baseRange.setStart({}, 0); 1.372 + do_throw("Should have thrown SecurityError!"); 1.373 + } catch (e) { 1.374 + do_check_eq(e.constructor.name, "TypeError"); 1.375 + } 1.376 + 1.377 + // Invalid index 1.378 + try { 1.379 + baseRange.setStart(startContainer, -1); 1.380 + do_throw("Should have thrown IndexSizeError!"); 1.381 + } catch (e) { 1.382 + do_check_eq(e.name, "IndexSizeError"); 1.383 + } 1.384 + 1.385 + // Invalid index 1.386 + var newOffset = startContainer instanceof C_i.nsIDOMText ? 1.387 + startContainer.nodeValue.length + 1 : 1.388 + startContainer.childNodes.length + 1; 1.389 + try { 1.390 + baseRange.setStart(startContainer, newOffset); 1.391 + do_throw("Should have thrown IndexSizeError!"); 1.392 + } catch (e) { 1.393 + do_check_eq(e.name, "IndexSizeError"); 1.394 + } 1.395 + 1.396 + newOffset--; 1.397 + // Valid index 1.398 + baseRange.setStart(startContainer, newOffset); 1.399 + do_check_eq(baseRange.startContainer, baseRange.endContainer); 1.400 + do_check_eq(baseRange.startOffset, newOffset); 1.401 + do_check_true(baseRange.collapsed); 1.402 + 1.403 + // Valid index 1.404 + baseRange.setEnd(startContainer, 0); 1.405 + do_check_eq(baseRange.startContainer, baseRange.endContainer); 1.406 + do_check_eq(baseRange.startOffset, 0); 1.407 + do_check_true(baseRange.collapsed); 1.408 + } else { 1.409 + do_throw("The first test should be a text-only range test. Test is invalid.") 1.410 + } 1.411 + 1.412 + /* See what happens when a range has a startContainer in one fragment, and an 1.413 + endContainer in another. According to the DOM spec, section 2.4, the range 1.414 + should collapse to the new container and offset. */ 1.415 + baseRange = getRange(baseSource, baseFrag); 1.416 + startContainer = baseRange.startContainer; 1.417 + var startOffset = baseRange.startOffset; 1.418 + endContainer = baseRange.endContainer; 1.419 + var endOffset = baseRange.endOffset; 1.420 + 1.421 + dump("External fragment test\n\n"); 1.422 + 1.423 + var externalTest = tests.item(1); 1.424 + var externalSource = externalTest.firstChild; 1.425 + var externalFrag = getFragment(externalSource); 1.426 + var externalRange = getRange(externalSource, externalFrag); 1.427 + 1.428 + baseRange.setEnd(externalRange.endContainer, 0); 1.429 + do_check_eq(baseRange.startContainer, externalRange.endContainer); 1.430 + do_check_eq(baseRange.startOffset, 0); 1.431 + do_check_true(baseRange.collapsed); 1.432 + 1.433 + /* 1.434 + // XXX ajvincent if rv == WRONG_DOCUMENT_ERR, return false? 1.435 + do_check_false(baseRange.isPointInRange(startContainer, startOffset)); 1.436 + do_check_false(baseRange.isPointInRange(startContainer, startOffset + 1)); 1.437 + do_check_false(baseRange.isPointInRange(endContainer, endOffset)); 1.438 + */ 1.439 + 1.440 + // Requested by smaug: A range involving a comment as a document child. 1.441 + doc = parser.parseFromString("<!-- foo --><foo/>", "application/xml"); 1.442 + do_check_true(doc instanceof C_i.nsIDOMDocument); 1.443 + do_check_eq(doc.childNodes.length, 2); 1.444 + baseRange = doc.createRange(); 1.445 + baseRange.setStart(doc.firstChild, 1); 1.446 + baseRange.setEnd(doc.firstChild, 2); 1.447 + var frag = baseRange.extractContents(); 1.448 + do_check_eq(frag.childNodes.length, 1); 1.449 + do_check_true(frag.firstChild instanceof C_i.nsIDOMComment); 1.450 + do_check_eq(frag.firstChild.nodeValue, "f"); 1.451 + 1.452 + /* smaug also requested attribute tests. Sadly, those are not yet supported 1.453 + in ranges - see https://bugzilla.mozilla.org/show_bug.cgi?id=302775. 1.454 + */ 1.455 +} 1.456 + 1.457 +function run_test() { 1.458 + run_extract_test(); 1.459 + run_miscellaneous_tests(); 1.460 +}