insertion-removing-steps-iframe.window.js (7241B)
1 // These tests ensure that: 2 // 1. The HTML element insertion steps for iframes [1] run *after* all DOM 3 // insertion mutations associated with any given call to 4 // #concept-node-insert [2] (which may insert many elements at once). 5 // Consequently, a preceding element's insertion steps can observe the 6 // side-effects of later elements being connected to the DOM, but cannot 7 // observe the side-effects of the later element's own insertion steps [1], 8 // since insertion steps are run in order after all DOM insertion mutations 9 // are complete. 10 // 2. The HTML element removing steps for iframes [3] *do not* synchronously 11 // run script during child navigable destruction. Therefore, script cannot 12 // observe the state of the DOM in the middle of iframe removal, even when 13 // multiple iframes are being removed in the same task. Iframe removal, 14 // from the perspective of the parent's DOM tree, is atomic. 15 // 16 // [1]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-insertion-steps 17 // [2]: https://dom.spec.whatwg.org/#concept-node-insert 18 // [3]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-removing-steps 19 20 promise_test(async t => { 21 const fragment = new DocumentFragment(); 22 23 const iframe1 = fragment.appendChild(document.createElement('iframe')); 24 const iframe2 = fragment.appendChild(document.createElement('iframe')); 25 26 t.add_cleanup(() => { 27 iframe1.remove(); 28 iframe2.remove(); 29 }); 30 31 let iframe1Loaded = false, iframe2Loaded = false; 32 iframe1.onload = t.step_func(e => { 33 // iframe1 assertions: 34 iframe1Loaded = true; 35 assert_equals(window.frames.length, 1, 36 "iframe1 load event can observe its own participation in the frame " + 37 "tree"); 38 assert_equals(iframe1.contentWindow, window.frames[0]); 39 40 // iframe2 assertions: 41 assert_false(iframe2Loaded, 42 "iframe2's load event hasn't fired before iframe1's"); 43 assert_true(iframe2.isConnected, 44 "iframe1 can observe that iframe2 is connected to the DOM..."); 45 assert_equals(iframe2.contentWindow, null, 46 "... but iframe1 cannot observe iframe2's contentWindow because " + 47 "iframe2's insertion steps have not been run yet"); 48 }); 49 50 iframe2.onload = t.step_func(e => { 51 iframe2Loaded = true; 52 assert_equals(window.frames.length, 2, 53 "iframe2 load event can observe its own participation in the frame tree"); 54 assert_equals(iframe1.contentWindow, window.frames[0]); 55 assert_equals(iframe2.contentWindow, window.frames[1]); 56 }); 57 58 // Synchronously consecutively adds both `iframe1` and `iframe2` to the DOM, 59 // invoking their insertion steps (and thus firing each of their `load` 60 // events) in order. `iframe1` will be able to observe itself in the DOM but 61 // not `iframe2`, and `iframe2` will be able to observe both itself and 62 // `iframe1`. 63 document.body.append(fragment); 64 assert_true(iframe1Loaded, "iframe1 loaded"); 65 assert_true(iframe2Loaded, "iframe2 loaded"); 66 }, "Insertion steps: load event fires synchronously *after* iframe DOM " + 67 "insertion, as part of the iframe element's insertion steps"); 68 69 // There are several versions of the removal variant, since there are several 70 // ways to remove multiple elements "at once". For example: 71 // 1. `node.innerHTML = ''` ultimately runs 72 // https://dom.spec.whatwg.org/#concept-node-replace-all which removes all 73 // of a node's children. 74 // 2. `node.replaceChildren()` which follows roughly the same path above. 75 // 3. `node.remove()` on a parent of many children will invoke not the DOM 76 // remove algorithm, but rather the "removing steps" hook [1], for each 77 // child. 78 // 79 // [1]: https://dom.spec.whatwg.org/#concept-node-remove-ext 80 81 function runRemovalTest(removal_method) { 82 promise_test(async t => { 83 const div = document.createElement('div'); 84 85 const iframe1 = div.appendChild(document.createElement('iframe')); 86 const iframe2 = div.appendChild(document.createElement('iframe')); 87 document.body.append(div); 88 89 // Now that both iframes have been inserted into the DOM, we'll set up a 90 // MutationObserver that we'll use to ensure that multiple synchronous 91 // mutations (removals) are only observed atomically at the end. Specifically, 92 // the observer's callback is not invoked synchronously for each removal. 93 let observerCallbackInvoked = false; 94 const removalObserver = new MutationObserver(t.step_func(mutations => { 95 assert_false(observerCallbackInvoked, 96 "MO callback is only invoked once, not multiple times, i.e., for " + 97 "each removal"); 98 observerCallbackInvoked = true; 99 assert_equals(mutations.length, 1, "Exactly one MutationRecord is recorded"); 100 assert_equals(mutations[0].removedNodes.length, 2); 101 assert_equals(window.frames.length, 0, 102 "No iframe Windows exist when the MO callback is run"); 103 assert_equals(document.querySelector('iframe'), null, 104 "No iframe elements are connected to the DOM when the MO callback is " + 105 "run"); 106 })); 107 108 removalObserver.observe(div, {childList: true}); 109 t.add_cleanup(() => removalObserver.disconnect()); 110 111 let iframe1UnloadFired = false, iframe2UnloadFired = false; 112 let iframe1PagehideFired = false, iframe2PagehideFired = false; 113 iframe1.contentWindow.addEventListener('pagehide', e => { 114 assert_false(iframe1UnloadFired, "iframe1 pagehide fires before unload"); 115 iframe1PagehideFired = true; 116 }); 117 iframe2.contentWindow.addEventListener('pagehide', e => { 118 assert_false(iframe2UnloadFired, "iframe2 pagehide fires before unload"); 119 iframe2PagehideFired = true; 120 }); 121 iframe1.contentWindow.addEventListener('unload', e => iframe1UnloadFired = true); 122 iframe2.contentWindow.addEventListener('unload', e => iframe2UnloadFired = true); 123 124 // Each `removal_method` will trigger the synchronous removal of each of 125 // `div`'s (iframe) children. This will synchronously, consecutively 126 // invoke HTML's "destroy a child navigable" (per [1]), for each iframe. 127 // 128 // [1]: https://html.spec.whatwg.org/C#the-iframe-element:destroy-a-child-navigable 129 130 if (removal_method === 'replaceChildren') { 131 div.replaceChildren(); 132 } else if (removal_method === 'remove') { 133 div.remove(); 134 } else if (removal_method === 'innerHTML') { 135 div.innerHTML = ''; 136 } 137 138 assert_false(iframe1PagehideFired, "iframe1 pagehide did not fire"); 139 assert_false(iframe2PagehideFired, "iframe2 pagehide did not fire"); 140 assert_false(iframe1UnloadFired, "iframe1 unload did not fire"); 141 assert_false(iframe2UnloadFired, "iframe2 unload did not fire"); 142 143 assert_false(observerCallbackInvoked, 144 "MO callback is not invoked synchronously after removals"); 145 146 // Wait one microtask. 147 await Promise.resolve(); 148 149 if (removal_method !== 'remove') { 150 assert_true(observerCallbackInvoked, 151 "MO callback is invoked asynchronously after removals"); 152 } 153 }, `Removing steps (${removal_method}): script does not run synchronously during iframe destruction`); 154 } 155 156 runRemovalTest('innerHTML'); 157 runRemovalTest('replaceChildren'); 158 runRemovalTest('remove');