| OLD | NEW |
| (Empty) | |
| 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 (lik
e |
| 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 && (range.startContainer.nodeType == Node.TEXT_NODE |
| 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 Co
mment node:" |
| 135 if (firstPartiallyContainedChild |
| 136 && (firstPartiallyContainedChild.nodeType == Node.TEXT_NODE |
| 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 Com
ment node:" |
| 191 if (lastPartiallyContainedChild |
| 192 && (lastPartiallyContainedChild.nodeType == Node.TEXT_NODE |
| 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.cre
ateDocumentType("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 cloneContent
s()"); |
| 277 assert_equals(expectedIframe.contentWindow.unexpectedException, null, |
| 278 "Unexpected exception thrown when setting up Range for simulated cloneCont
ents()"); |
| 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(expectedFrag, function() { |
| 316 actualRange.cloneContents(); |
| 317 }); |
| 318 } else { |
| 319 actualFrag = actualRange.cloneContents(); |
| 320 } |
| 321 |
| 322 actualRoots = []; |
| 323 for (var j = 0; j < actualAllNodes.length; j++) { |
| 324 if (!actualAllNodes[j].parentNode) { |
| 325 actualRoots.push(actualAllNodes[j]); |
| 326 } |
| 327 } |
| 328 |
| 329 expectedRoots = []; |
| 330 for (var j = 0; j < expectedAllNodes.length; j++) { |
| 331 if (!expectedAllNodes[j].parentNode) { |
| 332 expectedRoots.push(expectedAllNodes[j]); |
| 333 } |
| 334 } |
| 335 |
| 336 for (var j = 0; j < actualRoots.length; j++) { |
| 337 assertNodesEqual(actualRoots[j], expectedRoots[j], j ? "detached node #" +
j : "tree root"); |
| 338 |
| 339 if (j == 0) { |
| 340 // Clearly something is wrong if the node lists are different |
| 341 // lengths. We want to report this only after we've already |
| 342 // checked the main tree for equality, though, so it doesn't |
| 343 // mask more interesting errors. |
| 344 assert_equals(actualRoots.length, expectedRoots.length, |
| 345 "Actual and expected DOMs were broken up into a different number of pi
eces by cloneContents() (this probably means you created or detached nodes when
you weren't supposed to)"); |
| 346 } |
| 347 } |
| 348 }); |
| 349 domTests[i].done(); |
| 350 |
| 351 positionTests[i].step(function() { |
| 352 assert_equals(actualIframe.contentWindow.unexpectedException, null, |
| 353 "Unexpected exception thrown when setting up Range for actual cloneContent
s()"); |
| 354 assert_equals(expectedIframe.contentWindow.unexpectedException, null, |
| 355 "Unexpected exception thrown when setting up Range for simulated cloneCont
ents()"); |
| 356 assert_equals(typeof actualRange, "object", |
| 357 "typeof Range produced in actual iframe"); |
| 358 assert_equals(typeof expectedRange, "object", |
| 359 "typeof Range produced in expected iframe"); |
| 360 |
| 361 assert_true(actualRoots[0].isEqualNode(expectedRoots[0]), |
| 362 "The resulting DOMs were not equal, so comparing positions makes no sense"
); |
| 363 |
| 364 if (typeof expectedFrag == "string") { |
| 365 // It's no longer true that, e.g., startContainer and endContainer |
| 366 // must always be the same |
| 367 return; |
| 368 } |
| 369 |
| 370 assert_equals(actualRange.startOffset, expectedRange.startOffset, |
| 371 "Unexpected startOffset after cloneContents()"); |
| 372 // How do we decide that the two nodes are equal, since they're in |
| 373 // different trees? Since the DOMs are the same, it's enough to check |
| 374 // that the index in the parent is the same all the way up the tree. |
| 375 // But we can first cheat by just checking they're actually equal. |
| 376 assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContai
ner), |
| 377 "Unexpected startContainer after cloneContents(), expected " + |
| 378 expectedRange.startContainer.nodeName.toLowerCase() + " but got " + |
| 379 actualRange.startContainer.nodeName.toLowerCase()); |
| 380 var currentActual = actualRange.startContainer; |
| 381 var currentExpected = expectedRange.startContainer; |
| 382 var actual = ""; |
| 383 var expected = ""; |
| 384 while (currentActual && currentExpected) { |
| 385 actual = indexOf(currentActual) + "-" + actual; |
| 386 expected = indexOf(currentExpected) + "-" + expected; |
| 387 |
| 388 currentActual = currentActual.parentNode; |
| 389 currentExpected = currentExpected.parentNode; |
| 390 } |
| 391 actual = actual.substr(0, actual.length - 1); |
| 392 expected = expected.substr(0, expected.length - 1); |
| 393 assert_equals(actual, expected, |
| 394 "startContainer superficially looks right but is actually the wrong node i
f you trace back its index in all its ancestors (I'm surprised this actually hap
pened"); |
| 395 }); |
| 396 positionTests[i].done(); |
| 397 |
| 398 fragTests[i].step(function() { |
| 399 assert_equals(actualIframe.contentWindow.unexpectedException, null, |
| 400 "Unexpected exception thrown when setting up Range for actual cloneContent
s()"); |
| 401 assert_equals(expectedIframe.contentWindow.unexpectedException, null, |
| 402 "Unexpected exception thrown when setting up Range for simulated cloneCont
ents()"); |
| 403 assert_equals(typeof actualRange, "object", |
| 404 "typeof Range produced in actual iframe"); |
| 405 assert_equals(typeof expectedRange, "object", |
| 406 "typeof Range produced in expected iframe"); |
| 407 |
| 408 if (typeof expectedFrag == "string") { |
| 409 // Comparing makes no sense |
| 410 return; |
| 411 } |
| 412 assertNodesEqual(actualFrag, expectedFrag, |
| 413 "returned fragment"); |
| 414 }); |
| 415 fragTests[i].done(); |
| 416 } |
| 417 |
| 418 // First test a Range that has the no-op detach() called on it, synchronously |
| 419 test(function() { |
| 420 var range = document.createRange(); |
| 421 range.detach(); |
| 422 assert_array_equals(range.cloneContents().childNodes, []); |
| 423 }, "Range.detach()"); |
| 424 |
| 425 var iStart = 0; |
| 426 var iStop = testRanges.length; |
| 427 |
| 428 if (/subtest=[0-9]+/.test(location.search)) { |
| 429 var matches = /subtest=([0-9]+)/.exec(location.search); |
| 430 iStart = Number(matches[1]); |
| 431 iStop = Number(matches[1]) + 1; |
| 432 } |
| 433 |
| 434 var domTests = []; |
| 435 var positionTests = []; |
| 436 var fragTests = []; |
| 437 |
| 438 for (var i = iStart; i < iStop; i++) { |
| 439 domTests[i] = async_test("Resulting DOM for range " + i + " " + testRanges[i])
; |
| 440 positionTests[i] = async_test("Resulting cursor position for range " + i + " "
+ testRanges[i]); |
| 441 fragTests[i] = async_test("Returned fragment for range " + i + " " + testRange
s[i]); |
| 442 } |
| 443 |
| 444 var referenceDoc = document.implementation.createHTMLDocument(""); |
| 445 referenceDoc.removeChild(referenceDoc.documentElement); |
| 446 |
| 447 actualIframe.onload = function() { |
| 448 expectedIframe.onload = function() { |
| 449 for (var i = iStart; i < iStop; i++) { |
| 450 testCloneContents(i); |
| 451 } |
| 452 } |
| 453 expectedIframe.src = "Range-test-iframe.html"; |
| 454 referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNod
e(true)); |
| 455 } |
| 456 actualIframe.src = "Range-test-iframe.html"; |
| 457 </script> |
| OLD | NEW |