Skip to content

Commit bbb661d

Browse files
domfarolinochromium-wpt-export-bot
authored andcommittedJan 31, 2024
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. R=masonf@chromium.org Bug: N/A Change-Id: Iff959bbb0d32d772ae7162d5d9e54a5817959086
1 parent 231f825 commit bbb661d

File tree

2 files changed

+145
-0
lines changed

2 files changed

+145
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
promise_test(async t => {
2+
const fragmentWithTwoScripts = new DocumentFragment();
3+
const script0 = document.createElement('script');
4+
const script1 = fragmentWithTwoScripts.appendChild(document.createElement('script'));
5+
const script2 = fragmentWithTwoScripts.appendChild(document.createElement('script'));
6+
7+
window.kBaselineNumberOfScripts = 3;
8+
assert_equals(document.scripts.length, kBaselineNumberOfScripts,
9+
"The WPT infra starts out with exactly 3 scripts");
10+
11+
window.script0Executed = false;
12+
script0.innerText = `
13+
script0Executed = true;
14+
assert_equals(document.scripts.length, kBaselineNumberOfScripts + 1,
15+
'script0 can observe itself and no other scripts');
16+
`;
17+
18+
window.script1Executed = false;
19+
script1.innerText = `
20+
script1Executed = true;
21+
assert_equals(document.scripts.length, kBaselineNumberOfScripts + 2,
22+
"script1 executes synchronously, and thus observes only itself and " +
23+
"previous scripts");
24+
`;
25+
26+
window.script2Executed = false;
27+
script2.innerText = `
28+
script2Executed = true;
29+
assert_equals(document.scripts.length, kBaselineNumberOfScripts + 3,
30+
"script2 executes synchronously, and thus observes itself and all " +
31+
"previous scripts");
32+
`;
33+
34+
document.body.append(script0);
35+
assert_true(script0Executed,
36+
"Script0 executes synchronously during append()");
37+
38+
document.body.append(fragmentWithTwoScripts);
39+
assert_true(script1Executed,
40+
"Script1 executes synchronously during fragment append()");
41+
assert_true(script2Executed,
42+
"Script2 executes synchronously during fragment append()");
43+
}, "Script node insertion is not atomic with regard to execution. Each " +
44+
"script is synchronously executed during the HTML element insertion " +
45+
"steps hook");

0 commit comments

Comments
 (0)
Please sign in to comment.