// 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');