summaryrefslogtreecommitdiffstats
path: root/dom/base/test/test_content_iterator_subtree_shadow_tree.html
blob: 033aaf80a79bfd4772bd904dfb8ecb12f37bfba7 (plain)
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>