| OLD | NEW |
| (Empty) | |
| 1 <!doctype html> |
| 2 <title>Range.deleteContents() 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 restoreIframe(iframe, i) { |
| 26 // Most of this function is designed to work around the fact that Opera |
| 27 // doesn't let you add a doctype to a document that no longer has one, in |
| 28 // any way I can figure out. I eventually compromised on something that |
| 29 // will still let Opera pass most tests that don't actually involve |
| 30 // doctypes. |
| 31 while (iframe.contentDocument.firstChild |
| 32 && iframe.contentDocument.firstChild.nodeType != Node.DOCUMENT_TYPE_NODE) { |
| 33 iframe.contentDocument.removeChild(iframe.contentDocument.firstChild); |
| 34 } |
| 35 |
| 36 while (iframe.contentDocument.lastChild |
| 37 && iframe.contentDocument.lastChild.nodeType != Node.DOCUMENT_TYPE_NODE) { |
| 38 iframe.contentDocument.removeChild(iframe.contentDocument.lastChild); |
| 39 } |
| 40 |
| 41 if (!iframe.contentDocument.firstChild) { |
| 42 // This will throw an exception in Opera if we reach here, which is why |
| 43 // I try to avoid it. It will never happen in a browser that obeys the |
| 44 // spec, so it's really just insurance. I don't think it actually gets |
| 45 // hit by anything. |
| 46 iframe.contentDocument.appendChild(iframe.contentDocument.implementation.cre
ateDocumentType("html", "", "")); |
| 47 } |
| 48 iframe.contentDocument.appendChild(referenceDoc.documentElement.cloneNode(true
)); |
| 49 iframe.contentWindow.setupRangeTests(); |
| 50 iframe.contentWindow.testRangeInput = testRanges[i]; |
| 51 iframe.contentWindow.run(); |
| 52 } |
| 53 |
| 54 function myDeleteContents(range) { |
| 55 // "If the context object's start and end are the same, abort this method." |
| 56 if (range.startContainer == range.endContainer |
| 57 && range.startOffset == range.endOffset) { |
| 58 return; |
| 59 } |
| 60 |
| 61 // "Let original start node, original start offset, original end node, and |
| 62 // original end offset be the context object's start and end nodes and |
| 63 // offsets, respectively." |
| 64 var originalStartNode = range.startContainer; |
| 65 var originalStartOffset = range.startOffset; |
| 66 var originalEndNode = range.endContainer; |
| 67 var originalEndOffset = range.endOffset; |
| 68 |
| 69 // "If original start node and original end node are the same, and they are |
| 70 // a Text, ProcessingInstruction, or Comment node, replace data with node |
| 71 // original start node, offset original start offset, count original end |
| 72 // offset minus original start offset, and data the empty string, and then |
| 73 // terminate these steps" |
| 74 if (originalStartNode == originalEndNode |
| 75 && (range.startContainer.nodeType == Node.TEXT_NODE |
| 76 || range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE |
| 77 || range.startContainer.nodeType == Node.COMMENT_NODE)) { |
| 78 originalStartNode.deleteData(originalStartOffset, originalEndOffset - origin
alStartOffset); |
| 79 return; |
| 80 } |
| 81 |
| 82 // "Let nodes to remove be a list of all the Nodes that are contained in |
| 83 // the context object, in tree order, omitting any Node whose parent is |
| 84 // also contained in the context object." |
| 85 // |
| 86 // We rely on the fact that the contained nodes must lie in tree order |
| 87 // between the start node, and the end node's last descendant (inclusive). |
| 88 var nodesToRemove = []; |
| 89 var stop = nextNodeDescendants(range.endContainer); |
| 90 for (var node = range.startContainer; node != stop; node = nextNode(node)) { |
| 91 if (isContained(node, range) |
| 92 && !(node.parentNode && isContained(node.parentNode, range))) { |
| 93 nodesToRemove.push(node); |
| 94 } |
| 95 } |
| 96 |
| 97 // "If original start node is an ancestor container of original end node, |
| 98 // set new node to original start node and new offset to original start |
| 99 // offset." |
| 100 var newNode; |
| 101 var newOffset; |
| 102 if (originalStartNode == originalEndNode |
| 103 || originalEndNode.compareDocumentPosition(originalStartNode) & Node.DOCUMENT_
POSITION_CONTAINS) { |
| 104 newNode = originalStartNode; |
| 105 newOffset = originalStartOffset; |
| 106 // "Otherwise:" |
| 107 } else { |
| 108 // "Let reference node equal original start node." |
| 109 var referenceNode = originalStartNode; |
| 110 |
| 111 // "While reference node's parent is not null and is not an ancestor |
| 112 // container of original end node, set reference node to its parent." |
| 113 while (referenceNode.parentNode |
| 114 && referenceNode.parentNode != originalEndNode |
| 115 && !(originalEndNode.compareDocumentPosition(referenceNode.parentNode) & Nod
e.DOCUMENT_POSITION_CONTAINS)) { |
| 116 referenceNode = referenceNode.parentNode; |
| 117 } |
| 118 |
| 119 // "Set new node to the parent of reference node, and new offset to one |
| 120 // plus the index of reference node." |
| 121 newNode = referenceNode.parentNode; |
| 122 newOffset = 1 + indexOf(referenceNode); |
| 123 } |
| 124 |
| 125 // "If original start node is a Text, ProcessingInstruction, or Comment node, |
| 126 // replace data with node original start node, offset original start offset, |
| 127 // count original start node's length minus original start offset, data the |
| 128 // empty start" |
| 129 if (originalStartNode.nodeType == Node.TEXT_NODE |
| 130 || originalStartNode.nodeType == Node.PROCESSING_INSTRUCTION_NODE |
| 131 || originalStartNode.nodeType == Node.COMMENT_NODE) { |
| 132 originalStartNode.deleteData(originalStartOffset, nodeLength(originalStartNo
de) - originalStartOffset); |
| 133 } |
| 134 |
| 135 // "For each node in nodes to remove, in order, remove node from its |
| 136 // parent." |
| 137 for (var i = 0; i < nodesToRemove.length; i++) { |
| 138 nodesToRemove[i].parentNode.removeChild(nodesToRemove[i]); |
| 139 } |
| 140 |
| 141 // "If original end node is a Text, ProcessingInstruction, or Comment node, |
| 142 // replace data with node original end node, offset 0, count original end |
| 143 // offset, and data the empty string." |
| 144 if (originalEndNode.nodeType == Node.TEXT_NODE |
| 145 || originalEndNode.nodeType == Node.PROCESSING_INSTRUCTION_NODE |
| 146 || originalEndNode.nodeType == Node.COMMENT_NODE) { |
| 147 originalEndNode.deleteData(0, originalEndOffset); |
| 148 } |
| 149 |
| 150 // "Set the context object's start and end to (new node, new offset)." |
| 151 range.setStart(newNode, newOffset); |
| 152 range.setEnd(newNode, newOffset); |
| 153 } |
| 154 |
| 155 function testDeleteContents(i) { |
| 156 restoreIframe(actualIframe, i); |
| 157 restoreIframe(expectedIframe, i); |
| 158 |
| 159 var actualRange = actualIframe.contentWindow.testRange; |
| 160 var expectedRange = expectedIframe.contentWindow.testRange; |
| 161 var actualRoots, expectedRoots; |
| 162 |
| 163 domTests[i].step(function() { |
| 164 assert_equals(actualIframe.contentWindow.unexpectedException, null, |
| 165 "Unexpected exception thrown when setting up Range for actual deleteConten
ts()"); |
| 166 assert_equals(expectedIframe.contentWindow.unexpectedException, null, |
| 167 "Unexpected exception thrown when setting up Range for simulated deleteCon
tents()"); |
| 168 assert_equals(typeof actualRange, "object", |
| 169 "typeof Range produced in actual iframe"); |
| 170 assert_equals(typeof expectedRange, "object", |
| 171 "typeof Range produced in expected iframe"); |
| 172 |
| 173 // Just to be pedantic, we'll test not only that the tree we're |
| 174 // modifying is the same in expected vs. actual, but also that all the |
| 175 // nodes originally in it were the same. Typically some nodes will |
| 176 // become detached when the algorithm is run, but they still exist and |
| 177 // references can still be kept to them, so they should also remain the |
| 178 // same. |
| 179 // |
| 180 // We initialize the list to all nodes, and later on remove all the |
| 181 // ones which still have parents, since the parents will presumably be |
| 182 // tested for isEqualNode() and checking the children would be |
| 183 // redundant. |
| 184 var actualAllNodes = []; |
| 185 var node = furthestAncestor(actualRange.startContainer); |
| 186 do { |
| 187 actualAllNodes.push(node); |
| 188 } while (node = nextNode(node)); |
| 189 |
| 190 var expectedAllNodes = []; |
| 191 var node = furthestAncestor(expectedRange.startContainer); |
| 192 do { |
| 193 expectedAllNodes.push(node); |
| 194 } while (node = nextNode(node)); |
| 195 |
| 196 actualRange.deleteContents(); |
| 197 myDeleteContents(expectedRange); |
| 198 |
| 199 actualRoots = []; |
| 200 for (var j = 0; j < actualAllNodes.length; j++) { |
| 201 if (!actualAllNodes[j].parentNode) { |
| 202 actualRoots.push(actualAllNodes[j]); |
| 203 } |
| 204 } |
| 205 |
| 206 expectedRoots = []; |
| 207 for (var j = 0; j < expectedAllNodes.length; j++) { |
| 208 if (!expectedAllNodes[j].parentNode) { |
| 209 expectedRoots.push(expectedAllNodes[j]); |
| 210 } |
| 211 } |
| 212 |
| 213 for (var j = 0; j < actualRoots.length; j++) { |
| 214 if (!actualRoots[j].isEqualNode(expectedRoots[j])) { |
| 215 var msg = j ? "detached node #" + j : "tree root"; |
| 216 msg = "Actual and expected mismatch for " + msg + ". "; |
| 217 |
| 218 // Find the specific error |
| 219 var actual = actualRoots[j]; |
| 220 var expected = expectedRoots[j]; |
| 221 |
| 222 while (actual && expected) { |
| 223 assert_equals(actual.nodeType, expected.nodeType, |
| 224 msg + "First difference: differing nodeType"); |
| 225 assert_equals(actual.nodeName, expected.nodeName, |
| 226 msg + "First difference: differing nodeName"); |
| 227 assert_equals(actual.nodeValue, expected.nodeValue, |
| 228 msg + 'First difference: differing nodeValue (nodeName = "' + actual
.nodeName + '")'); |
| 229 assert_equals(actual.childNodes.length, expected.childNodes.length, |
| 230 msg + 'First difference: differing number of children (nodeName = "'
+ actual.nodeName + '")'); |
| 231 actual = nextNode(actual); |
| 232 expected = nextNode(expected); |
| 233 } |
| 234 |
| 235 assert_unreached("DOMs were not equal but we couldn't figure out why"); |
| 236 } |
| 237 |
| 238 if (j == 0) { |
| 239 // Clearly something is wrong if the node lists are different |
| 240 // lengths. We want to report this only after we've already |
| 241 // checked the main tree for equality, though, so it doesn't |
| 242 // mask more interesting errors. |
| 243 assert_equals(actualRoots.length, expectedRoots.length, |
| 244 "Actual and expected DOMs were broken up into a different number of pi
eces by deleteContents() (this probably means you created or detached nodes when
you weren't supposed to)"); |
| 245 } |
| 246 } |
| 247 }); |
| 248 domTests[i].done(); |
| 249 |
| 250 positionTests[i].step(function() { |
| 251 assert_equals(actualIframe.contentWindow.unexpectedException, null, |
| 252 "Unexpected exception thrown when setting up Range for actual deleteConten
ts()"); |
| 253 assert_equals(expectedIframe.contentWindow.unexpectedException, null, |
| 254 "Unexpected exception thrown when setting up Range for simulated deleteCon
tents()"); |
| 255 assert_equals(typeof actualRange, "object", |
| 256 "typeof Range produced in actual iframe"); |
| 257 assert_equals(typeof expectedRange, "object", |
| 258 "typeof Range produced in expected iframe"); |
| 259 assert_true(actualRoots[0].isEqualNode(expectedRoots[0]), |
| 260 "The resulting DOMs were not equal, so comparing positions makes no sense"
); |
| 261 |
| 262 assert_equals(actualRange.startContainer, actualRange.endContainer, |
| 263 "startContainer and endContainer must always be the same after deleteConte
nts()"); |
| 264 assert_equals(actualRange.startOffset, actualRange.endOffset, |
| 265 "startOffset and endOffset must always be the same after deleteContents()"
); |
| 266 assert_equals(expectedRange.startContainer, expectedRange.endContainer, |
| 267 "Test bug! Expected startContainer and endContainer must always be the sa
me after deleteContents()"); |
| 268 assert_equals(expectedRange.startOffset, expectedRange.endOffset, |
| 269 "Test bug! Expected startOffset and endOffset must always be the same aft
er deleteContents()"); |
| 270 |
| 271 assert_equals(actualRange.startOffset, expectedRange.startOffset, |
| 272 "Unexpected startOffset after deleteContents()"); |
| 273 // How do we decide that the two nodes are equal, since they're in |
| 274 // different trees? Since the DOMs are the same, it's enough to check |
| 275 // that the index in the parent is the same all the way up the tree. |
| 276 // But we can first cheat by just checking they're actually equal. |
| 277 assert_true(actualRange.startContainer.isEqualNode(expectedRange.startContai
ner), |
| 278 "Unexpected startContainer after deleteContents(), expected " + |
| 279 expectedRange.startContainer.nodeName.toLowerCase() + " but got " + |
| 280 actualRange.startContainer.nodeName.toLowerCase()); |
| 281 var currentActual = actualRange.startContainer; |
| 282 var currentExpected = expectedRange.startContainer; |
| 283 var actual = ""; |
| 284 var expected = ""; |
| 285 while (currentActual && currentExpected) { |
| 286 actual = indexOf(currentActual) + "-" + actual; |
| 287 expected = indexOf(currentExpected) + "-" + expected; |
| 288 |
| 289 currentActual = currentActual.parentNode; |
| 290 currentExpected = currentExpected.parentNode; |
| 291 } |
| 292 actual = actual.substr(0, actual.length - 1); |
| 293 expected = expected.substr(0, expected.length - 1); |
| 294 assert_equals(actual, expected, |
| 295 "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"); |
| 296 }); |
| 297 positionTests[i].done(); |
| 298 } |
| 299 |
| 300 // First test a detached Range, synchronously |
| 301 test(function() { |
| 302 var range = document.createRange(); |
| 303 range.detach(); |
| 304 range.deleteContents(); |
| 305 }, "Detached Range"); |
| 306 |
| 307 var iStart = 0; |
| 308 var iStop = testRanges.length; |
| 309 |
| 310 if (/subtest=[0-9]+/.test(location.search)) { |
| 311 var matches = /subtest=([0-9]+)/.exec(location.search); |
| 312 iStart = Number(matches[1]); |
| 313 iStop = Number(matches[1]) + 1; |
| 314 } |
| 315 |
| 316 var domTests = []; |
| 317 var positionTests = []; |
| 318 |
| 319 for (var i = iStart; i < iStop; i++) { |
| 320 domTests[i] = async_test("Resulting DOM for range " + i + " " + testRanges[i])
; |
| 321 positionTests[i] = async_test("Resulting cursor position for range " + i + " "
+ testRanges[i]); |
| 322 } |
| 323 |
| 324 var referenceDoc = document.implementation.createHTMLDocument(""); |
| 325 referenceDoc.removeChild(referenceDoc.documentElement); |
| 326 |
| 327 actualIframe.onload = function() { |
| 328 expectedIframe.onload = function() { |
| 329 for (var i = iStart; i < iStop; i++) { |
| 330 testDeleteContents(i); |
| 331 } |
| 332 } |
| 333 expectedIframe.src = "Range-test-iframe.html"; |
| 334 referenceDoc.appendChild(actualIframe.contentDocument.documentElement.cloneNod
e(true)); |
| 335 } |
| 336 actualIframe.src = "Range-test-iframe.html"; |
| 337 </script> |
| OLD | NEW |