summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:13:33 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:13:33 +0000
commit086c044dc34dfc0f74fbe41f4ecb402b2cd34884 (patch)
treea4f824bd33cb075dd5aa3eb5a0a94af221bbe83a /testing/web-platform/tests/dom
parentAdding debian version 124.0.1-1. (diff)
downloadfirefox-086c044dc34dfc0f74fbe41f4ecb402b2cd34884.tar.xz
firefox-086c044dc34dfc0f74fbe41f4ecb402b2cd34884.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')
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/overscroll-event-fired-to-scrolled-element.html11
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scroll_support.js12
-rw-r--r--testing/web-platform/tests/dom/nodes/Document-createEvent.js2
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-form-and-script-from-fragment.tentative.html22
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-append-meta-referrer-and-script-from-fragment.tentative.html29
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-button-from-div.tentative.html26
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-custom-from-fragment.tentative.html34
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html35
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-div-from-fragment.tentative.html29
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-iframe.tentative.html89
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-source-from-fragment.tentative.html33
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-style.tentative.html118
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-in-script.tentative.html51
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-with-mutation-observer-takeRecords.html21
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-text-and-script-in-style.tentative.html30
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-text-in-script.tentative.html24
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-three-scripts-from-fragment.tentative.html30
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-three-scripts.tentative.html30
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js19
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-iframe.window.js158
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/insertion-removing-steps-script.window.js48
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/script-does-not-run-on-child-removal.window.js33
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js35
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-drop.any.js152
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js105
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-map.any.js166
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-map.window.js40
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-take.any.js108
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js130
-rw-r--r--testing/web-platform/tests/dom/ranges/Range-in-shadow-after-the-shadow-removed.html47
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>