summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js
blob: 60c2bec0c8aa213b1bfa1d75c99ca1745cb9871c (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
// These tests ensure that:
//   1. The HTML element insertion steps for iframes [1] run *after* all DOM
//      insertion mutations associated with any given call to
//      #concept-node-insert [2] (which may insert many elements at once).
//      Consequently, a preceding element's insertion steps can observe the
//      side-effects of later elements being connected to the DOM, but cannot
//      observe the side-effects of the later element's own insertion steps [1],
//      since insertion steps are run in order after all DOM insertion mutations
//      are complete.
//   2. The HTML element removing steps for iframes [3] *do not* synchronously
//      run script during child navigable destruction. Therefore, script cannot
//      observe the state of the DOM in the middle of iframe removal, even when
//      multiple iframes are being removed in the same task. Iframe removal,
//      from the perspective of the parent's DOM tree, is atomic.
//
// [1]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-insertion-steps
// [2]: https://dom.spec.whatwg.org/#concept-node-insert
// [3]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-removing-steps

promise_test(async t => {
  const fragment = new DocumentFragment();

  const iframe1 = fragment.appendChild(document.createElement('iframe'));
  const iframe2 = fragment.appendChild(document.createElement('iframe'));

  t.add_cleanup(() => {
    iframe1.remove();
    iframe2.remove();
  });

  let iframe1Loaded = false, iframe2Loaded = false;
  iframe1.onload = e => {
    // iframe1 assertions:
    iframe1Loaded = true;
    assert_equals(window.frames.length, 1,
        "iframe1 load event can observe its own participation in the frame " +
        "tree");
    assert_equals(iframe1.contentWindow, window.frames[0]);

    // iframe2 assertions:
    assert_false(iframe2Loaded,
        "iframe2's load event hasn't fired before iframe1's");
    assert_true(iframe2.isConnected,
        "iframe1 can observe that iframe2 is connected to the DOM...");
    assert_equals(iframe2.contentWindow, null,
        "... but iframe1 cannot observe iframe2's contentWindow because " +
        "iframe2's insertion steps have not been run yet");
  };

  iframe2.onload = e => {
    iframe2Loaded = true;
    assert_equals(window.frames.length, 2,
        "iframe2 load event can observe its own participation in the frame tree");
    assert_equals(iframe1.contentWindow, window.frames[0]);
    assert_equals(iframe2.contentWindow, window.frames[1]);
  };

  // Synchronously consecutively adds both `iframe1` and `iframe2` to the DOM,
  // invoking their insertion steps (and thus firing each of their `load`
  // events) in order. `iframe1` will be able to observe itself in the DOM but
  // not `iframe2`, and `iframe2` will be able to observe both itself and
  // `iframe1`.
  document.body.append(fragment);
  assert_true(iframe1Loaded, "iframe1 loaded");
  assert_true(iframe2Loaded, "iframe2 loaded");
}, "Insertion steps: load event fires synchronously *after* iframe DOM " +
   "insertion, as part of the iframe element's insertion steps");

// There are several versions of the removal variant, since there are several
// ways to remove multiple elements "at once". For example:
//   1. `node.innerHTML = ''` ultimately runs
//      https://dom.spec.whatwg.org/#concept-node-replace-all which removes all
//      of a node's children.
//   2. `node.replaceChildren()` which follows roughly the same path above.
//   3. `node.remove()` on a parent of many children will invoke not the DOM
//      remove algorithm, but rather the "removing steps" hook [1], for each
//      child.
//
// [1]: https://dom.spec.whatwg.org/#concept-node-remove-ext

function runRemovalTest(removal_method) {
  promise_test(async t => {
    const div = document.createElement('div');

    const iframe1 = div.appendChild(document.createElement('iframe'));
    const iframe2 = div.appendChild(document.createElement('iframe'));
    document.body.append(div);

    // Now that both iframes have been inserted into the DOM, we'll set up a
    // MutationObserver that we'll use to ensure that multiple synchronous
    // mutations (removals) are only observed atomically at the end. Specifically,
    // the observer's callback is not invoked synchronously for each removal.
    let observerCallbackInvoked = false;
    const removalObserver = new MutationObserver(mutations => {
      assert_false(observerCallbackInvoked,
          "MO callback is only invoked once, not multiple times, i.e., for " +
          "each removal");
      observerCallbackInvoked = true;
      assert_equals(mutations.length, 1, "Exactly one MutationRecord is recorded");
      assert_equals(mutations[0].removedNodes.length, 2);
      assert_equals(window.frames.length, 0,
          "No iframe Windows exist when the MO callback is run");
      assert_equals(document.querySelector('iframe'), null,
          "No iframe elements are connected to the DOM when the MO callback is " +
          "run");
    });

    removalObserver.observe(div, {childList: true});
    t.add_cleanup(() => removalObserver.disconnect());

    let iframe1UnloadFired = false, iframe2UnloadFired = false;
    let iframe1PagehideFired = false, iframe2PagehideFired = false;
    iframe1.contentWindow.addEventListener('pagehide', e => {
      assert_false(iframe1UnloadFired, "iframe1 pagehide fires before unload");
      iframe1PagehideFired = true;
    });
    iframe2.contentWindow.addEventListener('pagehide', e => {
      assert_false(iframe2UnloadFired, "iframe2 pagehide fires before unload");
      iframe2PagehideFired = true;
    });
    iframe1.contentWindow.addEventListener('unload', e => iframe1UnloadFired = true);
    iframe2.contentWindow.addEventListener('unload', e => iframe2UnloadFired = true);

    // Each `removal_method` will trigger the synchronous removal of each of
    // `div`'s (iframe) children. This will synchronously, consecutively
    // invoke HTML's "destroy a child navigable" (per [1]), for each iframe.
    //
    // [1]: https://html.spec.whatwg.org/C#the-iframe-element:destroy-a-child-navigable

    if (removal_method === 'replaceChildren') {
      div.replaceChildren();
    } else if (removal_method === 'remove') {
      div.remove();
    } else if (removal_method === 'innerHTML') {
      div.innerHTML = '';
    }

    assert_false(iframe1PagehideFired, "iframe1 pagehide did not fire");
    assert_false(iframe2PagehideFired, "iframe2 pagehide did not fire");
    assert_false(iframe1UnloadFired, "iframe1 unload did not fire");
    assert_false(iframe2UnloadFired, "iframe2 unload did not fire");

    assert_false(observerCallbackInvoked,
        "MO callback is not invoked synchronously after removals");

    // Wait one microtask.
    await Promise.resolve();

    if (removal_method !== 'remove') {
      assert_true(observerCallbackInvoked,
          "MO callback is invoked asynchronously after removals");
    }
  }, `Removing steps (${removal_method}): script does not run synchronously during iframe destruction`);
}

runRemovalTest('innerHTML');
runRemovalTest('replaceChildren');
runRemovalTest('remove');