summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/dom')
-rw-r--r--testing/web-platform/tests/dom/abort/WEB_FEATURES.yml3
-rw-r--r--testing/web-platform/tests/dom/events/event-global.html10
-rw-r--r--testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html44
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html46
-rw-r--r--testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js17
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-first.any.js114
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js315
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-from.any.js354
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-last.any.js113
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js252
10 files changed, 1241 insertions, 27 deletions
diff --git a/testing/web-platform/tests/dom/abort/WEB_FEATURES.yml b/testing/web-platform/tests/dom/abort/WEB_FEATURES.yml
new file mode 100644
index 0000000000..169de93ae9
--- /dev/null
+++ b/testing/web-platform/tests/dom/abort/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: aborting
+ files: "**"
diff --git a/testing/web-platform/tests/dom/events/event-global.html b/testing/web-platform/tests/dom/events/event-global.html
index 3e8d25ecb5..f70606fb65 100644
--- a/testing/web-platform/tests/dom/events/event-global.html
+++ b/testing/web-platform/tests/dom/events/event-global.html
@@ -114,4 +114,14 @@ async_test(t => {
target.dispatchEvent(new Event("click"));
}, "window.event is set to the current event, which is the event passed to dispatch");
+
+async_test(t => {
+ let target = new XMLHttpRequest();
+
+ target.onload = t.step_func_done(e => {
+ assert_equals(e, window.event);
+ });
+
+ target.dispatchEvent(new Event("load"));
+}, "window.event is set to the current event, which is the event passed to dispatch (2)");
</script>
diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html
index 5e3af7966e..99a281480f 100644
--- a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html
+++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html
@@ -20,28 +20,48 @@
</style>
<div class="large"></div>
<script>
- window.onload = () => {
- promise_test(async () => {
- await waitForCompositorCommit();
-
- await pinchZoomIn();
- assert_greater_than(visualViewport.scale, 1, "page should be zoomed in.");
-
+ window.onload = async () => {
+ async function pan_viewport_test(add_event_listener_func) {
const preScrollVisualViewportOffsetTop = visualViewport.offsetTop;
const preScrollWindowScrollOffset = window.scrollY;
- const scrollend_promise = new Promise((resolve) => {
- visualViewport.addEventListener("scrollend", resolve);
- });
+
+ const scrollend_promise = add_event_listener_func();
const scrollAmount = 50;
await touchScrollInTarget(scrollAmount, document.documentElement, "up");
await scrollend_promise;
- assert_less_than(visualViewport.offsetTop, preScrollVisualViewportOffsetTop,
+ assert_less_than(visualViewport.offsetTop,
+ preScrollVisualViewportOffsetTop,
`visualViewport should be scrolled.`);
assert_equals(window.scrollY, preScrollWindowScrollOffset,
"the window should not scroll.");
- }, "scrollend fires when visual viewport is panned.");
+ // No need to undo scroll; subsequent test has room to scroll further.
+ }
+
+ await waitForCompositorCommit();
+ await pinchZoomIn();
+ assert_greater_than(visualViewport.scale, 1, "page should be zoomed in.");
+
+ promise_test(async (t) => {
+ await pan_viewport_test(() => {
+ return new Promise((resolve) => {
+ visualViewport.addEventListener("scrollend", resolve, { once: true});
+ });
+ });
+ }, "scrollend listener added via addEventlistener fires when the visual " +
+ "viewport is panned.");
+
+ promise_test(async (t) => {
+ await pan_viewport_test((t) => {
+ return new Promise((resolve) => {
+ visualViewport.onscrollend = () => {
+ visualViewport.onscrollend = undefined;
+ resolve();
+ }
+ });
+ });
+ }, "visualviewport.onscrollend fires when the visual viewport is panned.");
}
</script>
</body>
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
index a9b7ba633e..fa4a987751 100644
--- 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
@@ -7,29 +7,53 @@
<div id="div">hello</div>
<script>
let scriptRan = false;
-let computedStyleDuringInsertion = null;
+let computedStyleInPreScript = null;
+let computedStyleInPostScript = null;
test(() => {
const div = document.getElementById("div");
+
+ // 1. Gets inserted *before* the `<meta>` tag. Cannot observe the meta tag's
+ // effect, because this script runs before the meta tag's post-insertion steps
+ // run, and the meta tag's post-insertion steps is where the default style
+ // sheet actually changes.
+ const preScript = document.createElement("script");
+ preScript.textContent = `
+ computedStyleInPreScript = getComputedStyle(div).display;
+ scriptRan = true;
+ `;
+
+ // 2. The `<meta>` tag itself.
const meta = document.createElement("meta");
meta.httpEquiv = "default-style";
meta.content = "alternative";
- const script = document.createElement("script");
- script.textContent = `
- computedStyleDuringInsertion = getComputedStyle(div).display;
+
+ // 3. Gets inserted *after* the `<meta>` tag. Observes the meta tag's effect,
+ // because this script runs after the meta tag's post-insertion steps, which
+ // has the script-observable change to the default style sheet.
+ const postScript = document.createElement("script");
+ postScript.textContent = `
+ computedStyleInPostScript = 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");
+ df.append(preScript, meta, postScript);
+
+ assert_equals(getComputedStyle(div).display, "block",
+ "div still has block display before meta insertion");
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(computedStyleInPreScript, "block",
+ "display: none; style was NOT applied during DOM insertion steps, " +
+ "before earlier-inserted script post-insertion steps run");
+ assert_equals(computedStyleInPostScript, "none",
+ "display: none; style WAS applied during DOM post-insertion steps, " +
+ "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");
+ "during DOM post-insertion steps");
</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
index 4c8cd85cbf..fdca02dcda 100644
--- 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
@@ -12,8 +12,17 @@ test(() => {
const button = document.body.appendChild(document.createElement('button'));
button.focus();
- let blurCalled = false;
- button.onblur = e => blurCalled = true;
+ let blur_called = false;
+ let focus_out_called = false;
+ let focus_called = false;
+
+ button.onblur = () => { blur_called = true; }
+ button.onfocusout = () => { focus_out_called = true; }
+ document.body.addEventListener("focus",
+ () => { focus_called = true; }, {capture: true});
button.remove();
- assert_false(blurCalled, "Blur event was not fired");
-}, "<button> element does not fire blur event upon DOM removal");
+
+ assert_false(blur_called, "Blur event was not fired");
+ assert_false(focus_out_called, "FocusOut event was not fired");
+ assert_false(focus_called, "Focus was not fired");
+}, "<button> element does not fire blur/focusout events upon DOM removal");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js
new file mode 100644
index 0000000000..7c99066dc2
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js
@@ -0,0 +1,114 @@
+promise_test(async () => {
+ const results = [];
+
+ const source = new Observable(subscriber => {
+ subscriber.addTeardown(() => results.push('teardown'));
+ subscriber.next(1);
+ results.push(subscriber.active ? 'active' : 'inactive');
+ results.push(subscriber.signal.aborted ? 'aborted' : 'not aborted')
+
+ // Ignored.
+ subscriber.next(2);
+ subscriber.complete();
+ });
+
+ const value = await source.first();
+
+ assert_array_equals(results, ['teardown', 'inactive', 'aborted']);
+ assert_equals(value, 1,
+ "Promise resolves with the first value from the source Observable");
+}, "first(): Promise resolves with the first value from the source Observable");
+
+promise_test(async () => {
+ const error = new Error("error from source");
+ const source = new Observable(subscriber => {
+ subscriber.error(error);
+ });
+
+ let rejection;
+ try {
+ await source.first();
+ } catch (e) {
+ rejection = e;
+ }
+
+ assert_equals(rejection, error, "Promise rejects with source Observable error");
+}, "first(): Promise rejects with the error emitted from the source Observable");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.complete();
+ });
+
+ let rejection;
+ try {
+ await source.first();
+ } catch (e) {
+ rejection = e;
+ }
+
+ assert_true(rejection instanceof RangeError,
+ "Upon complete(), first() Promise rejects with RangeError");
+ assert_equals(rejection.message, "No values in Observable");
+}, "first(): Promise rejects with RangeError when source Observable " +
+ "completes without emitting any values");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {});
+
+ const controller = new AbortController();
+ const promise = source.first({ signal: controller.signal });
+
+ controller.abort();
+
+ let rejection;
+ try {
+ await promise;
+ } catch (e) {
+ rejection = e;
+ }
+
+ assert_true(rejection instanceof DOMException,
+ "Promise rejects with a DOMException for abortion");
+ assert_equals(rejection.name, "AbortError",
+ "Rejected with 'AbortError' DOMException");
+ assert_equals(rejection.message, "signal is aborted without reason");
+}, "first(): Aborting a signal rejects the Promise with an AbortError DOMException");
+
+promise_test(async () => {
+ const results = [];
+
+ const source = new Observable(subscriber => {
+ results.push("source subscribe");
+ subscriber.addTeardown(() => results.push("source teardown"));
+ subscriber.signal.addEventListener("abort", () => results.push("source abort"));
+ results.push("before source next 1");
+ subscriber.next(1);
+ results.push("after source next 1");
+ });
+
+ results.push("calling first");
+ const promise = source.first();
+
+ assert_array_equals(results, [
+ "calling first",
+ "source subscribe",
+ "before source next 1",
+ "source teardown",
+ "source abort",
+ "after source next 1"
+ ], "Array values after first() is called");
+
+ const firstValue = await promise;
+ results.push(`first resolved with: ${firstValue}`);
+
+ assert_array_equals(results, [
+ "calling first",
+ "source subscribe",
+ "before source next 1",
+ "source teardown",
+ "source abort",
+ "after source next 1",
+ "first resolved with: 1",
+ ], "Array values after Promise is awaited");
+}, "first(): Lifecycle");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js
new file mode 100644
index 0000000000..7cbfa6cb60
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js
@@ -0,0 +1,315 @@
+test(() => {
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ let projectionCalls = 0;
+
+ const results = [];
+
+ const flattened = source.flatMap(value => {
+ projectionCalls++;
+ return new Observable((subscriber) => {
+ subscriber.next(value * 10);
+ subscriber.next(value * 100);
+ subscriber.complete();
+ });
+ });
+
+ assert_true(flattened instanceof Observable, "flatMap() returns an Observable");
+ assert_equals(projectionCalls, 0,
+ "Projection is not called until subscription starts");
+
+ flattened.subscribe({
+ next: v => results.push(v),
+ error: () => results.push("error"),
+ complete: () => results.push("complete"),
+ });
+
+ assert_equals(projectionCalls, 3,
+ "Mapper is called three times, once for each source Observable value");
+ assert_array_equals(results, [10, 100, 20, 200, 30, 300, "complete"],
+ "flatMap() results are correct");
+}, "flatMap(): Flattens simple source Observable properly");
+
+test(() => {
+ const error = new Error("error");
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.error(error);
+ subscriber.next(3);
+ });
+
+ const flattened = source.flatMap(value => {
+ return new Observable(subscriber => {
+ subscriber.next(value * 10);
+ subscriber.next(value * 100);
+ subscriber.complete();
+ });
+ });
+
+ const results = [];
+
+ flattened.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [10, 100, 20, 200, error],
+ "Source error is passed through to the flatMap() Observable");
+}, "flatMap(): Returned Observable passes through source Observable errors");
+
+test(() => {
+ const results = [];
+ const error = new Error("error");
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ results.push(subscriber.active ? "active" : "inactive");
+ subscriber.next(2);
+ results.push(subscriber.active ? "active" : "inactive");
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const flattened = source.flatMap((value) => {
+ return new Observable((subscriber) => {
+ subscriber.next(value * 10);
+ subscriber.next(value * 100);
+ if (value === 2) {
+ subscriber.error(error);
+ } else {
+ subscriber.complete();
+ }
+ });
+ });
+
+ flattened.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [10, 100, "active", 20, 200, error, "inactive"],
+ "Inner subscription error gets surfaced");
+}, "flatMap(): Outer Subscription synchronously becomes inactive when an " +
+ "'inner' Observable emits an error");
+
+test(() => {
+ const results = [];
+ const error = new Error("error");
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ results.push(subscriber.active ? "active" : "inactive");
+ subscriber.complete();
+ });
+
+ const flattened = source.flatMap(value => {
+ if (value === 3) {
+ throw error;
+ }
+ return new Observable(subscriber => {
+ subscriber.next(value * 10);
+ subscriber.next(value * 100);
+ subscriber.complete();
+ });
+ });
+
+ flattened.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [10, 100, 20, 200, error, "inactive"],
+ "Inner subscriber thrown error gets surfaced");
+}, "flatMap(): Outer Subscription synchronously becomes inactive when an " +
+ "'inner' Observable throws an error");
+
+test(() => {
+ const source = createTestSubject();
+ const inner1 = createTestSubject();
+ const inner2 = createTestSubject();
+
+ const flattened = source.flatMap(value => {
+ if (value === 1) {
+ return inner1;
+ }
+
+ return inner2;
+ });
+
+ const results = [];
+
+ flattened.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, []);
+
+ source.next(1);
+ assert_equals(inner1.subscriberCount(), 1, "inner1 gets subscribed to");
+
+ source.next(2);
+ assert_equals(inner2.subscriberCount(), 0,
+ "inner2 is queued, not subscribed to until inner1 completes");
+
+ assert_array_equals(results, []);
+
+ inner1.next(100);
+ inner1.next(101);
+
+ assert_array_equals(results, [100, 101]);
+
+ inner1.complete();
+ assert_equals(inner1.subscriberCount(), 0,
+ "inner1 becomes inactive once it completes");
+ assert_equals(inner2.subscriberCount(), 1,
+ "inner2 gets un-queued and subscribed to once inner1 completes");
+
+ inner2.next(200);
+ inner2.next(201);
+ assert_array_equals(results, [100, 101, 200, 201]);
+
+ inner2.complete();
+ assert_equals(inner2.subscriberCount(), 0,
+ "inner2 becomes inactive once it completes");
+ assert_equals(source.subscriberCount(), 1,
+ "source is not unsubscribed from yet, since it has not completed");
+ assert_array_equals(results, [100, 101, 200, 201]);
+
+ source.complete();
+ assert_equals(source.subscriberCount(), 0,
+ "source unsubscribed from after it completes");
+
+ assert_array_equals(results, [100, 101, 200, 201, "complete"]);
+}, "flatMap(): result Observable does not complete until source and inner " +
+ "Observables all complete");
+
+test(() => {
+ const source = createTestSubject();
+ const inner1 = createTestSubject();
+ const inner2 = createTestSubject();
+
+ const flattened = source.flatMap(value => {
+ if (value === 1) {
+ return inner1;
+ }
+
+ return inner2;
+ });
+
+ const results = [];
+
+ flattened.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, []);
+
+ source.next(1);
+ source.next(2);
+ assert_equals(inner1.subscriberCount(), 1, "inner1 gets subscribed to");
+ assert_equals(inner2.subscriberCount(), 0,
+ "inner2 is queued, not subscribed to until inner1 completes");
+
+ assert_array_equals(results, []);
+
+ // Before `inner1` pushes any values, we first complete the source Observable.
+ // This will not fire completion of the Observable returned from `flatMap()`,
+ // because there are two values (corresponding to inner Observables) that are
+ // queued to the inner queue that need to be processed first. Once the last
+ // one of *those* completes (i.e., `inner2.complete()` further down), then the
+ // returned Observable can finally complete.
+ source.complete();
+ assert_equals(source.subscriberCount(), 0,
+ "source becomes inactive once it completes");
+
+ inner1.next(100);
+ inner1.next(101);
+
+ assert_array_equals(results, [100, 101]);
+
+ inner1.complete();
+ assert_array_equals(results, [100, 101],
+ "Outer completion not triggered after inner1 completes");
+ assert_equals(inner2.subscriberCount(), 1,
+ "inner2 gets un-queued and subscribed after inner1 completes");
+
+ inner2.next(200);
+ inner2.next(201);
+ assert_array_equals(results, [100, 101, 200, 201]);
+
+ inner2.complete();
+ assert_equals(inner2.subscriberCount(), 0,
+ "inner2 becomes inactive once it completes");
+ assert_array_equals(results, [100, 101, 200, 201, "complete"]);
+}, "flatMap(): result Observable does not complete after source Observable " +
+ "completes while there are still queued inner Observables to process " +
+ "Observables all complete");
+
+test(() => {
+ const source = createTestSubject();
+ const inner = createTestSubject();
+ const result = source.flatMap(() => inner);
+
+ const ac = new AbortController();
+
+ result.subscribe({}, { signal: ac.signal, });
+
+ source.next(1);
+
+ assert_equals(inner.subscriberCount(), 1,
+ "inner Observable subscribed to once source emits it");
+
+ ac.abort();
+
+ assert_equals(source.subscriberCount(), 0,
+ "source unsubscribed from, once outer signal is aborted");
+
+ assert_equals(inner.subscriberCount(), 0,
+ "inner Observable unsubscribed from once the outer Observable is " +
+ "subscribed from, as a result of the outer signal being aborted");
+}, "flatMap(): source and inner active Observables are both unsubscribed " +
+ "from once the outer subscription signal is aborted");
+
+// A helper function to create an Observable that can be externally controlled
+// and examined for testing purposes.
+function createTestSubject() {
+ const subscribers = new Set();
+ const subject = new Observable(subscriber => {
+ subscribers.add(subscriber);
+ subscriber.addTeardown(() => subscribers.delete(subscriber));
+ });
+
+ subject.next = value => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.next(value);
+ }
+ };
+ subject.error = error => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.error(error);
+ }
+ };
+ subject.complete = () => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.complete();
+ }
+ };
+ subject.subscriberCount = () => {
+ return subscribers.size;
+ };
+
+ return subject;
+}
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js
new file mode 100644
index 0000000000..80408ddced
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js
@@ -0,0 +1,354 @@
+// Because we test that the global error handler is called at various times.
+setup({allow_uncaught_exception: true});
+
+test(() => {
+ assert_equals(typeof Observable.from, "function",
+ "Observable.from() is a function");
+}, "from(): Observable.from() is a function");
+
+test(() => {
+ assert_throws_js(TypeError, () => Observable.from(10),
+ "Number cannot convert to an Observable");
+ assert_throws_js(TypeError, () => Observable.from(true),
+ "Boolean cannot convert to an Observable");
+ assert_throws_js(TypeError, () => Observable.from("String"),
+ "String cannot convert to an Observable");
+ assert_throws_js(TypeError, () => Observable.from({a: 10}),
+ "Object cannot convert to an Observable");
+ assert_throws_js(TypeError, () => Observable.from(Symbol.iterator),
+ "Bare Symbol.iterator cannot convert to an Observable");
+ assert_throws_js(TypeError, () => Observable.from(Promise),
+ "Promise constructor cannot convert to an Observable");
+}, "from(): Failed conversions");
+
+test(() => {
+ const target = new EventTarget();
+ const observable = target.on('custom');
+ const from_observable = Observable.from(observable);
+ assert_equals(observable, from_observable);
+}, "from(): Given an observable, it returns that exact observable");
+
+test(() => {
+ let completeCalled = false;
+ const results = [];
+ const array = [1, 2, 3, 'a', new Date(), 15, [12]];
+ const observable = Observable.from(array);
+ observable.subscribe({
+ next: v => results.push(v),
+ error: e => assert_unreached('error is not called'),
+ complete: () => completeCalled = true
+ });
+
+ assert_array_equals(results, array);
+ assert_true(completeCalled);
+}, "from(): Given an array");
+
+test(() => {
+ const iterable = {
+ [Symbol.iterator]() {
+ let n = 0;
+ return {
+ next() {
+ n++;
+ if (n <= 3) {
+ return { value: n, done: false };
+ }
+ return { value: undefined, done: true };
+ },
+ };
+ },
+ };
+
+ const observable = Observable.from(iterable);
+
+ assert_true(observable instanceof Observable, "Observable.from() returns an Observable");
+
+ const results = [];
+
+ observable.subscribe({
+ next: (value) => results.push(value),
+ error: () => assert_unreached("should not error"),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [1, 2, 3, "complete"],
+ "Subscription pushes iterable values out to Observable");
+
+ // A second subscription should restart iteration.
+ observable.subscribe({
+ next: (value) => results.push(value),
+ error: () => assert_unreached("should not error"),
+ complete: () => results.push("complete2"),
+ });
+
+ assert_array_equals(results, [1, 2, 3, "complete", 1, 2, 3, "complete2"],
+ "Subscribing again causes another fresh iteration on an un-exhausted iterable");
+}, "from(): Iterable converts to Observable");
+
+// The result of the @@iterator method of the converted object is called:
+// 1. Once on conversion (to test that the value is an iterable).
+// 2. Once on subscription, to re-pull the iterator implementation from the
+// raw JS object that the Observable owns once synchronous iteration is
+// about to begin.
+test(() => {
+ let numTimesSymbolIteratorCalled = 0;
+ let numTimesNextCalled = 0;
+
+ const iterable = {
+ [Symbol.iterator]() {
+ numTimesSymbolIteratorCalled++;
+ return {
+ next() {
+ numTimesNextCalled++;
+ return {value: undefined, done: true};
+ }
+ };
+ }
+ };
+
+ const observable = Observable.from(iterable);
+
+ assert_equals(numTimesSymbolIteratorCalled, 1,
+ "Observable.from(iterable) invokes the @@iterator method getter once");
+ assert_equals(numTimesNextCalled, 0,
+ "Iterator next() is not called until subscription");
+
+ // Override iterable's `[Symbol.iterator]` protocol with an error-throwing
+ // function. We assert that on subscription, this method (the new `@@iterator`
+ // implementation), is called because only the raw JS object gets stored in
+ // the Observable that results in conversion. This raw value must get
+ // re-converted to an iterable once iteration is about to start.
+ const customError = new Error('@@iterator override error');
+ iterable[Symbol.iterator] = () => {
+ throw customError;
+ };
+
+ let thrownError = null;
+ observable.subscribe({
+ error: e => thrownError = e,
+ });
+
+ assert_equals(thrownError, customError,
+ "Error thrown from next() is passed to the error() handler");
+
+ assert_equals(numTimesSymbolIteratorCalled, 1,
+ "Subscription re-invokes @@iterator method, which now is a different " +
+ "method that does *not* increment our assertion value");
+ assert_equals(numTimesNextCalled, 0, "Iterator next() is never called");
+}, "from(): [Symbol.iterator] side-effects (one observable)");
+
+// Similar to the above test, but with more Observables!
+test(() => {
+ let numTimesSymbolIteratorCalled = 0;
+ let numTimesNextCalled = 0;
+
+ const iterable = {
+ [Symbol.iterator]() {
+ numTimesSymbolIteratorCalled++;
+ return {
+ next() {
+ numTimesNextCalled++;
+ return {value: undefined, done: true};
+ }
+ };
+ }
+ };
+
+ const obs1 = Observable.from(iterable);
+ const obs2 = Observable.from(iterable);
+ const obs3 = Observable.from(iterable);
+ const obs4 = Observable.from(obs3);
+
+ assert_equals(numTimesSymbolIteratorCalled, 3, "Observable.from(iterable) invokes the iterator method getter once");
+ assert_equals(numTimesNextCalled, 0, "Iterator next() is not called until subscription");
+
+ iterable[Symbol.iterator] = () => {
+ throw new Error('Symbol.iterator override error');
+ };
+
+ let errorCount = 0;
+
+ const observer = {error: e => errorCount++};
+ obs1.subscribe(observer);
+ obs2.subscribe(observer);
+ obs3.subscribe(observer);
+ obs4.subscribe(observer);
+ assert_equals(errorCount, 4,
+ "Error-throwing `@@iterator` implementation is called once per " +
+ "subscription");
+
+ assert_equals(numTimesSymbolIteratorCalled, 3,
+ "Subscription re-invokes the iterator method getter once");
+ assert_equals(numTimesNextCalled, 0, "Iterator next() is never called");
+}, "from(): [Symbol.iterator] side-effects (many observables)");
+
+test(() => {
+ const customError = new Error('@@iterator next() error');
+ const iterable = {
+ [Symbol.iterator]() {
+ return {
+ next() {
+ throw customError;
+ }
+ };
+ }
+ };
+
+ let thrownError = null;
+ Observable.from(iterable).subscribe({
+ error: e => thrownError = e,
+ });
+
+ assert_equals(thrownError, customError,
+ "Error thrown from next() is passed to the error() handler");
+}, "from(): [Symbol.iterator] next() throws error");
+
+promise_test(async () => {
+ const promise = Promise.resolve('value');
+ const observable = Observable.from(promise);
+
+ assert_true(observable instanceof Observable, "Converts to Observable");
+
+ const results = [];
+
+ observable.subscribe({
+ next: (value) => results.push(value),
+ error: () => assert_unreached("error() is not called"),
+ complete: () => results.push("complete()"),
+ });
+
+ assert_array_equals(results, [], "Observable does not emit synchronously");
+
+ await promise;
+
+ assert_array_equals(results, ["value", "complete()"], "Observable emits and completes after Promise resolves");
+}, "from(): Converts Promise to Observable");
+
+promise_test(async t => {
+ let unhandledRejectionHandlerCalled = false;
+ const unhandledRejectionHandler = () => {
+ unhandledRejectionHandlerCalled = true;
+ };
+
+ self.addEventListener("unhandledrejection", unhandledRejectionHandler);
+ t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler));
+
+ const promise = Promise.reject("reason");
+ const observable = Observable.from(promise);
+
+ assert_true(observable instanceof Observable, "Converts to Observable");
+
+ const results = [];
+
+ observable.subscribe({
+ next: (value) => assert_unreached("next() not called"),
+ error: (error) => results.push(error),
+ complete: () => assert_unreached("complete() not called"),
+ });
+
+ assert_array_equals(results, [], "Observable does not emit synchronously");
+
+ let catchBlockEntered = false;
+ try {
+ await promise;
+ } catch {
+ catchBlockEntered = true;
+ }
+
+ assert_true(catchBlockEntered, "Catch block entered");
+ assert_false(unhandledRejectionHandlerCalled, "No unhandledrejection event");
+ assert_array_equals(results, ["reason"],
+ "Observable emits error() after Promise rejects");
+}, "from(): Converts rejected Promise to Observable. No " +
+ "`unhandledrejection` event when error is handled by subscription");
+
+promise_test(async t => {
+ let unhandledRejectionHandlerCalled = false;
+ const unhandledRejectionHandler = () => {
+ unhandledRejectionHandlerCalled = true;
+ };
+
+ self.addEventListener("unhandledrejection", unhandledRejectionHandler);
+ t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler));
+
+ let errorReported = null;
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ let catchBlockEntered = false;
+ try {
+ const promise = Promise.reject("custom reason");
+ const observable = Observable.from(promise);
+
+ observable.subscribe();
+ await promise;
+ } catch {
+ catchBlockEntered = true;
+ }
+
+ assert_true(catchBlockEntered, "Catch block entered");
+ assert_false(unhandledRejectionHandlerCalled,
+ "No unhandledrejection event, because error got reported to global");
+ assert_not_equals(errorReported, null, "Error was reported to the global");
+
+ assert_true(errorReported.message.includes("custom reason"),
+ "Error message matches");
+ assert_equals(errorReported.lineno, 0, "Error lineno is 0");
+ assert_equals(errorReported.colno, 0, "Error lineno is 0");
+ assert_equals(errorReported.error, "custom reason",
+ "Error object is equivalent");
+}, "from(): Rejections not handled by subscription are reported to the " +
+ "global, and still not sent as an unhandledrejection event");
+
+test(() => {
+ const results = [];
+ const observable = new Observable(subscriber => {
+ subscriber.next('from Observable');
+ subscriber.complete();
+ });
+
+ observable[Symbol.iterator] = () => {
+ results.push('Symbol.iterator() called');
+ return {
+ next() {
+ return {value: 'from @@iterator', done: true};
+ }
+ };
+ };
+
+ Observable.from(observable).subscribe({
+ next: v => results.push(v),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, ["from Observable", "complete"]);
+}, "from(): Observable that implements @@iterator protocol gets converted " +
+ "as an Observable, not iterator");
+
+test(() => {
+ const results = [];
+ const promise = new Promise(resolve => {
+ resolve('from Promise');
+ });
+
+ promise[Symbol.iterator] = () => {
+ let done = false;
+ return {
+ next() {
+ if (!done) {
+ done = true;
+ return {value: 'from @@iterator', done: false};
+ } else {
+ return {value: undefined, done: true};
+ }
+ }
+ };
+ };
+
+ Observable.from(promise).subscribe({
+ next: v => results.push(v),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, ["from @@iterator", "complete"]);
+}, "from(): Promise that implements @@iterator protocol gets converted as " +
+ "an iterable, not Promise");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js
new file mode 100644
index 0000000000..cd39a3700a
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js
@@ -0,0 +1,113 @@
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ // Never exposed to the `last()` promise.
+ subscriber.next(1);
+
+ subscriber.next(2);
+ subscriber.complete();
+ });
+
+ const value = await source.last();
+
+ assert_equals(value, 2);
+}, "last(): Promise resolves to last value");
+
+promise_test(async () => {
+ const error = new Error("error from source");
+ const source = new Observable(subscriber => {
+ subscriber.error(error);
+ });
+
+ let rejection = null;
+ try {
+ await source.last();
+ } catch (e) {
+ rejection = e;
+ }
+
+ assert_equals(rejection, error);
+}, "last(): Promise rejects with emitted error");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ subscriber.complete();
+ });
+
+ let rejection = null;
+ try {
+ await source.last();
+ } catch (e) {
+ rejection = e;
+ }
+
+ assert_true(rejection instanceof RangeError,
+ "Promise rejects with RangeError");
+ assert_equals(rejection.message, "No values in Observable");
+}, "last(): Promise rejects with RangeError when source Observable " +
+ "completes without emitting any values");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {});
+
+ const controller = new AbortController();
+ const promise = source.last({ signal: controller.signal });
+
+ controller.abort();
+
+ let rejection = null;
+ try {
+ await promise;
+ } catch (e) {
+ rejection = e;
+ }
+
+ assert_true(rejection instanceof DOMException,
+ "Promise rejects with a DOMException for abortion");
+ assert_equals(rejection.name, "AbortError",
+ "Rejected with 'AbortError' DOMException");
+ assert_equals(rejection.message, "signal is aborted without reason");
+}, "last(): Aborting a signal rejects the Promise with an AbortError DOMException");
+
+promise_test(async () => {
+ const results = [];
+ const source = new Observable(subscriber => {
+ results.push("source subscribe");
+ subscriber.addTeardown(() => results.push("source teardown"));
+ subscriber.signal.addEventListener("abort", () => results.push("source abort"));
+ results.push("before source next 1");
+ subscriber.next(1);
+ results.push("after source next 1");
+ results.push("before source complete");
+ subscriber.complete();
+ results.push("after source complete");
+ });
+
+ results.push("calling last");
+ const promise = source.last();
+
+ assert_array_equals(results, [
+ "calling last",
+ "source subscribe",
+ "before source next 1",
+ "after source next 1",
+ "before source complete",
+ "source teardown",
+ "source abort",
+ "after source complete",
+ ], "Array values after last() is called");
+
+ const lastValue = await promise;
+ results.push(`last resolved with: ${lastValue}`);
+
+ assert_array_equals(results, [
+ "calling last",
+ "source subscribe",
+ "before source next 1",
+ "after source next 1",
+ "before source complete",
+ "source teardown",
+ "source abort",
+ "after source complete",
+ "last resolved with: 1",
+ ], "Array values after Promise is awaited");
+}, "last(): Lifecycle");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js
new file mode 100644
index 0000000000..836a39a68e
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js
@@ -0,0 +1,252 @@
+test(() => {
+ const source = createTestSubject();
+ const inner1 = createTestSubject();
+ const inner2 = createTestSubject();
+
+ const result = source.switchMap((value, index) => {
+ if (value === 1) {
+ return inner1;
+ }
+ if (value === 2) {
+ return inner2;
+ }
+ throw new Error("invalid ");
+ });
+
+ const results = [];
+
+ result.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_equals(source.subscriberCount(), 1,
+ "source observable is subscribed to");
+
+ source.next(1);
+ assert_equals(inner1.subscriberCount(), 1,
+ "inner1 observable is subscribed to");
+
+ inner1.next("1a");
+ assert_array_equals(results, ["1a"]);
+
+ inner1.next("1b");
+ assert_array_equals(results, ["1a", "1b"]);
+
+ source.next(2);
+ assert_equals(inner1.subscriberCount(), 0,
+ "inner1 observable is unsubscribed from");
+ assert_equals(inner2.subscriberCount(), 1,
+ "inner2 observable is subscribed to");
+
+ inner2.next("2a");
+ assert_array_equals(results, ["1a", "1b", "2a"]);
+
+ inner2.next("2b");
+ assert_array_equals(results, ["1a", "1b", "2a", "2b"]);
+
+ inner2.complete();
+ assert_array_equals(results, ["1a", "1b", "2a", "2b"]);
+
+ source.complete();
+ assert_array_equals(results, ["1a", "1b", "2a", "2b", "complete"]);
+}, "switchMap(): result subscribes to one inner observable at a time, " +
+ "unsubscribing from the previous active one when a new one replaces it");
+
+test(() => {
+ const source = createTestSubject();
+ const inner = createTestSubject();
+
+ const result = source.switchMap(() => inner);
+
+ const results = [];
+
+ result.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_equals(source.subscriberCount(), 1,
+ "source observable is subscribed to");
+ assert_equals(inner.subscriberCount(), 0,
+ "inner observable is not subscribed to");
+
+ source.next(1);
+ assert_equals(inner.subscriberCount(), 1,
+ "inner observable is subscribed to");
+
+ inner.next("a");
+ assert_array_equals(results, ["a"]);
+
+ inner.next("b");
+ assert_array_equals(results, ["a", "b"]);
+
+ source.complete();
+ assert_array_equals(results, ["a", "b"],
+ "Result observable does not complete when source observable completes, " +
+ "because inner is still active");
+
+ inner.next("c");
+ assert_array_equals(results, ["a", "b", "c"]);
+
+ inner.complete();
+ assert_array_equals(results, ["a", "b", "c", "complete"],
+ "Result observable completes when inner observable completes, because " +
+ "source is already complete");
+}, "switchMap(): result does not complete when the source observable " +
+ "completes, if the inner observable is still active");
+
+test(() => {
+ const source = createTestSubject();
+
+ const e = new Error('thrown from mapper');
+ const result = source.switchMap(() => {
+ throw e;
+ });
+
+ const results = [];
+
+ result.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ assert_equals(source.subscriberCount(), 1,
+ "source observable is subscribed to");
+
+ source.next(1);
+ assert_array_equals(results, [e]);
+ assert_equals(source.subscriberCount(), 0,
+ "source observable is unsubscribed from");
+}, "switchMap(): result emits an error if Mapper callback throws an error");
+
+test(() => {
+ const source = createTestSubject();
+ const inner = createTestSubject();
+
+ const result = source.switchMap(() => inner);
+
+ const results = [];
+
+ result.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ source.next(1);
+ inner.next("a");
+ assert_array_equals(results, ["a"]);
+
+ const e = new Error('error from source');
+ source.error(e);
+ assert_array_equals(results, ["a", e],
+ "switchMap result emits an error if the source emits an error");
+ assert_equals(inner.subscriberCount(), 0,
+ "inner observable is unsubscribed from");
+ assert_equals(source.subscriberCount(), 0,
+ "source observable is unsubscribed from");
+}, "switchMap(): result emits an error if the source observable emits an " +
+ "error");
+
+test(() => {
+ const source = createTestSubject();
+ const inner = createTestSubject();
+
+ const result = source.switchMap(() => inner);
+
+ const results = [];
+
+ result.subscribe({
+ next: v => results.push(v),
+ error: e => results.push(e),
+ complete: () => results.push("complete"),
+ });
+
+ source.next(1);
+ inner.next("a");
+ assert_array_equals(results, ["a"]);
+
+ const e = new Error("error from inner");
+ inner.error(e);
+ assert_array_equals(results, ["a", e],
+ "result emits an error if the inner observable emits an error");
+ assert_equals(inner.subscriberCount(), 0,
+ "inner observable is unsubscribed from");
+ assert_equals(source.subscriberCount(), 0,
+ "source observable is unsubscribed from");
+}, "switchMap(): result emits an error if the inner observable emits an error");
+
+test(() => {
+ const results = [];
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.addTeardown(() => {
+ results.push('source teardown');
+ });
+ subscriber.signal.onabort = e => {
+ results.push('source onabort');
+ };
+ });
+
+ const inner = new Observable(subscriber => {
+ subscriber.addTeardown(() => {
+ results.push('inner teardown');
+ });
+ subscriber.signal.onabort = () => {
+ results.push('inner onabort');
+ };
+ });
+
+ const result = source.switchMap(() => inner);
+
+ const ac = new AbortController();
+ result.subscribe({
+ next: v => results.push(v),
+ error: e => results.error(e),
+ complete: () => results.complete("complete"),
+ }, {signal: ac.signal});
+
+ ac.abort();
+ assert_array_equals(results, [
+ "source teardown",
+ "source onabort",
+ "inner teardown",
+ "inner onabort",
+ ], "Unsubscription order is correct");
+}, "switchMap(): should unsubscribe in the correct order when user aborts " +
+ "the subscription");
+
+// A helper function to create an Observable that can be externally controlled
+// and examined for testing purposes.
+function createTestSubject() {
+ const subscribers = new Set();
+ const subject = new Observable(subscriber => {
+ subscribers.add(subscriber);
+ subscriber.addTeardown(() => subscribers.delete(subscriber));
+ });
+
+ subject.next = value => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.next(value);
+ }
+ };
+ subject.error = error => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.error(error);
+ }
+ };
+ subject.complete = () => {
+ for (const subscriber of Array.from(subscribers)) {
+ subscriber.complete();
+ }
+ };
+ subject.subscriberCount = () => {
+ return subscribers.size;
+ };
+
+ return subject;
+}