test_range.js (14838B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const UNORDERED_TYPE = 8; // XPathResult.ANY_UNORDERED_NODE_TYPE 6 7 /** 8 * Determine if the data node has only ignorable white-space. 9 * 10 * @return NodeFilter.FILTER_SKIP if it does. 11 * @return NodeFilter.FILTER_ACCEPT otherwise. 12 */ 13 function isWhitespace(aNode) { 14 return /\S/.test(aNode.nodeValue) 15 ? NodeFilter.FILTER_SKIP 16 : NodeFilter.FILTER_ACCEPT; 17 } 18 19 /** 20 * Create a DocumentFragment with cloned children equaling a node's children. 21 * 22 * @param aNode The node to copy from. 23 * 24 * @return DocumentFragment node. 25 */ 26 function getFragment(aNode) { 27 var frag = aNode.ownerDocument.createDocumentFragment(); 28 for (var i = 0; i < aNode.childNodes.length; i++) { 29 frag.appendChild(aNode.childNodes.item(i).cloneNode(true)); 30 } 31 return frag; 32 } 33 34 // Goodies from head_content.js 35 const parser = getParser(); 36 37 /** 38 * Translate an XPath to a DOM node. This method uses a document 39 * fragment as context node. 40 * 41 * @param aContextNode The context node to apply the XPath to. 42 * @param aPath The XPath to use. 43 * 44 * @return Node The target node retrieved from the XPath. 45 */ 46 function evalXPathInDocumentFragment(aContextNode, aPath) { 47 Assert.equal(ChromeUtils.getClassName(aContextNode), "DocumentFragment"); 48 Assert.ok(aContextNode.childNodes.length); 49 if (aPath == ".") { 50 return aContextNode; 51 } 52 53 // Separate the fragment's xpath lookup from the rest. 54 var firstSlash = aPath.indexOf("/"); 55 if (firstSlash == -1) { 56 firstSlash = aPath.length; 57 } 58 var prefix = aPath.substr(0, firstSlash); 59 var realPath = aPath.substr(firstSlash + 1); 60 if (!realPath) { 61 realPath = "."; 62 } 63 64 // Set up a special node filter to look among the fragment's child nodes. 65 var childIndex = 1; 66 var bracketIndex = prefix.indexOf("["); 67 if (bracketIndex != -1) { 68 childIndex = Number( 69 prefix.substring(bracketIndex + 1, prefix.indexOf("]")) 70 ); 71 Assert.greater(childIndex, 0); 72 prefix = prefix.substr(0, bracketIndex); 73 } 74 75 var targetType = NodeFilter.SHOW_ELEMENT; 76 var targetNodeName = prefix; 77 if (prefix.indexOf("processing-instruction(") == 0) { 78 targetType = NodeFilter.SHOW_PROCESSING_INSTRUCTION; 79 targetNodeName = prefix.substring( 80 prefix.indexOf("(") + 2, 81 prefix.indexOf(")") - 1 82 ); 83 } 84 switch (prefix) { 85 case "text()": 86 targetType = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION; 87 targetNodeName = null; 88 break; 89 case "comment()": 90 targetType = NodeFilter.SHOW_COMMENT; 91 targetNodeName = null; 92 break; 93 case "node()": 94 targetType = NodeFilter.SHOW_ALL; 95 targetNodeName = null; 96 } 97 98 var filter = { 99 count: 0, 100 101 // NodeFilter 102 acceptNode: function acceptNode(aNode) { 103 if (aNode.parentNode != aContextNode) { 104 // Don't bother looking at kids either. 105 return NodeFilter.FILTER_REJECT; 106 } 107 108 if (targetNodeName && targetNodeName != aNode.nodeName) { 109 return NodeFilter.FILTER_SKIP; 110 } 111 112 this.count++; 113 if (this.count != childIndex) { 114 return NodeFilter.FILTER_SKIP; 115 } 116 117 return NodeFilter.FILTER_ACCEPT; 118 }, 119 }; 120 121 // Look for the node matching the step from the document fragment. 122 var walker = aContextNode.ownerDocument.createTreeWalker( 123 aContextNode, 124 targetType, 125 filter 126 ); 127 var targetNode = walker.nextNode(); 128 Assert.notEqual(targetNode, null); 129 130 // Apply our remaining xpath to the found node. 131 var expr = aContextNode.ownerDocument.createExpression(realPath, null); 132 var result = expr.evaluate(targetNode, UNORDERED_TYPE, null); 133 return result.singleNodeValue; 134 } 135 136 /** 137 * Get a DOM range corresponding to the test's source node. 138 * 139 * @param aSourceNode <source/> element with range information. 140 * @param aFragment DocumentFragment generated with getFragment(). 141 * 142 * @return Range object. 143 */ 144 function getRange(aSourceNode, aFragment) { 145 Assert.ok(Element.isInstance(aSourceNode)); 146 Assert.equal(ChromeUtils.getClassName(aFragment), "DocumentFragment"); 147 var doc = aSourceNode.ownerDocument; 148 149 var containerPath = aSourceNode.getAttribute("startContainer"); 150 var startContainer = evalXPathInDocumentFragment(aFragment, containerPath); 151 var startOffset = Number(aSourceNode.getAttribute("startOffset")); 152 153 containerPath = aSourceNode.getAttribute("endContainer"); 154 var endContainer = evalXPathInDocumentFragment(aFragment, containerPath); 155 var endOffset = Number(aSourceNode.getAttribute("endOffset")); 156 157 var range = doc.createRange(); 158 range.setStart(startContainer, startOffset); 159 range.setEnd(endContainer, endOffset); 160 return range; 161 } 162 163 /** 164 * Get the document for a given path, and clean it up for our tests. 165 * 166 * @param aPath The path to the local document. 167 */ 168 function getParsedDocument(aPath) { 169 return do_parse_document(aPath, "application/xml").then( 170 processParsedDocument 171 ); 172 } 173 174 function processParsedDocument(doc) { 175 Assert.notEqual(doc.documentElement.localName, "parsererror"); 176 Assert.equal(ChromeUtils.getClassName(doc), "XMLDocument"); 177 178 // Clean out whitespace. 179 var walker = doc.createTreeWalker( 180 doc, 181 NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION, 182 isWhitespace 183 ); 184 while (walker.nextNode()) { 185 var parent = walker.currentNode.parentNode; 186 parent.removeChild(walker.currentNode); 187 walker.currentNode = parent; 188 } 189 190 // Clean out mandatory splits between nodes. 191 var splits = doc.getElementsByTagName("split"); 192 var i; 193 for (i = splits.length - 1; i >= 0; i--) { 194 let node = splits.item(i); 195 node.remove(); 196 } 197 splits = null; 198 199 // Replace empty CDATA sections. 200 var emptyData = doc.getElementsByTagName("empty-cdata"); 201 for (i = emptyData.length - 1; i >= 0; i--) { 202 let node = emptyData.item(i); 203 var cdata = doc.createCDATASection(""); 204 node.parentNode.replaceChild(cdata, node); 205 } 206 207 return doc; 208 } 209 210 /** 211 * Run the extraction tests. 212 */ 213 function run_extract_test() { 214 var filePath = "test_delete_range.xml"; 215 getParsedDocument(filePath).then(do_extract_test); 216 } 217 218 function do_extract_test(doc) { 219 var tests = doc.getElementsByTagName("test"); 220 221 // Run our deletion, extraction tests. 222 for (var i = 0; i < tests.length; i++) { 223 dump("Configuring for test " + i + "\n"); 224 var currentTest = tests.item(i); 225 226 // Validate the test is properly formatted for what this harness expects. 227 var baseSource = currentTest.firstChild; 228 Assert.equal(baseSource.nodeName, "source"); 229 var baseResult = baseSource.nextSibling; 230 Assert.equal(baseResult.nodeName, "result"); 231 var baseExtract = baseResult.nextSibling; 232 Assert.equal(baseExtract.nodeName, "extract"); 233 Assert.equal(baseExtract.nextSibling, null); 234 235 /* We do all our tests on DOM document fragments, derived from the test 236 element's children. This lets us rip the various fragments to shreds, 237 while preserving the original elements so we can make more copies of 238 them. 239 240 After the range's extraction or deletion is done, we use 241 Node.isEqualNode() between the altered source fragment and the 242 result fragment. We also run isEqualNode() between the extracted 243 fragment and the fragment from the baseExtract node. If they are not 244 equal, we have failed a test. 245 246 We also have to ensure the original nodes on the end points of the 247 range are still in the source fragment. This is bug 332148. The nodes 248 may not be replaced with equal but separate nodes. The range extraction 249 may alter these nodes - in the case of text containers, they will - but 250 the nodes must stay there, to preserve references such as user data, 251 event listeners, etc. 252 253 First, an extraction test. 254 */ 255 256 var resultFrag = getFragment(baseResult); 257 var extractFrag = getFragment(baseExtract); 258 259 dump("Extract contents test " + i + "\n\n"); 260 var baseFrag = getFragment(baseSource); 261 var baseRange = getRange(baseSource, baseFrag); 262 var startContainer = baseRange.startContainer; 263 var endContainer = baseRange.endContainer; 264 265 var cutFragment = baseRange.extractContents(); 266 dump("cutFragment: " + cutFragment + "\n"); 267 if (cutFragment) { 268 Assert.ok(extractFrag.isEqualNode(cutFragment)); 269 } else { 270 Assert.equal(extractFrag.firstChild, null); 271 } 272 Assert.ok(baseFrag.isEqualNode(resultFrag)); 273 274 dump("Ensure the original nodes weren't extracted - test " + i + "\n\n"); 275 var walker = doc.createTreeWalker(baseFrag, NodeFilter.SHOW_ALL, null); 276 var foundStart = false; 277 var foundEnd = false; 278 do { 279 if (walker.currentNode == startContainer) { 280 foundStart = true; 281 } 282 283 if (walker.currentNode == endContainer) { 284 // An end container node should not come before the start container node. 285 Assert.ok(foundStart); 286 foundEnd = true; 287 break; 288 } 289 } while (walker.nextNode()); 290 Assert.ok(foundEnd); 291 292 /* Now, we reset our test for the deleteContents case. This one differs 293 from the extractContents case only in that there is no extracted document 294 fragment to compare against. So we merely compare the starting fragment, 295 minus the extracted content, against the result fragment. 296 */ 297 dump("Delete contents test " + i + "\n\n"); 298 baseFrag = getFragment(baseSource); 299 baseRange = getRange(baseSource, baseFrag); 300 startContainer = baseRange.startContainer; 301 endContainer = baseRange.endContainer; 302 baseRange.deleteContents(); 303 Assert.ok(baseFrag.isEqualNode(resultFrag)); 304 305 dump("Ensure the original nodes weren't deleted - test " + i + "\n\n"); 306 walker = doc.createTreeWalker(baseFrag, NodeFilter.SHOW_ALL, null); 307 foundStart = false; 308 foundEnd = false; 309 do { 310 if (walker.currentNode == startContainer) { 311 foundStart = true; 312 } 313 314 if (walker.currentNode == endContainer) { 315 // An end container node should not come before the start container node. 316 Assert.ok(foundStart); 317 foundEnd = true; 318 break; 319 } 320 } while (walker.nextNode()); 321 Assert.ok(foundEnd); 322 323 // Clean up after ourselves. 324 walker = null; 325 } 326 } 327 328 /** 329 * Miscellaneous tests not covered above. 330 */ 331 function run_miscellaneous_tests() { 332 var filePath = "test_delete_range.xml"; 333 getParsedDocument(filePath).then(do_miscellaneous_tests); 334 } 335 336 function isText(node) { 337 return ( 338 node.nodeType == node.TEXT_NODE || node.nodeType == node.CDATA_SECTION_NODE 339 ); 340 } 341 342 function do_miscellaneous_tests(doc) { 343 var tests = doc.getElementsByTagName("test"); 344 345 // Let's try some invalid inputs to our DOM range and see what happens. 346 var currentTest = tests.item(0); 347 var baseSource = currentTest.firstChild; 348 349 var baseFrag = getFragment(baseSource); 350 351 var baseRange = getRange(baseSource, baseFrag); 352 var startContainer = baseRange.startContainer; 353 var endContainer = baseRange.endContainer; 354 var startOffset = baseRange.startOffset; 355 var endOffset = baseRange.endOffset; 356 357 // Text range manipulation. 358 if ( 359 endOffset > startOffset && 360 startContainer == endContainer && 361 isText(startContainer) 362 ) { 363 // Invalid start node 364 try { 365 baseRange.setStart(null, 0); 366 do_throw("Should have thrown NOT_OBJECT_ERR!"); 367 } catch (e) { 368 Assert.equal(e.constructor.name, "TypeError"); 369 } 370 371 // Invalid start node 372 try { 373 baseRange.setStart({}, 0); 374 do_throw("Should have thrown SecurityError!"); 375 } catch (e) { 376 Assert.equal(e.constructor.name, "TypeError"); 377 } 378 379 // Invalid index 380 try { 381 baseRange.setStart(startContainer, -1); 382 do_throw("Should have thrown IndexSizeError!"); 383 } catch (e) { 384 Assert.equal(e.name, "IndexSizeError"); 385 } 386 387 // Invalid index 388 var newOffset = isText(startContainer) 389 ? startContainer.nodeValue.length + 1 390 : startContainer.childNodes.length + 1; 391 try { 392 baseRange.setStart(startContainer, newOffset); 393 do_throw("Should have thrown IndexSizeError!"); 394 } catch (e) { 395 Assert.equal(e.name, "IndexSizeError"); 396 } 397 398 newOffset--; 399 // Valid index 400 baseRange.setStart(startContainer, newOffset); 401 Assert.equal(baseRange.startContainer, baseRange.endContainer); 402 Assert.equal(baseRange.startOffset, newOffset); 403 Assert.ok(baseRange.collapsed); 404 405 // Valid index 406 baseRange.setEnd(startContainer, 0); 407 Assert.equal(baseRange.startContainer, baseRange.endContainer); 408 Assert.equal(baseRange.startOffset, 0); 409 Assert.ok(baseRange.collapsed); 410 } else { 411 do_throw( 412 "The first test should be a text-only range test. Test is invalid." 413 ); 414 } 415 416 /* See what happens when a range has a startContainer in one fragment, and an 417 endContainer in another. According to the DOM spec, section 2.4, the range 418 should collapse to the new container and offset. */ 419 baseRange = getRange(baseSource, baseFrag); 420 startContainer = baseRange.startContainer; 421 startOffset = baseRange.startOffset; 422 endContainer = baseRange.endContainer; 423 endOffset = baseRange.endOffset; 424 425 dump("External fragment test\n\n"); 426 427 var externalTest = tests.item(1); 428 var externalSource = externalTest.firstChild; 429 var externalFrag = getFragment(externalSource); 430 var externalRange = getRange(externalSource, externalFrag); 431 432 baseRange.setEnd(externalRange.endContainer, 0); 433 Assert.equal(baseRange.startContainer, externalRange.endContainer); 434 Assert.equal(baseRange.startOffset, 0); 435 Assert.ok(baseRange.collapsed); 436 437 /* 438 // XXX ajvincent if rv == WRONG_DOCUMENT_ERR, return false? 439 do_check_false(baseRange.isPointInRange(startContainer, startOffset)); 440 do_check_false(baseRange.isPointInRange(startContainer, startOffset + 1)); 441 do_check_false(baseRange.isPointInRange(endContainer, endOffset)); 442 */ 443 444 // Requested by smaug: A range involving a comment as a document child. 445 doc = parser.parseFromString("<!-- foo --><foo/>", "application/xml"); 446 Assert.equal(ChromeUtils.getClassName(doc), "XMLDocument"); 447 Assert.equal(doc.childNodes.length, 2); 448 baseRange = doc.createRange(); 449 baseRange.setStart(doc.firstChild, 1); 450 baseRange.setEnd(doc.firstChild, 2); 451 var frag = baseRange.extractContents(); 452 Assert.equal(frag.childNodes.length, 1); 453 Assert.equal(ChromeUtils.getClassName(frag.firstChild), "Comment"); 454 Assert.equal(frag.firstChild.nodeType, frag.COMMENT_NODE); 455 Assert.equal(frag.firstChild.nodeValue, "f"); 456 457 /* smaug also requested attribute tests. Sadly, those are not yet supported 458 in ranges - see https://bugzilla.mozilla.org/show_bug.cgi?id=302775. 459 */ 460 } 461 462 function run_test() { 463 run_extract_test(); 464 run_miscellaneous_tests(); 465 }