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 0000000000000..a10610f4677b1 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js @@ -0,0 +1,147 @@ +// 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; + 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(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'); diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-script.window.js b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-script.window.js new file mode 100644 index 0000000000000..a1be3e1dd3e79 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-script.window.js @@ -0,0 +1,48 @@ +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 = document.scripts.length; + 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"); + `; + + assert_false(script0Executed, "Script0 does not execute before append()"); + document.body.append(script0); + assert_true(script0Executed, + "Script0 executes synchronously during append()"); + + assert_false(script1Executed, "Script1 does not execute before append()"); + assert_false(script2Executed, "Script2 does not execute before 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");