Range-cloneContents.html (18714B)
1 <!doctype html> 2 <title>Range.cloneContents() tests</title> 3 <link rel="author" title="Aryeh Gregor" href=ayg@aryeh.name> 4 <meta name=timeout content=long> 5 <p>To debug test failures, add a query parameter "subtest" with the test id (like 6 "?subtest=5"). Only that test will be run. Then you can look at the resulting 7 iframe in the DOM. 8 <div id=log></div> 9 <script src=/resources/testharness.js></script> 10 <script src=/resources/testharnessreport.js></script> 11 <script src=../common.js></script> 12 <script> 13 "use strict"; 14 15 testDiv.parentNode.removeChild(testDiv); 16 17 var actualIframe = document.createElement("iframe"); 18 actualIframe.style.display = "none"; 19 document.body.appendChild(actualIframe); 20 21 var expectedIframe = document.createElement("iframe"); 22 expectedIframe.style.display = "none"; 23 document.body.appendChild(expectedIframe); 24 25 function myCloneContents(range) { 26 // "Let frag be a new DocumentFragment whose ownerDocument is the same as 27 // the ownerDocument of the context object's start node." 28 var ownerDoc = range.startContainer.nodeType == Node.DOCUMENT_NODE 29 ? range.startContainer 30 : range.startContainer.ownerDocument; 31 var frag = ownerDoc.createDocumentFragment(); 32 33 // "If the context object's start and end are the same, abort this method, 34 // returning frag." 35 if (range.startContainer == range.endContainer 36 && range.startOffset == range.endOffset) { 37 return frag; 38 } 39 40 // "Let original start node, original start offset, original end node, and 41 // original end offset be the context object's start and end nodes and 42 // offsets, respectively." 43 var originalStartNode = range.startContainer; 44 var originalStartOffset = range.startOffset; 45 var originalEndNode = range.endContainer; 46 var originalEndOffset = range.endOffset; 47 48 // "If original start node and original end node are the same, and they are 49 // a Text, ProcessingInstruction, or Comment node:" 50 if (range.startContainer == range.endContainer 51 && (isText(range.startContainer) 52 || range.startContainer.nodeType == Node.COMMENT_NODE 53 || range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) { 54 // "Let clone be the result of calling cloneNode(false) on original 55 // start node." 56 var clone = originalStartNode.cloneNode(false); 57 58 // "Set the data of clone to the result of calling 59 // substringData(original start offset, original end offset − original 60 // start offset) on original start node." 61 clone.data = originalStartNode.substringData(originalStartOffset, 62 originalEndOffset - originalStartOffset); 63 64 // "Append clone as the last child of frag." 65 frag.appendChild(clone); 66 67 // "Abort this method, returning frag." 68 return frag; 69 } 70 71 // "Let common ancestor equal original start node." 72 var commonAncestor = originalStartNode; 73 74 // "While common ancestor is not an ancestor container of original end 75 // node, set common ancestor to its own parent." 76 while (!isAncestorContainer(commonAncestor, originalEndNode)) { 77 commonAncestor = commonAncestor.parentNode; 78 } 79 80 // "If original start node is an ancestor container of original end node, 81 // let first partially contained child be null." 82 var firstPartiallyContainedChild; 83 if (isAncestorContainer(originalStartNode, originalEndNode)) { 84 firstPartiallyContainedChild = null; 85 // "Otherwise, let first partially contained child be the first child of 86 // common ancestor that is partially contained in the context object." 87 } else { 88 for (var i = 0; i < commonAncestor.childNodes.length; i++) { 89 if (isPartiallyContained(commonAncestor.childNodes[i], range)) { 90 firstPartiallyContainedChild = commonAncestor.childNodes[i]; 91 break; 92 } 93 } 94 if (!firstPartiallyContainedChild) { 95 throw "Spec bug: no first partially contained child!"; 96 } 97 } 98 99 // "If original end node is an ancestor container of original start node, 100 // let last partially contained child be null." 101 var lastPartiallyContainedChild; 102 if (isAncestorContainer(originalEndNode, originalStartNode)) { 103 lastPartiallyContainedChild = null; 104 // "Otherwise, let last partially contained child be the last child of 105 // common ancestor that is partially contained in the context object." 106 } else { 107 for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) { 108 if (isPartiallyContained(commonAncestor.childNodes[i], range)) { 109 lastPartiallyContainedChild = commonAncestor.childNodes[i]; 110 break; 111 } 112 } 113 if (!lastPartiallyContainedChild) { 114 throw "Spec bug: no last partially contained child!"; 115 } 116 } 117 118 // "Let contained children be a list of all children of common ancestor 119 // that are contained in the context object, in tree order." 120 // 121 // "If any member of contained children is a DocumentType, raise a 122 // HIERARCHY_REQUEST_ERR exception and abort these steps." 123 var containedChildren = []; 124 for (var i = 0; i < commonAncestor.childNodes.length; i++) { 125 if (isContained(commonAncestor.childNodes[i], range)) { 126 if (commonAncestor.childNodes[i].nodeType 127 == Node.DOCUMENT_TYPE_NODE) { 128 return "HIERARCHY_REQUEST_ERR"; 129 } 130 containedChildren.push(commonAncestor.childNodes[i]); 131 } 132 } 133 134 // "If first partially contained child is a Text, ProcessingInstruction, or Comment node:" 135 if (firstPartiallyContainedChild 136 && (isText(firstPartiallyContainedChild) 137 || firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE 138 || firstPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) { 139 // "Let clone be the result of calling cloneNode(false) on original 140 // start node." 141 var clone = originalStartNode.cloneNode(false); 142 143 // "Set the data of clone to the result of calling substringData() on 144 // original start node, with original start offset as the first 145 // argument and (length of original start node − original start offset) 146 // as the second." 147 clone.data = originalStartNode.substringData(originalStartOffset, 148 nodeLength(originalStartNode) - originalStartOffset); 149 150 // "Append clone as the last child of frag." 151 frag.appendChild(clone); 152 // "Otherwise, if first partially contained child is not null:" 153 } else if (firstPartiallyContainedChild) { 154 // "Let clone be the result of calling cloneNode(false) on first 155 // partially contained child." 156 var clone = firstPartiallyContainedChild.cloneNode(false); 157 158 // "Append clone as the last child of frag." 159 frag.appendChild(clone); 160 161 // "Let subrange be a new Range whose start is (original start node, 162 // original start offset) and whose end is (first partially contained 163 // child, length of first partially contained child)." 164 var subrange = ownerDoc.createRange(); 165 subrange.setStart(originalStartNode, originalStartOffset); 166 subrange.setEnd(firstPartiallyContainedChild, 167 nodeLength(firstPartiallyContainedChild)); 168 169 // "Let subfrag be the result of calling cloneContents() on 170 // subrange." 171 var subfrag = myCloneContents(subrange); 172 173 // "For each child of subfrag, in order, append that child to clone as 174 // its last child." 175 for (var i = 0; i < subfrag.childNodes.length; i++) { 176 clone.appendChild(subfrag.childNodes[i]); 177 } 178 } 179 180 // "For each contained child in contained children:" 181 for (var i = 0; i < containedChildren.length; i++) { 182 // "Let clone be the result of calling cloneNode(true) of contained 183 // child." 184 var clone = containedChildren[i].cloneNode(true); 185 186 // "Append clone as the last child of frag." 187 frag.appendChild(clone); 188 } 189 190 // "If last partially contained child is a Text, ProcessingInstruction, or Comment node:" 191 if (lastPartiallyContainedChild 192 && (isText(lastPartiallyContainedChild) 193 || lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE 194 || lastPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE)) { 195 // "Let clone be the result of calling cloneNode(false) on original 196 // end node." 197 var clone = originalEndNode.cloneNode(false); 198 199 // "Set the data of clone to the result of calling substringData(0, 200 // original end offset) on original end node." 201 clone.data = originalEndNode.substringData(0, originalEndOffset); 202 203 // "Append clone as the last child of frag." 204 frag.appendChild(clone); 205 // "Otherwise, if last partially contained child is not null:" 206 } else if (lastPartiallyContainedChild) { 207 // "Let clone be the result of calling cloneNode(false) on last 208 // partially contained child." 209 var clone = lastPartiallyContainedChild.cloneNode(false); 210 211 // "Append clone as the last child of frag." 212 frag.appendChild(clone); 213 214 // "Let subrange be a new Range whose start is (last partially 215 // contained child, 0) and whose end is (original end node, original 216 // end offset)." 217 var subrange = ownerDoc.createRange(); 218 subrange.setStart(lastPartiallyContainedChild, 0); 219 subrange.setEnd(originalEndNode, originalEndOffset); 220 221 // "Let subfrag be the result of calling cloneContents() on 222 // subrange." 223 var subfrag = myCloneContents(subrange); 224 225 // "For each child of subfrag, in order, append that child to clone as 226 // its last child." 227 for (var i = 0; i < subfrag.childNodes.length; i++) { 228 clone.appendChild(subfrag.childNodes[i]); 229 } 230 } 231 232 // "Return frag." 233 return frag; 234 } 235 236 function restoreIframe(iframe, i) { 237 // Most of this function is designed to work around the fact that Opera 238 // doesn't let you add a doctype to a document that no longer has one, in 239 // any way I can figure out. I eventually compromised on something that 240 // will still let Opera pass most tests that don't actually involve 241 // doctypes. 242 while (iframe.contentDocument.firstChild 243 && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) { 244 iframe.contentDocument.removeChild(iframe.contentDocument.firstChild); 245 } 246 247 while (iframe.contentDocument.lastChild 248 && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) { 249 iframe.contentDocument.removeChild(iframe.contentDocument.lastChild); 250 } 251 252 if (!iframe.contentDocument.firstChild) { 253 // This will throw an exception in Opera if we reach here, which is why 254 // I try to avoid it. It will never happen in a browser that obeys the 255 // spec, so it's really just insurance. I don't think it actually gets 256 // hit by anything. 257 iframe.contentDocument.appendChild(iframe.contentDocument.implementation.createDocumentType("html", "", "")); 258 } 259 iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true)); 260 iframe.contentWindow.setupRangeTests(); 261 iframe.contentWindow.testRangeInput = testRanges[i]; 262 iframe.contentWindow.run(); 263 } 264 265 function testCloneContents(i) { 266 restoreIframe(actualIframe, i); 267 restoreIframe(expectedIframe, i); 268 269 var actualRange = actualIframe.contentWindow.testRange; 270 var expectedRange = expectedIframe.contentWindow.testRange; 271 var actualFrag, expectedFrag; 272 var actualRoots, expectedRoots; 273 274 domTests[i].step(function() { 275 assert_equals(actualIframe.contentWindow.unexpectedException, null, 276 "Unexpected exception thrown when setting up Range for actual cloneContents()"); 277 assert_equals(expectedIframe.contentWindow.unexpectedException, null, 278 "Unexpected exception thrown when setting up Range for simulated cloneContents()"); 279 assert_equals(typeof actualRange, "object", 280 "typeof Range produced in actual iframe"); 281 assert_equals(typeof expectedRange, "object", 282 "typeof Range produced in expected iframe"); 283 284 // NOTE: We could just assume that cloneContents() doesn't change 285 // anything. That would simplify these tests, taken in isolation. But 286 // once we've already set up the whole apparatus for extractContents() 287 // and deleteContents(), we just reuse it here, on the theory of "why 288 // not test some more stuff if it's easy to do". 289 // 290 // Just to be pedantic, we'll test not only that the tree we're 291 // modifying is the same in expected vs. actual, but also that all the 292 // nodes originally in it were the same. Typically some nodes will 293 // become detached when the algorithm is run, but they still exist and 294 // references can still be kept to them, so they should also remain the 295 // same. 296 // 297 // We initialize the list to all nodes, and later on remove all the 298 // ones which still have parents, since the parents will presumably be 299 // tested for isEqualNode() and checking the children would be 300 // redundant. 301 var actualAllNodes = []; 302 var node = furthestAncestor(actualRange.startContainer); 303 do { 304 actualAllNodes.push(node); 305 } while (node = nextNode(node)); 306 307 var expectedAllNodes = []; 308 var node = furthestAncestor(expectedRange.startContainer); 309 do { 310 expectedAllNodes.push(node); 311 } while (node = nextNode(node)); 312 313 expectedFrag = myCloneContents(expectedRange); 314 if (typeof expectedFrag == "string") { 315 assert_throws_dom( 316 expectedFrag, 317 actualIframe.contentWindow.DOMException, 318 function() { 319 actualRange.cloneContents(); 320 } 321 ); 322 } else { 323 actualFrag = actualRange.cloneContents(); 324 } 325 326 actualRoots = []; 327 for (var j = 0; j < actualAllNodes.length; j++) { 328 if (!actualAllNodes[j].parentNode) { 329 actualRoots.push(actualAllNodes[j]); 330 } 331 } 332 333 expectedRoots = []; 334 for (var j = 0; j < expectedAllNodes.length; j++) { 335 if (!expectedAllNodes[j].parentNode) { 336 expectedRoots.push(expectedAllNodes[j]); 337 } 338 } 339 340 for (var j = 0; j < actualRoots.length; j++) { 341 assertNodesEqual(actualRoots[j], expectedRoots[j], j ? "detached node #" + j : "tree root"); 342 343 if (j == 0) { 344 // Clearly something is wrong if the node lists are different 345 // lengths. We want to report this only after we've already 346 // checked the main tree for equality, though, so it doesn't 347 // mask more interesting errors. 348 assert_equals(actualRoots.length, expectedRoots.length, 349 "Actual and expected DOMs were broken up into a different number of pieces by cloneContents() (this probably means you created or detached nodes when you weren't supposed to)"); 350 } 351 } 352 }); 353 domTests[i].done(); 354 355 positionTests[i].step(function() { 356 assert_equals(actualIframe.contentWindow.unexpectedException, null, 357 "Unexpected exception thrown when setting up Range for actual cloneContents()"); 358 assert_equals(expectedIframe.contentWindow.unexpectedException, null, 359 "Unexpected exception thrown when setting up Range for simulated cloneContents()"); 360 assert_equals(typeof actualRange, "object", 361 "typeof Range produced in actual iframe"); 362 assert_equals(typeof expectedRange, "object", 363 "typeof Range produced in expected iframe"); 364 365 assert_true(actualRoots[0].isEqualNode(expectedRoots[0]), 366 "The resulting DOMs were not equal, so comparing positions makes no sense"); 367 368 if (typeof expectedFrag == "string") { 369 // It's no longer true that, e.g., startContainer and endContainer 370 // must always be the same 371 return; 372 } 373 374 assert_equals(actualRange.startOffset, expectedRange.startOffset, 375 "Unexpected startOffset after cloneContents()"); 376 // How do we decide that the two nodes are equal, since they're in 377 // different trees? Since the DOMs are the same, it's enough to check 378 // that the index in the parent is the same all the way up the tree. 379 // But we can first cheat by just checking they're actually equal. 380 assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContainer), 381 "Unexpected startContainer after cloneContents(), expected " + 382 expectedRange.startContainer.nodeName.toLowerCase() + " but got " + 383 actualRange.startContainer.nodeName.toLowerCase()); 384 var currentActual = actualRange.startContainer; 385 var currentExpected = expectedRange.startContainer; 386 var actual = ""; 387 var expected = ""; 388 while (currentActual && currentExpected) { 389 actual = indexOf(currentActual) + "-" + actual; 390 expected = indexOf(currentExpected) + "-" + expected; 391 392 currentActual = currentActual.parentNode; 393 currentExpected = currentExpected.parentNode; 394 } 395 actual = actual.substr(0, actual.length - 1); 396 expected = expected.substr(0, expected.length - 1); 397 assert_equals(actual, expected, 398 "startContainer superficially looks right but is actually the wrong node if you trace back its index in all its ancestors (I'm surprised this actually happened"); 399 }); 400 positionTests[i].done(); 401 402 fragTests[i].step(function() { 403 assert_equals(actualIframe.contentWindow.unexpectedException, null, 404 "Unexpected exception thrown when setting up Range for actual cloneContents()"); 405 assert_equals(expectedIframe.contentWindow.unexpectedException, null, 406 "Unexpected exception thrown when setting up Range for simulated cloneContents()"); 407 assert_equals(typeof actualRange, "object", 408 "typeof Range produced in actual iframe"); 409 assert_equals(typeof expectedRange, "object", 410 "typeof Range produced in expected iframe"); 411 412 if (typeof expectedFrag == "string") { 413 // Comparing makes no sense 414 return; 415 } 416 assertNodesEqual(actualFrag, expectedFrag, 417 "returned fragment"); 418 }); 419 fragTests[i].done(); 420 } 421 422 // First test a Range that has the no-op detach() called on it, synchronously 423 test(function() { 424 var range = document.createRange(); 425 range.detach(); 426 assert_array_equals(range.cloneContents().childNodes, []); 427 }, "Range.detach()"); 428 429 var iStart = 0; 430 var iStop = testRanges.length; 431 432 if (/subtest=[0-9]+/.test(location.search)) { 433 var matches = /subtest=([0-9]+)/.exec(location.search); 434 iStart = Number(matches[1]); 435 iStop = Number(matches[1]) + 1; 436 } 437 438 var domTests = []; 439 var positionTests = []; 440 var fragTests = []; 441 442 for (var i = iStart; i < iStop; i++) { 443 domTests[i] = async_test("Resulting DOM for range " + i + " " + testRanges[i]); 444 positionTests[i] = async_test("Resulting cursor position for range " + i + " " + testRanges[i]); 445 fragTests[i] = async_test("Returned fragment for range " + i + " " + testRanges[i]); 446 } 447 448 var referenceDoc = document.implementation.createHTMLDocument(""); 449 referenceDoc.removeChild(referenceDoc.documentElement); 450 451 actualIframe.onload = function() { 452 expectedIframe.onload = function() { 453 for (var i = iStart; i < iStop; i++) { 454 testCloneContents(i); 455 } 456 } 457 expectedIframe.src = "Range-test-iframe.html"; 458 referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNode(true)); 459 } 460 actualIframe.src = "Range-test-iframe.html"; 461 </script>