|
| 1 | +// These tests ensure that: |
| 2 | +// 1. The HTML element insertion steps for iframes [1] can synchronously run |
| 3 | +// script during iframe insertion, which can observe an iframe's |
| 4 | +// participation in the DOM tree mid-insertion. |
| 5 | +// 2. The HTML element removing steps for iframes [2] *do not* synchronously |
| 6 | +// run script during child navigable destruction. Therefore, script cannot |
| 7 | +// observe the state of the DOM in the middle of iframe removal, even when |
| 8 | +// multiple iframes are being removed in the same task. Iframe removal, |
| 9 | +// from the perspective of the parent's DOM tree, is atomic. |
| 10 | +// |
| 11 | +// [1]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-insertion-steps |
| 12 | +// [2]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-removing-steps |
| 13 | + |
| 14 | +promise_test(async t => { |
| 15 | + const fragment = new DocumentFragment(); |
| 16 | + |
| 17 | + const iframe1 = fragment.appendChild(document.createElement('iframe')); |
| 18 | + const iframe2 = fragment.appendChild(document.createElement('iframe')); |
| 19 | + |
| 20 | + t.add_cleanup(() => { |
| 21 | + iframe1.remove(); |
| 22 | + iframe2.remove(); |
| 23 | + }); |
| 24 | + |
| 25 | + let iframe1Loaded = false, iframe2Loaded = false; |
| 26 | + iframe1.onload = e => { |
| 27 | + iframe1Loaded = true; |
| 28 | + assert_equals(window.frames.length, 1, |
| 29 | + "iframe1 load event can observe its own participation in the frame tree"); |
| 30 | + assert_equals(iframe1.contentWindow, window.frames[0]); |
| 31 | + }; |
| 32 | + |
| 33 | + iframe2.onload = e => { |
| 34 | + iframe2Loaded = true; |
| 35 | + assert_equals(window.frames.length, 2, |
| 36 | + "iframe2 load event can observe its own participation in the frame tree"); |
| 37 | + assert_equals(iframe1.contentWindow, window.frames[0]); |
| 38 | + assert_equals(iframe2.contentWindow, window.frames[1]); |
| 39 | + }; |
| 40 | + |
| 41 | + // Synchronously consecutively adds both `iframe1` and `iframe2` to the DOM, |
| 42 | + // invoking their insertion steps (and thus firing each of their `load` |
| 43 | + // events) in order. `iframe1` will be able to observe itself in the DOM but |
| 44 | + // not `iframe2`, and `iframe2` will be able to observe both itself and |
| 45 | + // `iframe1`. |
| 46 | + document.body.append(fragment); |
| 47 | + assert_true(iframe1Loaded, "iframe1 loaded"); |
| 48 | + assert_true(iframe2Loaded, "iframe2 loaded"); |
| 49 | +}, "Insertion steps: load event fires synchronously during iframe insertion steps"); |
| 50 | + |
| 51 | +promise_test(async t => { |
| 52 | + const div = document.createElement('div'); |
| 53 | + |
| 54 | + const iframe1 = div.appendChild(document.createElement('iframe')); |
| 55 | + const iframe2 = div.appendChild(document.createElement('iframe')); |
| 56 | + document.body.append(div); |
| 57 | + |
| 58 | + // Now that both iframes have been inserted into the DOM, we'll set up a |
| 59 | + // MutationObserver that we'll use to ensure that multiple synchronous |
| 60 | + // mutations (removals) are only observed atomically at the end. Specifically, |
| 61 | + // the observer's callback is not invoked synchronously for each removal. |
| 62 | + let observerCallbackInvoked = false; |
| 63 | + const removalObserver = new MutationObserver(mutations => { |
| 64 | + assert_false(observerCallbackInvoked, |
| 65 | + "MO callback is only invoked once, not multiple times, i.e., for " + |
| 66 | + "each removal"); |
| 67 | + observerCallbackInvoked = true; |
| 68 | + assert_equals(mutations.length, 1, "Exactly one MutationRecord are recorded"); |
| 69 | + assert_equals(mutations[0].removedNodes.length, 2); |
| 70 | + assert_equals(window.frames.length, 0, |
| 71 | + "No iframe Windows exist when the MO callback is run"); |
| 72 | + assert_equals(document.querySelector('iframe'), null, |
| 73 | + "No iframe elements are connected to the DOM when the MO callback is " + |
| 74 | + "run"); |
| 75 | + }); |
| 76 | + |
| 77 | + removalObserver.observe(div, {childList: true}); |
| 78 | + t.add_cleanup(() => removalObserver.disconnect()); |
| 79 | + |
| 80 | + let iframe1UnloadFired = false, iframe2UnloadFired = false; |
| 81 | + iframe1.contentWindow.addEventListener('unload', e => iframe1UnloadFired = true); |
| 82 | + iframe2.contentWindow.addEventListener('unload', e => iframe2UnloadFired = true); |
| 83 | + |
| 84 | + // replaceChildren() will trigger the synchronous removal of each of `div`'s |
| 85 | + // (iframe) children. This will synchronously, consecutively invoke HTML's |
| 86 | + // "destroy a child navigable" (per [1]), for each iframe. |
| 87 | + // |
| 88 | + // [1]: https://html.spec.whatwg.org/C#the-iframe-element:destroy-a-child-navigable |
| 89 | + div.replaceChildren(); |
| 90 | + assert_false(iframe1UnloadFired, "iframe1 unload did not fire"); |
| 91 | + assert_false(iframe2UnloadFired, "iframe2 unload did not fire"); |
| 92 | + |
| 93 | + assert_false(observerCallbackInvoked, |
| 94 | + "MO callback is not invoked synchronously after removals"); |
| 95 | + |
| 96 | + // Wait one microtask. |
| 97 | + await Promise.resolve(); |
| 98 | + |
| 99 | + assert_true(observerCallbackInvoked, "MO callback is invoked asynchronously after removals"); |
| 100 | +}, "Removing steps: script does not run synchronously during iframe destruction"); |
0 commit comments