From bbb661d08356de65f63414e4fb86c0dfae141262 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Wed, 31 Jan 2024 11:20:04 -0800 Subject: [PATCH] DOM: Add iframe insertion & removal steps WPTs To help resolve https://github.com/whatwg/dom/issues/808, we need WPTs asserting exactly when (DOM-observing) script can and cannot be invoked during the insertion and removing steps for iframes. R=masonf@chromium.org Bug: N/A Change-Id: Iff959bbb0d32d772ae7162d5d9e54a5817959086 --- .../insertion-removal-steps-iframe.window.js | 100 ++++++++++++++++++ .../insertion-removal-steps-script.window.js | 45 ++++++++ 2 files changed, 145 insertions(+) create mode 100644 dom/nodes/insertion-removal-steps-iframe.window.js create mode 100644 dom/nodes/insertion-removal-steps-script.window.js diff --git a/dom/nodes/insertion-removal-steps-iframe.window.js b/dom/nodes/insertion-removal-steps-iframe.window.js new file mode 100644 index 000000000000000..6397f68a5e236c4 --- /dev/null +++ b/dom/nodes/insertion-removal-steps-iframe.window.js @@ -0,0 +1,100 @@ +// These tests ensure that: +// 1. The HTML element insertion steps for iframes [1] can synchronously run +// script during iframe insertion, which can observe an iframe's +// participation in the DOM tree mid-insertion. +// 2. The HTML element removing steps for iframes [2] *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://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 => { + 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.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 during iframe insertion steps"); + +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 are 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; + iframe1.contentWindow.addEventListener('unload', e => iframe1UnloadFired = true); + iframe2.contentWindow.addEventListener('unload', e => iframe2UnloadFired = true); + + // replaceChildren() 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 + div.replaceChildren(); + 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(); + + assert_true(observerCallbackInvoked, "MO callback is invoked asynchronously after removals"); +}, "Removing steps: script does not run synchronously during iframe destruction"); diff --git a/dom/nodes/insertion-removal-steps-script.window.js b/dom/nodes/insertion-removal-steps-script.window.js new file mode 100644 index 000000000000000..360c9cff5cd07b5 --- /dev/null +++ b/dom/nodes/insertion-removal-steps-script.window.js @@ -0,0 +1,45 @@ +promise_test(async t => { + const fragmentWithTwoScripts = new DocumentFragment(); + const script0 = document.createElement('script'); + const script1 = fragmentWithTwoScripts.appendChild(document.createElement('script')); + const script2 = fragmentWithTwoScripts.appendChild(document.createElement('script')); + + window.kBaselineNumberOfScripts = 3; + assert_equals(document.scripts.length, kBaselineNumberOfScripts, + "The WPT infra starts out with exactly 3 scripts"); + + window.script0Executed = false; + script0.innerText = ` + script0Executed = true; + assert_equals(document.scripts.length, kBaselineNumberOfScripts + 1, + 'script0 can observe itself and no other scripts'); + `; + + window.script1Executed = false; + script1.innerText = ` + script1Executed = true; + assert_equals(document.scripts.length, kBaselineNumberOfScripts + 2, + "script1 executes synchronously, and thus observes only itself and " + + "previous scripts"); + `; + + window.script2Executed = false; + script2.innerText = ` + script2Executed = true; + assert_equals(document.scripts.length, kBaselineNumberOfScripts + 3, + "script2 executes synchronously, and thus observes itself and all " + + "previous scripts"); + `; + + document.body.append(script0); + assert_true(script0Executed, + "Script0 executes synchronously during append()"); + + document.body.append(fragmentWithTwoScripts); + assert_true(script1Executed, + "Script1 executes synchronously during fragment append()"); + assert_true(script2Executed, + "Script2 executes synchronously during fragment append()"); +}, "Script node insertion is not atomic with regard to execution. Each " + + "script is synchronously executed during the HTML element insertion " + + "steps hook");