diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:14:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:14:29 +0000 |
commit | fbaf0bb26397aa498eb9156f06d5a6fe34dd7dd8 (patch) | |
tree | 4c1ccaf5486d4f2009f9a338a98a83e886e29c97 /testing/web-platform/tests/dom | |
parent | Releasing progress-linux version 124.0.1-1~progress7.99u1. (diff) | |
download | firefox-fbaf0bb26397aa498eb9156f06d5a6fe34dd7dd8.tar.xz firefox-fbaf0bb26397aa498eb9156f06d5a6fe34dd7dd8.zip |
Merging upstream version 125.0.1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/dom')
30 files changed, 1598 insertions, 69 deletions
diff --git a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html index cfc782a809..be4176df59 100644 --- a/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html +++ b/testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html @@ -10,11 +10,14 @@ width: 200px; height: 200px; overflow: scroll; + position: absolute; + left: 150px; + top: 150px; } #innerDiv { - width: 400px; - height: 400px; + width: 250px; + height: 250px; } </style> @@ -45,7 +48,7 @@ function runTest() { await waitForCompositorCommit(); // Do a horizontal scroll and wait for overscroll event. - await touchScrollInTarget(300, scrolling_div , 'right'); + await touchScrollInTarget(100, scrolling_div , 'right'); await waitFor(() => { return overscrolled_x_delta > 0; }, 'Scroller did not receive overscroll event after horizontal scroll.'); assert_equals(scrolling_div.scrollWidth - scrolling_div.scrollLeft, @@ -55,7 +58,7 @@ function runTest() { overscrolled_y_delta = 0; // Do a vertical scroll and wait for overscroll event. - await touchScrollInTarget(300, scrolling_div, 'down'); + await touchScrollInTarget(100, scrolling_div, 'down'); await waitFor(() => { return overscrolled_y_delta > 0; }, 'Scroller did not receive overscroll event after vertical scroll.'); assert_equals(scrolling_div.scrollHeight - scrolling_div.scrollTop, diff --git a/testing/web-platform/tests/dom/events/scrolling/scroll_support.js b/testing/web-platform/tests/dom/events/scrolling/scroll_support.js index e86ead5456..e536b7d748 100644 --- a/testing/web-platform/tests/dom/events/scrolling/scroll_support.js +++ b/testing/web-platform/tests/dom/events/scrolling/scroll_support.js @@ -26,17 +26,15 @@ async function waitForPointercancelEvent(test, target, timeoutMs = 500) { // Resets the scroll position to (0,0). If a scroll is required, then the // promise is not resolved until the scrollend event is received. -async function waitForScrollReset(test, scroller, timeoutMs = 500) { +async function waitForScrollReset(test, scroller, x = 0, y = 0) { return new Promise(resolve => { - if (scroller.scrollTop == 0 && - scroller.scrollLeft == 0) { + if (scroller.scrollTop == x && scroller.scrollLeft == y) { resolve(); } else { const eventTarget = scroller == document.scrollingElement ? document : scroller; - scroller.scrollTop = 0; - scroller.scrollLeft = 0; - waitForScrollendEvent(test, eventTarget, timeoutMs).then(resolve); + scroller.scrollTo(x, y); + waitForScrollendEventNoTimeout(eventTarget).then(resolve); } }); } @@ -121,7 +119,7 @@ function waitForCompositorCommit() { // deferred running the tests until after paint holding. async function waitForCompositorReady() { const animation = - document.body.animate({ opacity: [ 1, 1 ] }, {duration: 1 }); + document.body.animate({ opacity: [ 0, 1 ] }, {duration: 1 }); return animation.finished; } diff --git a/testing/web-platform/tests/dom/nodes/Document-createEvent.js b/testing/web-platform/tests/dom/nodes/Document-createEvent.js index 57e8e966f8..0df5d3f6e1 100644 --- a/testing/web-platform/tests/dom/nodes/Document-createEvent.js +++ b/testing/web-platform/tests/dom/nodes/Document-createEvent.js @@ -16,7 +16,7 @@ var aliases = { "MouseEvents": "MouseEvent", "StorageEvent": "StorageEvent", "SVGEvents": "Event", - "TextEvent": "CompositionEvent", + "TextEvent": "TextEvent", "UIEvent": "UIEvent", "UIEvents": "UIEvent", }; 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"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js index f108e902b3..2cd2ee2b66 100644 --- a/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js +++ b/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js @@ -235,14 +235,18 @@ test(t => { source.subscribe({ complete: () => { - activeDuringComplete = innerSubscriber.active - abortedDuringComplete = innerSubscriber.active + activeDuringComplete = innerSubscriber.active; + abortedDuringComplete = innerSubscriber.signal.aborted; } }); assert_true(activeBeforeComplete, "Subscription is active before complete"); assert_false(abortedBeforeComplete, "Subscription is not aborted before complete"); - assert_false(activeDuringComplete, "Subscription is not active during complete"); - assert_false(abortedDuringComplete, "Subscription is not aborted during complete"); + assert_false(activeDuringComplete, + "Subscription becomes inactive during Subscriber#complete(), just " + + "before Observer#complete() callback is invoked"); + assert_true(abortedDuringComplete, + "Subscription's signal is aborted during Subscriber#complete(), just " + + "before Observer#complete() callback is invoked"); assert_false(activeAfterComplete, "Subscription is not active after complete"); assert_true(abortedAfterComplete, "Subscription is aborted after complete"); }, "Subscription is inactive after complete()"); @@ -269,13 +273,18 @@ test(t => { source.subscribe({ error: () => { - activeDuringError = innerSubscriber.active + activeDuringError = innerSubscriber.active; + abortedDuringError = innerSubscriber.signal.aborted; } }); assert_true(activeBeforeError, "Subscription is active before error"); assert_false(abortedBeforeError, "Subscription is not aborted before error"); - assert_false(activeDuringError, "Subscription is not active during error"); - assert_false(abortedDuringError, "Subscription is not aborted during error"); + assert_false(activeDuringError, + "Subscription becomes inactive during Subscriber#error(), just " + + "before Observer#error() callback is invoked"); + assert_true(abortedDuringError, + "Subscription's signal is aborted during Subscriber#error(), just " + + "before Observer#error() callback is invoked"); assert_false(activeAfterError, "Subscription is not active after error"); assert_true(abortedAfterError, "Subscription is not aborted after error"); }, "Subscription is inactive after error()"); @@ -691,6 +700,18 @@ test(() => { }, "Unsubscription lifecycle"); test(t => { + let innerSubscriber = null; + const source = new Observable(subscriber => { + innerSubscriber = subscriber; + subscriber.error('calling error()'); + }); + + source.subscribe(); + assert_equals(innerSubscriber.signal.reason, "calling error()", + "Reason is set correctly"); +}, "Subscriber#error() value is stored as Subscriber's AbortSignal's reason"); + +test(t => { const source = new Observable((subscriber) => { let n = 0; while (!subscriber.signal.aborted) { diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-drop.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-drop.any.js new file mode 100644 index 0000000000..4b15fedfd3 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-drop.any.js @@ -0,0 +1,152 @@ +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.next(4); + subscriber.complete(); + }); + + const results = []; + + source.drop(2).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [3, 4, "complete"]); +}, "drop(): Observable should skip the first n values from the source " + + "observable, then pass through the rest of the values and completion"); + +test(() => { + const error = new Error('source error'); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.next(4); + subscriber.error(error); + }); + + const results = []; + + source.drop(2).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [3, 4, error]); +}, "drop(): Observable passes through errors from source Observable"); + +test(() => { + const error = new Error('source error'); + const source = new Observable(subscriber => { + subscriber.error(error); + subscriber.next(1); + }); + + const results = []; + + source.drop(2).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [error]); +}, "drop(): Observable passes through errors from source observable even " + + "before drop count is met"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.complete(); + }); + + const results = []; + + source.drop(2).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["complete"]); +}, "drop(): Observable passes through completions from source observable even " + + "before drop count is met"); + +test(() => { + let sourceTeardownCalled = false; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => sourceTeardownCalled = true); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.next(4); + subscriber.next(5); + subscriber.complete(); + }); + + const results = []; + + const controller = new AbortController(); + + source.drop(2).subscribe({ + next: v => { + results.push(v); + if (v === 3) { + controller.abort(); + } + }, + error: (e) => results.push(e), + complete: () => results.push("complete"), + }, {signal: controller.signal}); + + assert_true(sourceTeardownCalled, + "Aborting outer observable unsubscribes the source observable"); + assert_array_equals(results, [3]); +}, "drop(): Unsubscribing from the Observable returned by drop() also " + + "unsubscribes from the source Observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const results = []; + + source.drop(0).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, 3, "complete"], + "Source Observable is mirrored"); +}, "drop(): A drop amount of 0 simply mirrors the source Observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const results = []; + + // Passing `-1` here is subject to the Web IDL integer conversion semantics, + // which converts the drop amount to the maximum of `18446744073709551615`. + source.drop(-1).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["complete"], "Source Observable is mirrored"); +}, "drop(): Passing negative value wraps to maximum value "); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js new file mode 100644 index 0000000000..8a49bcf467 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js @@ -0,0 +1,105 @@ +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.next(4); + subscriber.complete(); + }); + + const results = []; + + source + .filter(value => value % 2 === 0) + .subscribe({ + next: v => results.push(v), + error: () => results.push("error"), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [2, 4, "complete"]); +}, "filter(): Returned Observable filters out results based on predicate"); + +test(() => { + const error = new Error("error while filtering"); + const results = []; + let teardownCalled = false; + + const source = new Observable(subscriber => { + subscriber.addTeardown(() => teardownCalled = true); + subscriber.next(1); + assert_true(teardownCalled, "Teardown called once map unsubscribes due to error"); + assert_false(subscriber.active, "Unsubscription makes Subscriber inactive"); + subscriber.next(2); + subscriber.complete(); + }); + + source + .filter(() => { + throw error; + }) + .subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [error]); +}, "filter(): Errors thrown in filter predicate are emitted to Observer error() handler"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.complete(); + subscriber.next(2); + }); + + let predicateCalls = 0; + const results = []; + source.filter(v => ++predicateCalls).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push('complete'), + }); + + assert_equals(predicateCalls, 1, "Predicate is not called after complete()"); + assert_array_equals(results, [1, "complete"]); +}, "filter(): Passes complete() through from source Observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.error('error'); + subscriber.next(2); + }); + + let predicateCalls = 0; + const results = []; + source.map(v => ++predicateCalls).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push('complete'), + }); + + assert_equals(predicateCalls, 1, "Predicate is not called after error()"); + assert_array_equals(results, [1, "error"]); +}, "filter(): Passes error() through from source Observable"); + +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('source teardown')); + subscriber.signal.addEventListener('abort', + () => results.push('source abort event')); + + subscriber.complete(); + }); + + source.filter(() => results.push('filter predicate called')).subscribe({ + complete: () => results.push('filter observable complete'), + }); + + assert_array_equals(results, + ['source teardown', 'source abort event', 'filter observable complete']); +}, "filter(): Upon source completion, source Observable teardown sequence " + + "happens after downstream filter complete() is called"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-map.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-map.any.js new file mode 100644 index 0000000000..275505fb5d --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-map.any.js @@ -0,0 +1,166 @@ +test(() => { + const results = []; + const indices = []; + const source = new Observable((subscriber) => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const mapped = source.map((value, i) => { + indices.push(i); + return value * 2; + }); + + assert_true(mapped instanceof Observable, "map() returns an Observable"); + + assert_array_equals(results, [], "Does not map until subscribed (values)"); + assert_array_equals(indices, [], "Does not map until subscribed (indices)"); + + mapped.subscribe({ + next: (value) => results.push(value), + error: () => results.push('error'), + complete: () => results.push('complete'), + }); + + assert_array_equals(results, [2, 4, 6, 'complete']); + assert_array_equals(indices, [0, 1, 2]); +}, "map(): Maps values correctly"); + +test(() => { + const error = new Error("error"); + const results = []; + let teardownCalled = false; + + const source = new Observable((subscriber) => { + subscriber.addTeardown(() => teardownCalled = true); + + subscriber.next(1); + assert_false(teardownCalled, + "Teardown not called until until map unsubscribes due to error"); + subscriber.next(2); + assert_true(teardownCalled, "Teardown called once map unsubscribes due to error"); + assert_false(subscriber.active, "Unsubscription makes Subscriber inactive"); + subscriber.next(3); + subscriber.complete(); + }); + + const mapped = source.map((value) => { + if (value === 2) { + throw error; + } + return value * 2; + }); + + mapped.subscribe({ + next: (value) => results.push(value), + error: (error) => results.push(error), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [2, error], + "Mapper errors are emitted to Observer error() handler"); +}, "map(): Mapper errors are emitted to Observer error() handler"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.complete(); + subscriber.next(2); + }); + + let mapperCalls = 0; + const results = []; + source.map(v => { + mapperCalls++; + return v * 2; + }).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push('complete'), + }); + + assert_equals(mapperCalls, 1, "Mapper is not called after complete()"); + assert_array_equals(results, [2, "complete"]); +}, "map(): Passes complete() through from source Observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.error('error'); + subscriber.next(2); + }); + + let mapperCalls = 0; + const results = []; + source.map(v => { + mapperCalls++; + return v * 2; + }).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push('complete'), + }); + + assert_equals(mapperCalls, 1, "Mapper is not called after error()"); + assert_array_equals(results, [2, "error"]); +}, "map(): Passes error() through from source Observable"); + +// This is mostly ensuring that the ordering in +// https://wicg.github.io/observable/#dom-subscriber-complete is consistent. +// +// That is, the `Subscriber#complete()` method *first* closes itself and signals +// abort on its own `Subscriber#signal()` and *then* calls whatever supplied +// completion algorithm exists. In the case of `map()`, the "supplied completion +// algorithm" is simply a set of internal observer steps that call +// `Subscriber#complete()` on the *outer* mapper's Observer. This means the +// outer Observer is notified of completion *after* the source Subscriber's +// signal is aborted / torn down. +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('source teardown')); + subscriber.signal.addEventListener('abort', + () => results.push('source abort event')); + + subscriber.complete(); + }); + + source.map(() => results.push('mapper called')).subscribe({ + complete: () => results.push('map observable complete'), + }); + + assert_array_equals(results, + ['source teardown', 'source abort event', 'map observable complete']); +}, "map(): Upon source completion, source Observable teardown sequence " + + "happens before downstream mapper complete() is called"); + +test(() => { + const results = []; + let sourceSubscriber = null; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('source teardown')); + sourceSubscriber = subscriber; + + subscriber.next(1); + }); + + const controller = new AbortController(); + source.map(v => v * 2).subscribe({ + next: v => { + results.push(v); + + // Triggers unsubscription to `source`. + controller.abort(); + + // Does nothing, since `source` is already torn down. + sourceSubscriber.next(100); + }, + complete: () => results.push('mapper complete'), + error: e => results.push('mapper error'), + }, {signal: controller.signal}); + + assert_array_equals(results, [2, 'source teardown']); +}, "map(): Map observable unsubscription causes source Observable " + + "unsubscription. Mapper Observer's complete()/error() are not called"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-map.window.js b/testing/web-platform/tests/dom/observable/tentative/observable-map.window.js new file mode 100644 index 0000000000..06bf2e26b5 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-map.window.js @@ -0,0 +1,40 @@ +async function loadIframeAndReturnContentWindow() { + // Create and attach an iframe. + const iframe = document.createElement('iframe'); + const iframeLoadPromise = new Promise((resolve, reject) => { + iframe.onload = resolve; + iframe.onerror = reject; + }); + document.body.append(iframe); + await iframeLoadPromise; + return iframe.contentWindow; +} + +promise_test(async t => { + const contentWin = await loadIframeAndReturnContentWindow(); + + window.results = []; + + contentWin.eval(` + const parentResults = parent.results; + + const source = new Observable(subscriber => { + // Detach the document before calling next(). + window.frameElement.remove(); + + // This invokes the map() operator's internal observer's next steps, + // which at least in Chromium, must have a special "context is detached" + // check to early-return, so as to not crash before invoking the "mapper" + // callback supplied to the map() operator. + subscriber.next(1); + }); + + source.map(value => { + parentResults.push(value); + }).subscribe(v => parentResults.push(v)); + `); + + // If we got here, we didn't crash! Let's also check that `results` is empty. + assert_array_equals(results, []); +}, "map()'s internal observer's next steps do not crash in a detached document"); + diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js new file mode 100644 index 0000000000..8350d0214c --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js @@ -0,0 +1,108 @@ +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push("source teardown")); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const result = source.take(2); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, "source teardown", "complete"]); +}, "take(): Takes the first N values from the source observable, then completes"); + +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push("source teardown")); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const result = source.take(5); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, 3, "source teardown", "complete"], + "complete() is immediately forwarded"); +}, "take(): Forwards complete()s that happen before the take count is met, " + + "and unsubscribes from source Observable"); + +test(() => { + const results = []; + const error = new Error('source error'); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.error(error); + }); + + const result = source.take(100); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, error], "Errors are forwarded"); +}, "take(): Should forward errors from the source observable"); + +test(() => { + const results = []; + const source = new Observable((subscriber) => { + results.push("source subscribe"); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const result = source.take(0); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["complete"]); +}, "take(): take(0) should not subscribe to the source observable, and " + + "should return an observable that immediately completes"); + +test(() => { + const results = []; + const source = new Observable((subscriber) => { + results.push("source subscribe"); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + // Per WebIDL, `-1` passed into an `unsigned long long` gets wrapped around + // into the maximum value (18446744073709551615), which means the `result` + // Observable captures everything that `source` does. + const result = source.take(-1); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["source subscribe", 1, 2, 3, "complete"]); +}, "take(): Negative count is treated as maximum value"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js index 6421777e09..2895dd31e3 100644 --- a/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js +++ b/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js @@ -32,74 +32,102 @@ promise_test(async () => { // `takeUntil()` operator, the spec responds to `notifier`'s `next()` by // unsubscribing from `notifier`, which is what this test asserts. promise_test(async () => { - const source = new Observable(subscriber => {}); + const results = []; + const source = new Observable(subscriber => { + results.push('source subscribe callback'); + subscriber.addTeardown(() => results.push('source teardown')); + }); - let notifierSubscriberActiveBeforeNext; - let notifierSubscriberActiveAfterNext; - let teardownCalledAfterNext; - let notifierSignalAbortedAfterNext; const notifier = new Observable(subscriber => { - let teardownCalled; - subscriber.addTeardown(() => teardownCalled = true); + subscriber.addTeardown(() => results.push('notifier teardown')); + results.push('notifier subscribe callback'); // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`. - notifierSubscriberActiveBeforeNext = subscriber.active; + results.push(`notifer active before next(): ${subscriber.active}`); subscriber.next('value'); - notifierSubscriberActiveAfterNext = subscriber.active; - teardownCalledAfterNext = (teardownCalled === true); - notifierSignalAbortedAfterNext = subscriber.signal.aborted; + results.push(`notifer active after next(): ${subscriber.active}`); }); - let nextOrErrorCalled = false; - let completeCalled = false; source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, - complete: () => completeCalled = true, + next: () => results.push('takeUntil() next callback'), + error: e => results.push(`takeUntil() error callback: ${error}`), + complete: () => results.push('takeUntil() complete callback'), }); - assert_true(notifierSubscriberActiveBeforeNext); - assert_false(notifierSubscriberActiveAfterNext); - assert_true(teardownCalledAfterNext); - assert_true(notifierSignalAbortedAfterNext); - assert_false(nextOrErrorCalled); - assert_true(completeCalled); -}, "takeUntil: notifier next() unsubscribes to notifier"); + assert_array_equals(results, [ + 'notifier subscribe callback', + 'notifer active before next(): true', + 'notifier teardown', + 'takeUntil() complete callback', + 'notifer active after next(): false', + ]); +}, "takeUntil: notifier next() unsubscribes from notifier"); // This test is identical to the one above, with the exception being that the // `notifier` calls `subscriber.error()` instead `subscriber.next()`. promise_test(async () => { - const source = new Observable(subscriber => {}); + const results = []; + const source = new Observable(subscriber => { + results.push('source subscribe callback'); + subscriber.addTeardown(() => results.push('source teardown')); + }); - let notifierSubscriberActiveBeforeNext; - let notifierSubscriberActiveAfterNext; - let teardownCalledAfterNext; - let notifierSignalAbortedAfterNext; const notifier = new Observable(subscriber => { - let teardownCalled; - subscriber.addTeardown(() => teardownCalled = true); + subscriber.addTeardown(() => results.push('notifier teardown')); + results.push('notifier subscribe callback'); // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`. - notifierSubscriberActiveBeforeNext = subscriber.active; + results.push(`notifer active before error(): ${subscriber.active}`); subscriber.error('error'); - notifierSubscriberActiveAfterNext = subscriber.active; - teardownCalledAfterNext = (teardownCalled === true); - notifierSignalAbortedAfterNext = subscriber.signal.aborted; + results.push(`notifer active after error(): ${subscriber.active}`); }); - let nextOrErrorCalled = false; - let completeCalled = false; source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, - complete: () => completeCalled = true, + next: () => results.push('takeUntil() next callback'), + error: e => results.push(`takeUntil() error callback: ${error}`), + complete: () => results.push('takeUntil() complete callback'), }); - assert_true(notifierSubscriberActiveBeforeNext); - assert_false(notifierSubscriberActiveAfterNext); - assert_true(teardownCalledAfterNext); - assert_true(notifierSignalAbortedAfterNext); - assert_false(nextOrErrorCalled); - assert_true(completeCalled); -}, "takeUntil: notifier error() unsubscribes to notifier"); + + assert_array_equals(results, [ + 'notifier subscribe callback', + 'notifer active before error(): true', + 'notifier teardown', + 'takeUntil() complete callback', + 'notifer active after error(): false', + ]); +}, "takeUntil: notifier error() unsubscribes from notifier"); +// This test is identical to the above except it `throw`s instead of calling +// `Subscriber#error()`. +promise_test(async () => { + const results = []; + const source = new Observable(subscriber => { + results.push('source subscribe callback'); + subscriber.addTeardown(() => results.push('source teardown')); + }); + + const notifier = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('notifier teardown')); + + results.push('notifier subscribe callback'); + // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`. + results.push(`notifer active before throw: ${subscriber.active}`); + throw new Error('custom error'); + // Won't run: + results.push(`notifer active after throw: ${subscriber.active}`); + }); + + source.takeUntil(notifier).subscribe({ + next: () => results.push('takeUntil() next callback'), + error: e => results.push(`takeUntil() error callback: ${error}`), + complete: () => results.push('takeUntil() complete callback'), + }); + + assert_array_equals(results, [ + 'notifier subscribe callback', + 'notifer active before throw: true', + 'notifier teardown', + 'takeUntil() complete callback', + ]); +}, "takeUntil: notifier throw Error unsubscribes from notifier"); // Test that `notifier` unsubscribes from source Observable. promise_test(async t => { @@ -130,9 +158,10 @@ promise_test(async t => { let notifierTeardownCalledBeforeCompleteCallback; await new Promise(resolve => { source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, + next: () => {nextOrErrorCalled = true; results.push('next callback');}, + error: () => {nextOrErrorCalled = true; results.push('error callback');}, complete: () => { + results.push('complete callback'); notifierTeardownCalledBeforeCompleteCallback = notifierTeardownCalled; resolve(); }, @@ -145,7 +174,7 @@ promise_test(async t => { // The notifier/source teardowns are not called by the time the outer // `Observer#complete()` callback is invoked, but they are all run *after* // (i.e., before `notifier`'s `subscriber.next()` returns internally). - assert_false(notifierTeardownCalledBeforeCompleteCallback); + assert_true(notifierTeardownCalledBeforeCompleteCallback); assert_true(notifierTeardownCalled); assert_array_equals(results, [ "notifier subscribed", @@ -153,7 +182,8 @@ promise_test(async t => { "notifier teardown", "notifier signal abort", "source teardown", - "source signal abort" + "source signal abort", + "complete callback", ]); }, "takeUntil: notifier next() unsubscribes from notifier & source observable"); diff --git a/testing/web-platform/tests/dom/ranges/Range-in-shadow-after-the-shadow-removed.html b/testing/web-platform/tests/dom/ranges/Range-in-shadow-after-the-shadow-removed.html new file mode 100644 index 0000000000..54ea8aabea --- /dev/null +++ b/testing/web-platform/tests/dom/ranges/Range-in-shadow-after-the-shadow-removed.html @@ -0,0 +1,47 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="variant" content="?mode=closed"> +<meta name="variant" content="?mode=open"> +<title>Range in shadow after removing the shadow</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +"use strict"; + +addEventListener("load", () => { + const mode = (new URLSearchParams(document.location.search)).get("mode"); + test(() => { + const host = document.createElement("div"); + host.id = "host"; + const root = host.attachShadow({mode}); + root.innerHTML = '<div id="in-shadow">ABC</div>'; + document.body.appendChild(host); + const range = document.createRange(); + range.setStart(root.firstChild, 1); + host.remove(); + assert_equals(range.startContainer, root.firstChild, "startContainer should not be changed"); + assert_equals(range.startOffset, 1, "startOffset should not be changed"); + }, "Range in shadow should stay in the shadow after the host is removed"); + + test(() => { + const wrapper = document.createElement("div"); + wrapper.id = "wrapper"; + const host = document.createElement("div"); + host.id = "host"; + const root = host.attachShadow({mode}); + root.innerHTML = '<div id="in-shadow">ABC</div>'; + wrapper.appendChild(host); + document.body.appendChild(wrapper); + const range = document.createRange(); + range.setStart(root.firstChild, 1); + wrapper.remove(); + assert_equals(range.startContainer, root.firstChild, "startContainer should not be changed"); + assert_equals(range.startOffset, 1, "startOffset should not be changed"); + }, "Range in shadow should stay in the shadow after the host parent is removed"); +}, {once: true}); +</script> +</head> +<body></body> +</html> |