Range-deleteContents.html (14433B)
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 (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 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.createDocumentType("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 && (isText(range.startContainer) 76 || range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE 77 || range.startContainer.nodeType == Node.COMMENT_NODE)) { 78 originalStartNode.deleteData(originalStartOffset, originalEndOffset - originalStartOffset); 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) & Node.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 (isText(originalStartNode) 130 || originalStartNode.nodeType == Node.PROCESSING_INSTRUCTION_NODE 131 || originalStartNode.nodeType == Node.COMMENT_NODE) { 132 originalStartNode.deleteData(originalStartOffset, nodeLength(originalStartNode) - 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 (isText(originalEndNode) 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 deleteContents()"); 166 assert_equals(expectedIframe.contentWindow.unexpectedException, null, 167 "Unexpected exception thrown when setting up Range for simulated deleteContents()"); 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 pieces 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 deleteContents()"); 253 assert_equals(expectedIframe.contentWindow.unexpectedException, null, 254 "Unexpected exception thrown when setting up Range for simulated deleteContents()"); 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 deleteContents()"); 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 same after deleteContents()"); 268 assert_equals(expectedRange.startOffset, expectedRange.endOffset, 269 "Test bug! Expected startOffset and endOffset must always be the same after 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.startContainer), 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 if you trace back its index in all its ancestors (I'm surprised this actually happened"); 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.cloneNode(true)); 335 } 336 actualIframe.src = "Range-test-iframe.html"; 337 </script>