summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js')
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js158
1 files changed, 158 insertions, 0 deletions
diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js
new file mode 100644
index 0000000000..60c2bec0c8
--- /dev/null
+++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js
@@ -0,0 +1,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');