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