test_content_iterator_subtree_shadow_tree.html (14229B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>Test for content subtree iterator with ShadowDOM involved</title> 6 <script src="/tests/SimpleTest/SimpleTest.js"></script> 7 <link rel="stylesheet" href="/tests/SimpleTest/test.css"> 8 <script> 9 var Cc = SpecialPowers.Cc; 10 var Ci = SpecialPowers.Ci; 11 function finish() { 12 // The SimpleTest may require usual elements in the template, but they shouldn't be during test. 13 // So, let's create them at end of the test. 14 document.body.innerHTML = '<div id="display"></div><div id="content"></div><pre id="test"></pre>'; 15 SimpleTest.finish(); 16 } 17 18 function createContentIterator() { 19 return Cc["@mozilla.org/scriptable-content-iterator;1"] 20 .createInstance(Ci.nsIScriptableContentIterator); 21 } 22 23 function getNodeDescription(aNode) { 24 if (aNode === undefined) { 25 return "undefine"; 26 } 27 if (aNode === null) { 28 return "null"; 29 } 30 function getElementDescription(aElement) { 31 if (aElement.host) { 32 aElement = aElement.host; 33 } 34 if (aElement.tagName === "BR") { 35 if (aElement.previousSibling) { 36 return `<br> element after ${getNodeDescription(aElement.previousSibling)}`; 37 } 38 return `<br> element in ${getElementDescription(aElement.parentElement)}`; 39 } 40 let hasHint = aElement == document.body; 41 let tag = `<${aElement.tagName.toLowerCase()}`; 42 if (aElement.getAttribute("id")) { 43 tag += ` id="${aElement.getAttribute("id")}"`; 44 hasHint = true; 45 } 46 if (aElement.getAttribute("class")) { 47 tag += ` class="${aElement.getAttribute("class")}"`; 48 hasHint = true; 49 } 50 if (aElement.getAttribute("type")) { 51 tag += ` type="${aElement.getAttribute("type")}"`; 52 } 53 if (aElement.getAttribute("name")) { 54 tag += ` name="${aElement.getAttribute("name")}"`; 55 } 56 if (aElement.getAttribute("value")) { 57 tag += ` value="${aElement.getAttribute("value")}"`; 58 hasHint = true; 59 } 60 if (aElement.getAttribute("style")) { 61 tag += ` style="${aElement.getAttribute("style")}"`; 62 hasHint = true; 63 } 64 if (hasHint) { 65 return tag + ">"; 66 } 67 68 return `${tag}> in ${getElementDescription(aElement.parentElement || aElement.parentNode)}`; 69 } 70 switch (aNode.nodeType) { 71 case aNode.TEXT_NODE: 72 return `text node, "${aNode.wholeText.replace(/\n/g, '\\n')}"`; 73 case aNode.COMMENT_NODE: 74 return `comment node, "${aNode.data.replace(/\n/g, '\\n')}"`; 75 case aNode.ELEMENT_NODE: 76 return getElementDescription(SpecialPowers.unwrap(aNode)); 77 default: 78 return "unknown node"; 79 } 80 } 81 82 SimpleTest.waitForExplicitFinish(); 83 SimpleTest.waitForFocus(function () { 84 let iter = createContentIterator(); 85 86 function runTest() { 87 /** 88 * Basic tests with complicated tree. 89 */ 90 function check(aIter, aExpectedResult, aDescription) { 91 if (aExpectedResult.length) { 92 is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0], 93 `${aDescription}: currentNode should be the text node immediately after initialization (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`); 94 ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true immediately after initialization`); 95 96 aIter.first(); 97 is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0], 98 `${aDescription}: currentNode should be the text node after calling first() (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`); 99 ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true after calling first()`); 100 101 for (let expected of aExpectedResult) { 102 is(SpecialPowers.unwrap(aIter.currentNode), expected, 103 `${aDescription}: currentNode should be the node (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(expected)})`); 104 ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true when ${getNodeDescription(expected)} is expected`); 105 aIter.next(); 106 } 107 108 is(SpecialPowers.unwrap(aIter.currentNode), null, 109 `${aDescription}: currentNode should be null after calling next() finally (got: ${getNodeDescription(aIter.currentNode)}`); 110 ok(aIter.isDone, `${aDescription}: isDone should be true after calling next() finally`); 111 } else { 112 is(SpecialPowers.unwrap(aIter.currentNode), null, 113 `${aDescription}: currentNode should be null immediately after initialization (got: ${getNodeDescription(aIter.currentNode)})`); 114 ok(aIter.isDone, `${aDescription}: isDone should be true immediately after initialization`); 115 116 aIter.first(); 117 is(SpecialPowers.unwrap(aIter.currentNode), null, 118 `${aDescription}: currentNode should be null after calling first() (got: ${getNodeDescription(aIter.currentNode)})`); 119 ok(aIter.isDone, `${aDescription}: isDone should be true after calling first()`); 120 } 121 } 122 123 // Structure 124 // <div>OuterText1</div> 125 // <div #host1> 126 // #ShadowRoot 127 // InnerText1 128 // <div>OuterText2</div> 129 // <div #host2> 130 // #ShadowRoot 131 // <div>InnerText2</div> 132 // <div>InnerText3</div> 133 // <div #host3> 134 // #ShadowRoot 135 // <div #host4> 136 // #ShadowRoot 137 // InnerText4 138 // OuterText3 139 140 document.body.innerHTML = `<div id="outerText1">OuterText1</div>` + 141 `<div id="host1"></div>` + 142 `<div id="outerText2">OuterText2</div>` + 143 `<div id="host2"></div>` + 144 `<div id="host3"></div>` + 145 `OuterText3`; 146 const outerText1 = document.getElementById("outerText1"); 147 const outerText2 = document.getElementById("outerText2"); 148 149 const host1 = document.getElementById("host1"); 150 const root1 = host1.attachShadow({mode: "open"}); 151 root1.innerHTML = "InnerText1"; 152 153 const host2 = document.getElementById("host2"); 154 const root2 = host2.attachShadow({mode: "open"}); 155 root2.innerHTML = "<div>InnerText2</div><div>InnerText3</div>"; 156 157 const host3 = document.getElementById("host3"); 158 const root3 = host3.attachShadow({mode: "open"}); 159 root3.innerHTML = `<div id="host4"></div>`; 160 161 const host4 = root3.getElementById("host4"); 162 const root4 = host4.attachShadow({mode: "open"}); 163 root4.innerHTML = "InnerText4"; 164 165 /** 166 * Selects the <body> with a range. 167 */ 168 range = document.createRange(); 169 range.selectNode(document.body); 170 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 171 check(iter, [document.body], "Initialized with range selecting the <body>"); 172 173 /** 174 * Selects all children in the <body> with a range. 175 */ 176 range = document.createRange(); 177 range.selectNodeContents(document.body); 178 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 179 check(iter, [outerText1, host1, 180 outerText2, host2, 181 host3, // host4 is a child of host3 182 document.body.lastChild], 183 "Initialized with range selecting all children in the <body>"); 184 185 /** 186 * range around elements. 187 */ 188 range = document.createRange(); 189 SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1.firstChild, 0); 190 SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root1.firstChild, root1.firstChild.length); 191 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 192 // outerText1.firstChild is a node without children, so the 193 // next candidate is root1.firstChild, given root1.firstChild 194 // is also the end container which isn't fully contained 195 // by this range, so the iterator returns nothing. 196 check(iter, [], "Initialized with range selecting 'OuterText1 and InnerText1'"); 197 198 // From light DOM to Shadow DOM #1 199 range = document.createRange(); 200 SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1, 0); 201 SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root1.firstChild, root1.firstChild.length); 202 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 203 // [start] outerText1 is a container and it has children, so the first node 204 // is the topmost descendant, which is outerText.firstChild. 205 // [end] The end point of this iteration is also outerText1.firstChild because 206 // it is also the topmost element in the previous node of root1.firstChild. 207 // Iteration #1: outerText1.firstChild as it is the start node 208 check(iter, [outerText1.firstChild], "Initialized with range selecting 'OuterText1 and InnerText1'"); 209 210 // From light DOM to Shadow DOM #2 211 SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1, 0); 212 SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2, root2.childNodes.length); 213 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 214 // [start] outerText1 is a container and it has children, so the first node 215 // is the topmost descendant, which is outerText.firstChild. 216 // [end] root2 is the container and it has children, so the end node is 217 // the last node of root2, which is root2.lastChild 218 // Iteration #1: outerText1.firstChild, as it's the start node 219 // Iteration #2: host1, as it's next available node after outerText1.firstChild 220 // Iteration #3: outerText2, as it's the next sibiling of host1 221 // Iteration #4: host2, as it's the next sibling of outerText2. Since it's 222 // the ancestor of the end node, so we get into this tree and returns 223 // root2.firstChild here. 224 // Iteration #5: root2.lastChild, as it's the next sibling of root2.firstChild 225 check(iter, [outerText1.firstChild, host1, outerText2, root2.firstChild, root2.lastChild], 226 "Initialized with range selecting 'OuterText1, InnerText1, OuterText2 and InnerText2'"); 227 228 // From Shadow DOM to Shadow DOM #1 229 SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1.firstChild, 0); 230 SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2.lastChild, root2.lastChild.length); 231 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 232 // [start] outerText2 is the start because root1.firstChild doesn't have children, 233 // so we look for next available node which is outerText2. 234 // [end] root2.lastChild is the end container, so we look for previous 235 // nodes and get root2.firstChild 236 // Iteration #1: outerText2, as it's the start node 237 // Iteration #2: host2, as it's the next sibling of outerText2. Since it's 238 // the ancestor of the end node, so we get into this tree and returns 239 // root2.firstChild here. 240 check(iter, [outerText2, root2.firstChild], "Initialized with range selecting 'InnerText1, OuterText2 and InnerText2'"); 241 242 // From Shadow DOM to Shadow DOM #2 243 SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1, 0); 244 SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2.lastChild, root2.lastChild.length); 245 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 246 // [start] root1 is the start container and it has children, so the first node 247 // is the topmost descendant, which is root1.firstChild. 248 // [end] root2.lastChild is the end container, so we look for previous 249 // nodes and get root2.firstChild 250 // Iteration #1: root1.firstChild, as it's the start node 251 // Iteration #2: outerText2, as it's the next available node 252 // Iteration #3: host2, as it's the next sibling of outerText2. Since it's 253 // the ancestor of the end node, so we get into this tree and returns 254 // root2.firstChild here. 255 check(iter, [root1.firstChild, outerText2, root2.firstChild], "Initialized with range selecting 'InnerText1, OuterText2 and InnerText2'"); 256 257 SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1.firstChild, 1); 258 SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root4.firstChild, root4.firstChild.length); 259 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 260 // [start] outerText2 is the start because root1.firstChild doesn't have children, 261 // so we look for next available node which is outerText2. 262 // [end] host2 is the end container, so we look for previous 263 // nodes root4.firstChild and eventually get host2. 264 // Iteration #1: outerText2, as it's the start node 265 // Iteration #2: host2, as it's the next sibling of outerText2 266 check(iter, [outerText2, host2], "Initialized with range selecting 'InnerText1, OuterText2, InnerText2 and InnerText3'"); 267 268 // From light to light 269 SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1.firstChild, 0); 270 SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(document.body.lastChild, document.body.lastChild.length); 271 iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); 272 // [start] host1 is the start because it's the next available node of 273 // outerText1.firstChild. 274 // [end] host3 is the end because the previous node of document.body.lastChild is host3. 275 // Iteration #1: host1, as it's the start node 276 // Iteration #2: outerText2, as it's the next sibling of host1 277 // Iteration #3: host2, as it's the next sibling of outerText2 278 // Iteration #4: host3, as it's the next sibling of host2 279 check(iter, [host1, outerText2, host2, host3], 280 "Initialized with range selecting 'OuterText1, InnerText1, OuterText2, InnerText2, InnerText3 and OuterText3'"); 281 282 finish(); 283 } 284 285 SpecialPowers.pushPrefEnv({"set": [["dom.shadowdom.selection_across_boundary.enabled", true]]}, runTest); 286 }); 287 </script> 288 </head> 289 <body></body> 290 </html>