diff options
Diffstat (limited to 'testing/web-platform/tests/dom/nodes/insertion-removing-steps')
19 files changed, 859 insertions, 0 deletions
diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-form-and-script-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-form-and-script-from-fragment.tentative.html new file mode 100644 index 0000000000..10351d1645 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-form-and-script-from-fragment.tentative.html @@ -0,0 +1,22 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting script and associated form</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<button id="someButton" form="someForm"></button> +<script> +test(() => { + const script = document.createElement("script"); + const form = document.createElement("form"); + form.id = "someForm"; + const fragment = new DocumentFragment(); + script.textContent = ` + window.buttonAssociatedForm = document.querySelector("#someButton").form; + `; + fragment.append(script, form); + document.body.append(fragment); + assert_equals(window.buttonAssociatedForm, form); +}, "When adding a script+form in a fragment and the form matches an associated element, " + + "the script that checks whether the button is associated to the form should run after " + + "inserting the form"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html new file mode 100644 index 0000000000..d247797603 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html @@ -0,0 +1,29 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting script and meta-referrer from a div</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +promise_test(async t => { + const script = document.createElement("script"); + const meta = document.createElement("meta"); + meta.name = "referrer"; + meta.content = "no-referrer"; + const fragment = new DocumentFragment(); + const done = new Promise(resolve => { + window.didFetch = resolve; + }); + script.textContent = ` + (async function() { + const response = await fetch("/html/infrastructure/urls/terminology-0/resources/echo-referrer-text.py") + const text = await response.text(); + window.didFetch(text); + })(); + `; + fragment.append(script, meta); + document.head.append(fragment); + const result = await done; + assert_equals(result, ""); +}, "<meta name=referrer> should apply before script, as it is an insertion step " + + "and not a post-insertion step"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-button-from-div.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-button-from-div.tentative.html new file mode 100644 index 0000000000..91f09ae500 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-button-from-div.tentative.html @@ -0,0 +1,26 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting script and button from a div</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<form id="form"></form> +<script> +let button = null; +let buttonForm = null; +test(() => { + const form = document.getElementById("form"); + const script = document.createElement("script"); + script.textContent = ` + buttonForm = button.form; + `; + button = document.createElement("button"); + const div = document.createElement("div"); + div.appendChild(script); + div.appendChild(button); + assert_equals(buttonForm, null); + form.appendChild(div); + assert_equals(buttonForm, form); +}, "Script inserted before a form-associated button can observe the button's " + + "form, because by the time the script executes, the DOM insertion that " + + "associates the button with the form is already done"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-custom-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-custom-from-fragment.tentative.html new file mode 100644 index 0000000000..23a050f37e --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-custom-from-fragment.tentative.html @@ -0,0 +1,34 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting script and custom element from a DocumentFragment</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> +<script> +let customConstructed = false; +let customConstructedDuringEarlierScript = false; +class CustomElement extends HTMLElement { + constructor() { + super(); + customConstructed = true; + } +} +test(() => { + const script = document.createElement("script"); + script.textContent = ` + customElements.define("custom-element", CustomElement); + customConstructedDuringEarlierScript = customConstructed; + `; + const custom = document.createElement("custom-element"); + const df = document.createDocumentFragment(); + df.appendChild(script); + df.appendChild(custom); + assert_false(customConstructed); + assert_false(customConstructedDuringEarlierScript); + document.head.appendChild(df); + assert_true(customConstructed); + assert_true(customConstructedDuringEarlierScript); +}, "An earlier-inserted script can upgrade a later-inserted custom element, " + + "whose upgrading is synchronously observable to the script, since DOM " + + "insertion has been completed by the time it runs"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html new file mode 100644 index 0000000000..a9b7ba633e --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html @@ -0,0 +1,35 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting script and default-style meta from a fragment</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<link rel="alternate stylesheet" title="alternative" href="data:text/css,%23div{display:none}"> +<div id="div">hello</div> +<script> +let scriptRan = false; +let computedStyleDuringInsertion = null; +test(() => { + const div = document.getElementById("div"); + const meta = document.createElement("meta"); + meta.httpEquiv = "default-style"; + meta.content = "alternative"; + const script = document.createElement("script"); + script.textContent = ` + computedStyleDuringInsertion = getComputedStyle(div).display; + scriptRan = true; + `; + const df = document.createDocumentFragment(); + df.appendChild(script); + df.appendChild(meta); + assert_equals(getComputedStyle(div).display, "block", "div has block display"); + assert_false(scriptRan, "script has not run before insertion"); + document.head.appendChild(df); + assert_true(scriptRan, "script has run after insertion"); + assert_equals(computedStyleDuringInsertion, "none", + "display: none; style was applied during DOM insertion, before " + + "later-inserted script runs"); + assert_equals(getComputedStyle(div).display, "none", + "style remains display: none; after insertion"); +}, "Inserting <meta> that uses alternate stylesheets, applies the style " + + "during DOM insertion, and before script runs as a result of any atomic insertions"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-div-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-div-from-fragment.tentative.html new file mode 100644 index 0000000000..b154c1bf4f --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-div-from-fragment.tentative.html @@ -0,0 +1,29 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting script and div from a DocumentFragment</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> +<script> +let script = null; +let scriptParent = null; +let div = null; +let divParent = null; +test(() => { + script = document.createElement("script"); + div = document.createElement("div"); + script.textContent = ` + divParent = div.parentNode; + scriptParent = script.parentNode; + `; + const df = document.createDocumentFragment(); + df.appendChild(script); + df.appendChild(div); + assert_equals(divParent, null); + assert_equals(scriptParent, null); + document.head.appendChild(df); + assert_equals(divParent, scriptParent); + assert_equals(divParent, document.head); +}, "Earlier-inserted scripts can observe the parentNode of later-inserted " + + "nodes, because script runs after DOM insertion completes"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-iframe.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-iframe.tentative.html new file mode 100644 index 0000000000..68b288f24d --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-iframe.tentative.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Node.appendChild: inserting script and iframe</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> +<script> + +const kScriptContent = ` + state = iframe.contentWindow ? "iframe with content window" : "contentWindow is null"; +`; + +// This test ensures that a later-inserted script can observe an +// earlier-inserted iframe's contentWindow. +test(t => { + window.state = "script not run yet"; + window.iframe = document.createElement("iframe"); + t.add_cleanup(() => window.iframe.remove()); + + const script = document.createElement("script"); + script.textContent = kScriptContent; + + const div = document.createElement("div"); + div.appendChild(iframe); + div.appendChild(script); + + assert_equals(state, "script not run yet"); + document.body.appendChild(div); + assert_equals(state, "iframe with content window"); +}, "Script inserted after an iframe in the same appendChild() call can " + + "observe the iframe's non-null contentWindow"); + +// The below tests assert that an earlier-inserted script does not observe a +// later-inserted iframe's contentWindow. +test(t => { + window.state = "script not run yet"; + window.iframe = document.createElement("iframe"); + t.add_cleanup(() => window.iframe.remove()); + + const script = document.createElement("script"); + script.textContent = kScriptContent; + + const div = document.createElement("div"); + div.appendChild(script); + div.appendChild(iframe); + + assert_equals(state, "script not run yet"); + document.body.appendChild(div); + assert_equals(state, "contentWindow is null"); +}, "A script inserted atomically before an iframe (using a div) does not " + + "observe the iframe's contentWindow, since the 'script running' and " + + "'iframe setup' both happen in order, after DOM insertion completes"); + +test(t => { + window.state = "script not run yet"; + window.iframe = document.createElement("iframe"); + t.add_cleanup(() => window.iframe.remove()); + + const script = document.createElement("script"); + script.textContent = kScriptContent; + + const df = document.createDocumentFragment(); + df.appendChild(script); + df.appendChild(iframe); + + assert_equals(state, "script not run yet"); + document.body.appendChild(df); + assert_equals(state, "contentWindow is null"); +}, "A script inserted atomically before an iframe (using a DocumentFragment) " + + "does not observe the iframe's contentWindow, since the 'script running' " + + "and 'iframe setup' both happen in order, after DOM insertion completes"); + +test(t => { + window.state = "script not run yet"; + window.iframe = document.createElement("iframe"); + t.add_cleanup(() => window.iframe.remove()); + + const script = document.createElement("script"); + script.textContent = kScriptContent; + + assert_equals(state, "script not run yet"); + document.body.append(script, iframe); + + assert_equals(state, "contentWindow is null"); +}, "A script inserted atomically before an iframe (using a append() with " + + "multiple arguments) does not observe the iframe's contentWindow, since " + + "the 'script running' and 'iframe setup' both happen in order, after DOM " + + "insertion completes"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-source-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-source-from-fragment.tentative.html new file mode 100644 index 0000000000..7f93ac43bd --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-source-from-fragment.tentative.html @@ -0,0 +1,33 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting script and source from a fragment</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<video id="media"></video> +<script> +const happened = []; +const media = document.getElementById("media"); +test(() => { + const source = document.createElement("source"); + const script = document.createElement("script"); + script.textContent = ` + happened.push(media.networkState); + `; + + const df = document.createDocumentFragment(); + df.appendChild(script); + df.appendChild(source); + + assert_array_equals(happened, []); + media.appendChild(df); + // This is because immediately during DOM insertion, before the + // post-insertion steps invoke script, `<source>` insertion invokes the + // resource selection algorithm [1] which does this assignment. This + // assignment takes place before earlier-inserted script elements run + // post-insertion. + // + // [1]: https://html.spec.whatwg.org/#concept-media-load-algorithm + assert_array_equals(happened, [HTMLMediaElement.NETWORK_NO_SOURCE]); +}, "Empty <source> immediately sets media.networkState during DOM insertion, " + + "so that an earlier-running script can observe networkState"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-style.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-style.tentative.html new file mode 100644 index 0000000000..d3365f8a5e --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-style.tentative.html @@ -0,0 +1,118 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting a script and a style where the script modifies the style</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> +<script> +// <script> & <style> element tests. +test(() => { + window.happened = []; + window.style = document.createElement("style"); + let styleSheet = null; + + style.appendChild(new Text("body {}")); + const script = document.createElement("script"); + script.textContent = ` + styleSheet = style.sheet; + happened.push(style.sheet ? "sheet" : null); + style.appendChild(new Text("body {}")); + happened.push(style.sheet?.cssRules.length); + `; + + const div = document.createElement("div"); + div.appendChild(script); + div.appendChild(style); + + assert_array_equals(happened, []); + document.body.appendChild(div); + assert_array_equals(happened, ["sheet", 2]); + assert_not_equals(style.sheet, styleSheet, "style sheet was created only once"); +}, "An earlier-inserted <script> synchronously observes a later-inserted " + + "<style> (via a div) being applied"); + +test(() => { + window.happened = []; + window.style = document.createElement("style"); + let styleSheet = null; + + style.appendChild(new Text("body {}")); + const script = document.createElement("script"); + script.textContent = ` + styleSheet = style.sheet; + happened.push(style.sheet ? "sheet" : null); + style.appendChild(new Text("body {}")); + happened.push(style.sheet?.cssRules.length); +`; + + const df = document.createDocumentFragment(); + df.appendChild(script); + df.appendChild(style); + + assert_array_equals(happened, []); + document.body.appendChild(df); + assert_array_equals(happened, ["sheet", 2]); + assert_not_equals(style.sheet, styleSheet, "style sheet was created only once"); +}, "An earlier-inserted <script> synchronously observes a later-inserted " + + "<style> (via a DocumentFragment) being applied"); + +// <script> & <link rel=stylesheet> element tests. +test(() => { + window.happened = []; + window.link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "data:text/css,"; + + const script = document.createElement("script"); + script.textContent = ` + happened.push(link.sheet ? "sheet" : null); + `; + + const df = document.createDocumentFragment(); + df.appendChild(script); + df.appendChild(link); + + assert_array_equals(happened, []); + document.body.appendChild(df); + assert_array_equals(happened, ["sheet"]); +}, "Earlier-inserted <script> (via a DocumentFragment) synchronously " + + "observes a later-inserted <link rel=stylesheet>'s CSSStyleSheet creation"); + +test(() => { + window.happened = []; + window.link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "data:text/css,"; + + const script = document.createElement("script"); + script.textContent = ` + happened.push(link.sheet ? "sheet" : null); +`; + + const div = document.createElement("div"); + div.appendChild(script); + div.appendChild(link); + + assert_array_equals(happened, []); + document.body.appendChild(div); + assert_array_equals(happened, ["sheet"]); +}, "Earlier-inserted <script> (via a div) synchronously observes a " + + "later-inserted <link rel=stylesheet>'s CSSStyleSheet creation"); + +test(() => { + window.happened = []; + window.link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "data:text/css,"; + + const script = document.createElement("script"); + script.textContent = ` + happened.push(link.sheet ? "sheet" : null); +`; + + assert_array_equals(happened, []); + document.body.append(script, link); + assert_array_equals(happened, ["sheet"]); +}, "Earlier-inserted <script> (via a append()) synchronously observes a " + + "later-inserted <link rel=stylesheet>'s CSSStyleSheet creation"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-in-script.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-in-script.tentative.html new file mode 100644 index 0000000000..39c4393323 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-in-script.tentative.html @@ -0,0 +1,51 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting a script and some code in an empty script</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> +<script id="s1"></script> +<script> +const happened = []; +test(() => { + const s1 = document.getElementById("s1"); + const s2 = document.createElement("script"); + + // This script, which is ultimately a *child* of the + // already-connected-but-empty `s1` script, runs second, after `s1` runs. See + // the example in + // http://html.spec.whatwg.org/C/#script-processing-model:children-changed-steps + // for more information. + // + // HISTORICAL CONTEXT: There used to be a condition in the HTML standard that + // said an "outer" script must be "prepared" when a node gets inserted into + // the script. BUT it also stipulated that if the insertion consists of any + // "inner" (nested, essentially) script elements, then this "outer" script + // must prepare/execute after any of those "inner" newly-inserted scripts + // themselves get prepared. + // + // This changed in https://github.com/whatwg/html/pull/10188. + s2.textContent = ` + happened.push("s2"); + + // This text never executes in the outer script, because by the time this + // gets appended, the outer script has "already started" [1], so it does not + // get re-prepared/executed a second time. + // + // [1]: https://html.spec.whatwg.org/C#already-started + s1.appendChild(new Text("happened.push('s1ran');")); + + happened.push("s2ran"); +`; + + const df = document.createDocumentFragment(); + df.appendChild(new Text(`happened.push("s1");`)); + df.appendChild(s2); + + assert_array_equals(happened, []); + s1.appendChild(df); + assert_array_equals(happened, ["s1", "s2", "s2ran"]); +}, "An outer script whose preparation/execution gets triggered by the " + + "insertion of a 'nested'/'inner' script, executes *before* the inner " + + "script executes"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-with-mutation-observer-takeRecords.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-with-mutation-observer-takeRecords.html new file mode 100644 index 0000000000..33598e6408 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-with-mutation-observer-takeRecords.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Node.appendChild: inserted script should be able to take own mutation record</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> +<main></main> +<script> + +test(() => { + window.mutationObserver = new MutationObserver(() => {}); + window.mutationObserver.observe(document.querySelector("main"), {childList: true}); + const script = document.createElement("script"); + script.textContent = ` + window.mutationRecords = window.mutationObserver.takeRecords(); + `; + document.querySelector("main").appendChild(script); + assert_equals(window.mutationRecords.length, 1); + assert_array_equals(window.mutationRecords[0].addedNodes, [script]); +}, "An inserted script should be able to observe its own mutation record with takeRecords"); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-text-and-script-in-style.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-text-and-script-in-style.tentative.html new file mode 100644 index 0000000000..850af680a0 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-text-and-script-in-style.tentative.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting text and script nodes in a style element</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<style id="style"></style> +<body> +<script> +const happened = [] +const style = document.getElementById("style"); +test(() => { + const r1 = new Text("body {}"); + const r2 = new Text("body {}"); + const script = document.createElement("script"); + script.textContent = ` + happened.push(style.sheet.cssRules.length); + `; + + const df = document.createDocumentFragment(); + df.appendChild(r1); + df.appendChild(script); + df.appendChild(r2); + + assert_array_equals(happened, []); + style.appendChild(df); + assert_array_equals(happened, [2]); +}, "All style rules appended to a <style> element are inserted and " + + "script-observable to scripts inserted in the `<style>` element, by the " + + "time scripts execute after DOM insertions."); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-text-in-script.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-text-in-script.tentative.html new file mode 100644 index 0000000000..4d6543695c --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-text-in-script.tentative.html @@ -0,0 +1,24 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting two text nodes in an empty script</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> + <script id="script"></script> +<script> +const happened = []; +test(() => { + const script = document.getElementById("script"); + const df = document.createDocumentFragment(); + df.appendChild(new Text("happened.push('t1');")); + df.appendChild(new Text("happened.push('t2');")); + assert_array_equals(happened, []); + script.appendChild(df); + assert_array_equals(happened, ["t1", "t2"]); + // At this point it's already executed so further motifications are a no-op + script.appendChild(new Text("happened.push('t3');")); + script.textContent = "happened.push('t4');" + script.text = "happened.push('t5');" + assert_array_equals(happened, ["t1", "t2"]); +}); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-three-scripts-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-three-scripts-from-fragment.tentative.html new file mode 100644 index 0000000000..a7b7405b64 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-three-scripts-from-fragment.tentative.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting three scripts from a document fragment</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> + <script> +const s1 = document.createElement("script"); +const s2 = document.createElement("script"); +const s3 = document.createElement("script"); +const happened = []; + +test(() => { + s1.textContent = ` + s3.appendChild(new Text("happened.push('s3');")); + happened.push("s1"); + `; + s2.textContent = ` + happened.push("s2"); + `; + const df = document.createDocumentFragment(); + df.appendChild(s1); + df.appendChild(s2); + df.appendChild(s3); + + assert_array_equals(happened, []); + document.body.appendChild(df); + assert_array_equals(happened, ["s3", "s1", "s2"]); +}); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-three-scripts.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-three-scripts.tentative.html new file mode 100644 index 0000000000..6ffa35515e --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-three-scripts.tentative.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<title>Node.appendChild: inserting three scripts from a div</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<body> + <script> +const s1 = document.createElement("script"); +const s2 = document.createElement("script"); +const s3 = document.createElement("script"); +const happened = []; + +test(() => { + s1.textContent = ` + s3.appendChild(new Text("happened.push('s3');")); + happened.push("s1"); + `; + s2.textContent = ` + happened.push("s2"); + `; + const div = document.createElement("div"); + div.appendChild(s1); + div.appendChild(s2); + div.appendChild(s3); + + assert_array_equals(happened, []); + document.body.appendChild(div); + assert_array_equals(happened, ["s3", "s1", "s2"]); +}); +</script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js new file mode 100644 index 0000000000..4c8cd85cbf --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js @@ -0,0 +1,19 @@ +test(() => { + const input = document.body.appendChild(document.createElement('input')); + input.focus(); + + let blurCalled = false; + input.onblur = e => blurCalled = true; + input.remove(); + assert_false(blurCalled, "Blur event was not fired"); +}, "<input> element does not fire blur event upon DOM removal"); + +test(() => { + const button = document.body.appendChild(document.createElement('button')); + button.focus(); + + let blurCalled = false; + button.onblur = e => blurCalled = true; + button.remove(); + assert_false(blurCalled, "Blur event was not fired"); +}, "<button> element does not fire blur event upon DOM removal"); diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js new file mode 100644 index 0000000000..60c2bec0c8 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js @@ -0,0 +1,158 @@ +// 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'); diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-script.window.js b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-script.window.js new file mode 100644 index 0000000000..a1be3e1dd3 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-script.window.js @@ -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"); diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/script-does-not-run-on-child-removal.window.js b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/script-does-not-run-on-child-removal.window.js new file mode 100644 index 0000000000..ed5bfbaa60 --- /dev/null +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/script-does-not-run-on-child-removal.window.js @@ -0,0 +1,33 @@ +// See: +// - https://github.com/whatwg/dom/issues/808 +// - https://github.com/whatwg/dom/pull/1261 +// - https://github.com/whatwg/html/pull/10188 +// - https://source.chromium.org/chromium/chromium/src/+/604e798ec6ee30f44d57a5c4a44ce3dab3a871ed +// - https://github.com/whatwg/dom/pull/732#pullrequestreview-328249015 +// - https://github.com/whatwg/html/pull/4354#issuecomment-476038918 +test(() => { + window.script_did_run = false; + + const script = document.createElement('script'); + // This prevents execution on insertion. + script.type = '0'; + script.textContent = `script_did_run = true;`; + document.body.append(script); + assert_false(script_did_run, + 'Appending script with invalid type does not trigger execution'); + + const div = document.createElement('div'); + script.append(div); + assert_false(script_did_run, + 'Appending a child to an invalid-type script does not trigger execution'); + + // This enables, but does not trigger, execution. + script.type = ''; + assert_false(script_did_run, + 'Unsetting script type does not trigger execution'); + + div.remove(); + assert_false(script_did_run, + 'Removing child from valid script that has not already run, does not ' + + 'trigger execution'); +}, "Script execution is never triggered on child removals"); |