Bug 1881960 [wpt PR 44774] - DOM: Add WPTs for removing steps, a=testonly
[gecko.git] / testing / web-platform / tests / dom / nodes / insertion-removing-steps / insertion-removing-steps-iframe.window.js
blob60c2bec0c8aa213b1bfa1d75c99ca1745cb9871c
1 // These tests ensure that:
2 //   1. The HTML element insertion steps for iframes [1] run *after* all DOM
3 //      insertion mutations associated with any given call to
4 //      #concept-node-insert [2] (which may insert many elements at once).
5 //      Consequently, a preceding element's insertion steps can observe the
6 //      side-effects of later elements being connected to the DOM, but cannot
7 //      observe the side-effects of the later element's own insertion steps [1],
8 //      since insertion steps are run in order after all DOM insertion mutations
9 //      are complete.
10 //   2. The HTML element removing steps for iframes [3] *do not* synchronously
11 //      run script during child navigable destruction. Therefore, script cannot
12 //      observe the state of the DOM in the middle of iframe removal, even when
13 //      multiple iframes are being removed in the same task. Iframe removal,
14 //      from the perspective of the parent's DOM tree, is atomic.
16 // [1]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-insertion-steps
17 // [2]: https://dom.spec.whatwg.org/#concept-node-insert
18 // [3]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-removing-steps
20 promise_test(async t => {
21   const fragment = new DocumentFragment();
23   const iframe1 = fragment.appendChild(document.createElement('iframe'));
24   const iframe2 = fragment.appendChild(document.createElement('iframe'));
26   t.add_cleanup(() => {
27     iframe1.remove();
28     iframe2.remove();
29   });
31   let iframe1Loaded = false, iframe2Loaded = false;
32   iframe1.onload = e => {
33     // iframe1 assertions:
34     iframe1Loaded = true;
35     assert_equals(window.frames.length, 1,
36         "iframe1 load event can observe its own participation in the frame " +
37         "tree");
38     assert_equals(iframe1.contentWindow, window.frames[0]);
40     // iframe2 assertions:
41     assert_false(iframe2Loaded,
42         "iframe2's load event hasn't fired before iframe1's");
43     assert_true(iframe2.isConnected,
44         "iframe1 can observe that iframe2 is connected to the DOM...");
45     assert_equals(iframe2.contentWindow, null,
46         "... but iframe1 cannot observe iframe2's contentWindow because " +
47         "iframe2's insertion steps have not been run yet");
48   };
50   iframe2.onload = e => {
51     iframe2Loaded = true;
52     assert_equals(window.frames.length, 2,
53         "iframe2 load event can observe its own participation in the frame tree");
54     assert_equals(iframe1.contentWindow, window.frames[0]);
55     assert_equals(iframe2.contentWindow, window.frames[1]);
56   };
58   // Synchronously consecutively adds both `iframe1` and `iframe2` to the DOM,
59   // invoking their insertion steps (and thus firing each of their `load`
60   // events) in order. `iframe1` will be able to observe itself in the DOM but
61   // not `iframe2`, and `iframe2` will be able to observe both itself and
62   // `iframe1`.
63   document.body.append(fragment);
64   assert_true(iframe1Loaded, "iframe1 loaded");
65   assert_true(iframe2Loaded, "iframe2 loaded");
66 }, "Insertion steps: load event fires synchronously *after* iframe DOM " +
67    "insertion, as part of the iframe element's insertion steps");
69 // There are several versions of the removal variant, since there are several
70 // ways to remove multiple elements "at once". For example:
71 //   1. `node.innerHTML = ''` ultimately runs
72 //      https://dom.spec.whatwg.org/#concept-node-replace-all which removes all
73 //      of a node's children.
74 //   2. `node.replaceChildren()` which follows roughly the same path above.
75 //   3. `node.remove()` on a parent of many children will invoke not the DOM
76 //      remove algorithm, but rather the "removing steps" hook [1], for each
77 //      child.
79 // [1]: https://dom.spec.whatwg.org/#concept-node-remove-ext
81 function runRemovalTest(removal_method) {
82   promise_test(async t => {
83     const div = document.createElement('div');
85     const iframe1 = div.appendChild(document.createElement('iframe'));
86     const iframe2 = div.appendChild(document.createElement('iframe'));
87     document.body.append(div);
89     // Now that both iframes have been inserted into the DOM, we'll set up a
90     // MutationObserver that we'll use to ensure that multiple synchronous
91     // mutations (removals) are only observed atomically at the end. Specifically,
92     // the observer's callback is not invoked synchronously for each removal.
93     let observerCallbackInvoked = false;
94     const removalObserver = new MutationObserver(mutations => {
95       assert_false(observerCallbackInvoked,
96           "MO callback is only invoked once, not multiple times, i.e., for " +
97           "each removal");
98       observerCallbackInvoked = true;
99       assert_equals(mutations.length, 1, "Exactly one MutationRecord is recorded");
100       assert_equals(mutations[0].removedNodes.length, 2);
101       assert_equals(window.frames.length, 0,
102           "No iframe Windows exist when the MO callback is run");
103       assert_equals(document.querySelector('iframe'), null,
104           "No iframe elements are connected to the DOM when the MO callback is " +
105           "run");
106     });
108     removalObserver.observe(div, {childList: true});
109     t.add_cleanup(() => removalObserver.disconnect());
111     let iframe1UnloadFired = false, iframe2UnloadFired = false;
112     let iframe1PagehideFired = false, iframe2PagehideFired = false;
113     iframe1.contentWindow.addEventListener('pagehide', e => {
114       assert_false(iframe1UnloadFired, "iframe1 pagehide fires before unload");
115       iframe1PagehideFired = true;
116     });
117     iframe2.contentWindow.addEventListener('pagehide', e => {
118       assert_false(iframe2UnloadFired, "iframe2 pagehide fires before unload");
119       iframe2PagehideFired = true;
120     });
121     iframe1.contentWindow.addEventListener('unload', e => iframe1UnloadFired = true);
122     iframe2.contentWindow.addEventListener('unload', e => iframe2UnloadFired = true);
124     // Each `removal_method` will trigger the synchronous removal of each of
125     // `div`'s (iframe) children. This will synchronously, consecutively
126     // invoke HTML's "destroy a child navigable" (per [1]), for each iframe.
127     //
128     // [1]: https://html.spec.whatwg.org/C#the-iframe-element:destroy-a-child-navigable
130     if (removal_method === 'replaceChildren') {
131       div.replaceChildren();
132     } else if (removal_method === 'remove') {
133       div.remove();
134     } else if (removal_method === 'innerHTML') {
135       div.innerHTML = '';
136     }
138     assert_false(iframe1PagehideFired, "iframe1 pagehide did not fire");
139     assert_false(iframe2PagehideFired, "iframe2 pagehide did not fire");
140     assert_false(iframe1UnloadFired, "iframe1 unload did not fire");
141     assert_false(iframe2UnloadFired, "iframe2 unload did not fire");
143     assert_false(observerCallbackInvoked,
144         "MO callback is not invoked synchronously after removals");
146     // Wait one microtask.
147     await Promise.resolve();
149     if (removal_method !== 'remove') {
150       assert_true(observerCallbackInvoked,
151           "MO callback is invoked asynchronously after removals");
152     }
153   }, `Removing steps (${removal_method}): script does not run synchronously during iframe destruction`);
156 runRemovalTest('innerHTML');
157 runRemovalTest('replaceChildren');
158 runRemovalTest('remove');