-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DOM: Add iframe insertion & removal steps WPTs
To help resolve whatwg/dom#808, we need WPTs asserting exactly when (DOM-observing) script can and cannot be invoked during the insertion and removing steps for iframes and script elements. The tests in this CL assert the current spec behavior of: Iframe: - Insertion: - Synchronously fire the `load` event in the iframe document - Removal: - No script is run in between multiple iframe removals. Script cannot observe the state of the DOM in between multiple synchronous removals because, i.e., no `unload` events are fired in this case per HTML [1]. Script: - Insertion: - Synchronously execute <script> elements upon insertion, even in between the insertions of individual <script> elements that are added "atomically" by a single DocumentFragment insertion. Note that Chromium and Gecko fail this test. In the DocumentFragment case, both of these browsers insert all <scripts> into the DOM before executing any of them. This means that once the scripts start executing, they can all observe each other's participation in the DOM tree. At the moment, there is ongoing discussion [2] about the possibility of changing the DOM/HTML Standard's model to more-closely match what Gecko and Chromium do with "atomic" DOM insertion (i.e., running script as a side effect very explicitly after all DOM node insertion is done). [1]: https://html.spec.whatwg.org/C#the-iframe-element:html-element-removing-steps [2]: whatwg/dom#808 (comment) R=masonf@chromium.org Bug: 40150299 Change-Id: Iff959bbb0d32d772ae7162d5d9e54a5817959086 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5251828 Reviewed-by: Mason Freed <masonf@chromium.org> Commit-Queue: Dominic Farolino <dom@chromium.org> Cr-Commit-Position: refs/heads/main@{#1264406}
- Loading branch information
1 parent
250e5d5
commit c5697f6
Showing
2 changed files
with
195 additions
and
0 deletions.
There are no files selected for viewing
147 changes: 147 additions & 0 deletions
147
dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); |
48 changes: 48 additions & 0 deletions
48
dom/nodes/insertion-removing-steps/insertion-removing-steps-script.window.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); |