summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/observable
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/dom/observable
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/dom/observable')
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/idlharness.html20
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js935
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-constructor.window.js127
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-event-target.any.js71
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-event-target.window.js39
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-forEach.any.js184
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-forEach.window.js59
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js324
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.window.js61
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-toArray.any.js174
10 files changed, 1994 insertions, 0 deletions
diff --git a/testing/web-platform/tests/dom/observable/tentative/idlharness.html b/testing/web-platform/tests/dom/observable/tentative/idlharness.html
new file mode 100644
index 0000000000..9a3842c4e6
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/idlharness.html
@@ -0,0 +1,20 @@
+<!doctype html>
+ <meta charset="utf-8" />
+ <meta name="author" title="Keith Cirkel" href="mailto:keithamus@github.com" />
+ <link rel="help" href="https://open-ui.org/components/invokers.explainer/" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+
+ <script>
+ idl_test(["observable.tentative"], ["dom"], (idl_array) => {
+ idl_array.add_objects({
+ Observable: ["new Observable(() => {})"],
+ Subscriber: [
+ "(() => { let s = null; new Observable(_s => s = _s).subscribe({}); return s })()",
+ ],
+ });
+ });
+ </script>
+</t>
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
new file mode 100644
index 0000000000..f108e902b3
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js
@@ -0,0 +1,935 @@
+// Because we test that the global error handler is called at various times.
+setup({allow_uncaught_exception: true});
+
+test(() => {
+ assert_implements(self.Observable, "The Observable interface is not implemented");
+
+ assert_true(
+ typeof Observable === "function",
+ "Observable constructor is defined"
+ );
+
+ assert_throws_js(TypeError, () => { new Observable(); });
+}, "Observable constructor");
+
+test(() => {
+ let initializerCalled = false;
+ const source = new Observable(() => {
+ initializerCalled = true;
+ });
+
+ assert_false(
+ initializerCalled,
+ "initializer should not be called by construction"
+ );
+ source.subscribe();
+ assert_true(initializerCalled, "initializer should be called by subscribe");
+}, "subscribe() can be called with no arguments");
+
+test(() => {
+ assert_implements(self.Subscriber, "The Subscriber interface is not implemented");
+ assert_true(
+ typeof Subscriber === "function",
+ "Subscriber interface is defined as a function"
+ );
+
+ assert_throws_js(TypeError, () => { new Subscriber(); });
+
+ let initializerCalled = false;
+ new Observable(subscriber => {
+ assert_not_equals(subscriber, undefined, "A Subscriber must be passed into the subscribe callback");
+ assert_implements(subscriber.next, "A Subscriber object must have a next() method");
+ assert_implements(subscriber.complete, "A Subscriber object must have a complete() method");
+ assert_implements(subscriber.error, "A Subscriber object must have an error() method");
+ initializerCalled = true;
+ }).subscribe();
+ assert_true(initializerCalled, "initializer should be called by subscribe");
+}, "Subscriber interface is not constructible");
+
+test(() => {
+ let initializerCalled = false;
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ initializerCalled = true;
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ });
+
+ assert_false(
+ initializerCalled,
+ "initializer should not be called by construction"
+ );
+
+ source.subscribe(x => results.push(x));
+
+ assert_true(initializerCalled, "initializer should be called by subscribe");
+ assert_array_equals(
+ results,
+ [1, 2, 3],
+ "should emit values synchronously, but not complete"
+ );
+}, "Subscribe with just a function as the next handler");
+
+test(() => {
+ let initializerCalled = false;
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ initializerCalled = true;
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ assert_false(
+ initializerCalled,
+ "initializer should not be called by construction"
+ );
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: () => assert_unreached("error should not be called"),
+ complete: () => results.push("complete"),
+ });
+
+ assert_true(initializerCalled, "initializer should be called by subscribe");
+ assert_array_equals(
+ results,
+ [1, 2, 3, "complete"],
+ "should emit values synchronously"
+ );
+}, "Observable constructor calls initializer on subscribe");
+
+test(() => {
+ const error = new Error("error");
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.error(error);
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (e) => results.push(e),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, error],
+ "should emit error synchronously"
+ );
+}, "Observable error path called synchronously");
+
+test(() => {
+ let subscriber;
+ new Observable(s => { subscriber = s }).subscribe();
+ const {next, complete, error} = subscriber;
+ assert_throws_js(TypeError, () => next(1));
+ assert_throws_js(TypeError, () => complete());
+ assert_throws_js(TypeError, () => error(1));
+}, "Subscriber must have receiver");
+
+test(() => {
+ let subscriber;
+ new Observable(s => { subscriber = s }).subscribe();
+ assert_throws_js(TypeError, () => subscriber.next());
+ assert_throws_js(TypeError, () => subscriber.error());
+}, "Subscriber next & error must recieve argument");
+
+test(() => {
+ let subscriber;
+ new Observable(s => { subscriber = s }).subscribe();
+ assert_true(subscriber.active);
+ assert_false(subscriber.signal.aborted);
+ subscriber.complete();
+ assert_false(subscriber.active);
+ assert_true(subscriber.signal.aborted);
+}, "Subscriber complete() will set active to false, and abort signal");
+
+test(() => {
+ let subscriber;
+ new Observable(s => { subscriber = s }).subscribe();
+ assert_true(subscriber.active);
+ subscriber.active = false;
+ assert_true(subscriber.active);
+}, "Subscriber active is readonly");
+
+test(() => {
+ let subscriber;
+ new Observable(s => { subscriber = s }).subscribe();
+ assert_false(subscriber.signal.aborted);
+ const oldSignal = subscriber.signal;
+ const newSignal = AbortSignal.abort();
+ subscriber.signal = newSignal;
+ assert_false(subscriber.signal.aborted);
+ assert_equals(subscriber.signal, oldSignal, "signal did not change");
+}, "Subscriber signal is readonly");
+
+test(() => {
+ const error = new Error("error");
+ const results = [];
+ let errorReported = null;
+ let innerSubscriber = null;
+ let subscriptionActivityInFinallyAfterThrow;
+ let subscriptionActivityInErrorHandlerAfterThrow;
+
+ self.addEventListener("error", e => errorReported = e, {once: true});
+
+ const source = new Observable((subscriber) => {
+ innerSubscriber = subscriber;
+ subscriber.next(1);
+ try {
+ throw error;
+ } finally {
+ subscriptionActivityInFinallyAfterThrow = subscriber.active;
+ }
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (e) => {
+ subscriptionActivityInErrorHandlerAfterThrow = innerSubscriber.active;
+ results.push(e);
+ },
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ assert_equals(errorReported, null, "The global error handler should not be " +
+ "invoked when the subscribe callback throws an error and the " +
+ "subscriber has given an error handler");
+ assert_true(subscriptionActivityInFinallyAfterThrow, "Subscriber is " +
+ "considered active in finally block before error handler is invoked");
+ assert_false(subscriptionActivityInErrorHandlerAfterThrow, "Subscriber is " +
+ "considered inactive in error handler block after thrown error");
+ assert_array_equals(
+ results,
+ [1, error],
+ "should emit values and the thrown error synchronously"
+ );
+}, "Observable should error if initializer throws");
+
+test(t => {
+ let innerSubscriber = null;
+ let activeBeforeComplete = false;
+ let activeAfterComplete = false;
+ let activeDuringComplete = false;
+ let abortedBeforeComplete = false;
+ let abortedDuringComplete = false;
+ let abortedAfterComplete = false;
+
+ const source = new Observable((subscriber) => {
+ innerSubscriber = subscriber;
+ activeBeforeComplete = subscriber.active;
+ abortedBeforeComplete = subscriber.signal.aborted;
+
+ subscriber.complete();
+ activeAfterComplete = subscriber.active;
+ abortedAfterComplete = subscriber.signal.aborted;
+ });
+
+ source.subscribe({
+ complete: () => {
+ activeDuringComplete = innerSubscriber.active
+ abortedDuringComplete = innerSubscriber.active
+ }
+ });
+ 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(activeAfterComplete, "Subscription is not active after complete");
+ assert_true(abortedAfterComplete, "Subscription is aborted after complete");
+}, "Subscription is inactive after complete()");
+
+test(t => {
+ let innerSubscriber = null;
+ let activeBeforeError = false;
+ let activeAfterError = false;
+ let activeDuringError = false;
+ let abortedBeforeError = false;
+ let abortedDuringError = false;
+ let abortedAfterError = false;
+
+ const error = new Error("error");
+ const source = new Observable((subscriber) => {
+ innerSubscriber = subscriber;
+ activeBeforeError = subscriber.active;
+ abortedBeforeError = subscriber.signal.aborted;
+
+ subscriber.error(error);
+ activeAfterError = subscriber.active;
+ abortedAfterError = subscriber.signal.aborted;
+ });
+
+ source.subscribe({
+ error: () => {
+ activeDuringError = innerSubscriber.active
+ }
+ });
+ 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(activeAfterError, "Subscription is not active after error");
+ assert_true(abortedAfterError, "Subscription is not aborted after error");
+}, "Subscription is inactive after error()");
+
+test(t => {
+ let innerSubscriber;
+ let initialActivity;
+ let initialSignalAborted;
+
+ const source = new Observable((subscriber) => {
+ innerSubscriber = subscriber;
+ initialActivity = subscriber.active;
+ initialSignalAborted = subscriber.signal.aborted;
+ });
+
+ source.subscribe({}, {signal: AbortSignal.abort('Initially aborted')});
+ assert_false(initialActivity);
+ assert_true(initialSignalAborted);
+ assert_equals(innerSubscriber.signal.reason, 'Initially aborted');
+}, "Subscription is inactive when aborted signal is passed in");
+
+test(() => {
+ let outerSubscriber = null;
+
+ const source = new Observable(subscriber => outerSubscriber = subscriber);
+
+ const controller = new AbortController();
+ source.subscribe({}, {signal: controller.signal});
+
+ assert_not_equals(controller.signal, outerSubscriber.signal);
+}, "Subscriber#signal is not the same AbortSignal as the one passed into `subscribe()`");
+
+test(() => {
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.complete();
+ subscriber.next(3);
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: () => assert_unreached("error should not be called"),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, "complete"],
+ "should emit values synchronously, but not nexted values after complete"
+ );
+}, "Subscription does not emit values after completion");
+
+test(() => {
+ const error = new Error("error");
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.error(error);
+ subscriber.next(3);
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (e) => results.push(e),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, error],
+ "should emit values synchronously, but not nexted values after error"
+ );
+}, "Subscription does not emit values after error");
+
+test(() => {
+ const error = new Error("error");
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.error(error);
+ assert_false(subscriber.active, "subscriber is closed after error");
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (error) => results.push(error),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ assert_array_equals(results, [1, 2, error], "should emit synchronously");
+}, "Completing or nexting a subscriber after an error does nothing");
+
+test(() => {
+ const error = new Error("custom error");
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable((subscriber) => {
+ subscriber.error(error);
+ });
+
+ // No error handler provided...
+ source.subscribe({
+ next: () => assert_unreached("next should not be called"),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ // ... still the exception is reported to the global.
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_equals(errorReported.message, "Uncaught Error: custom error", "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error, "Error object is equivalent");
+}, "Errors pushed to the subscriber that are not handled by the subscription " +
+ "are reported to the global");
+
+test(() => {
+ const error = new Error("custom error");
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable((subscriber) => {
+ throw error;
+ });
+
+ // No error handler provided...
+ source.subscribe({
+ next: () => assert_unreached("next should not be called"),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ // ... still the exception is reported to the global.
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_equals(errorReported.message, "Uncaught Error: custom error", "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error, "Error object is equivalent");
+}, "Errors thrown in the initializer that are not handled by the " +
+ "subscription are reported to the global");
+
+test(() => {
+ const error = new Error("custom error");
+ const results = [];
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.complete();
+ subscriber.error(error);
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: () => assert_unreached("error should not be called"),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, "complete"],
+ "should emit values synchronously, but not error values after complete"
+ );
+
+ // Error reporting still happens even after the subscription is closed.
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_equals(errorReported.message, "Uncaught Error: custom error", "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error, "Error object is equivalent");
+}, "Subscription reports errors that are pushed after subscriber is closed " +
+ "by completion");
+
+test(t => {
+ const error = new Error("custom error");
+ const results = [];
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.complete();
+ throw error;
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: () => assert_unreached("error should not be called"),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [1, 2, "complete"],
+ "should emit values synchronously, but not error after complete"
+ );
+
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_true(errorReported.message.includes("custom error"), "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error, "Error object is equivalent");
+}, "Errors thrown by initializer function after subscriber is closed by " +
+ "completion are reported");
+
+test(() => {
+ const error1 = new Error("error 1");
+ const error2 = new Error("error 2");
+ const results = [];
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.error(error1);
+ throw error2;
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (error) => results.push(error),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, error1],
+ "should emit values synchronously, but not nexted values after error"
+ );
+
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_true(errorReported.message.includes("error 2"), "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error2, "Error object is equivalent");
+}, "Errors thrown by initializer function after subscriber is closed by " +
+ "error are reported");
+
+test(() => {
+ const error1 = new Error("error 1");
+ const error2 = new Error("error 2");
+ const results = [];
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.error(error1);
+ subscriber.error(error2);
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (error) => results.push(error),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, error1],
+ "should emit values synchronously, but not nexted values after error"
+ );
+
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_true(errorReported.message.includes("error 2"), "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error2, "Error object is equivalent");
+}, "Errors pushed by initializer function after subscriber is closed by " +
+ "error are reported");
+
+test(() => {
+ const results = [];
+ const target = new EventTarget();
+
+ const source = new Observable((subscriber) => {
+ target.addEventListener('custom event', e => {
+ subscriber.next(1);
+ subscriber.complete();
+ subscriber.error('not a real error');
+ });
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (error) => results.push(error),
+ complete: () => {
+ results.push('complete'),
+ // Re-entrantly tries to invoke `complete()`. However, this function must
+ // only ever run once.
+ target.dispatchEvent(new Event('custom event'));
+ },
+ });
+
+ target.dispatchEvent(new Event('custom event'));
+
+ assert_array_equals(
+ results,
+ [1, 'complete'],
+ "complete() can only be called once, and cannot invoke other Observer methods"
+ );
+}, "Subscriber#complete() cannot re-entrantly invoke itself");
+
+test(() => {
+ const results = [];
+ const target = new EventTarget();
+
+ const source = new Observable((subscriber) => {
+ target.addEventListener('custom event', e => {
+ subscriber.next(1);
+ subscriber.error('not a real error');
+ subscriber.complete();
+ });
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (error) => {
+ results.push('error'),
+ // Re-entrantly tries to invoke `error()`. However, this function must
+ // only ever run once.
+ target.dispatchEvent(new Event('custom event'));
+ },
+ complete: () => results.push('complete'),
+ });
+
+ target.dispatchEvent(new Event('custom event'));
+
+ assert_array_equals(
+ results,
+ [1, 'error'],
+ "error() can only be called once, and cannot invoke other Observer methods"
+ );
+}, "Subscriber#error() cannot re-entrantly invoke itself");
+
+test(() => {
+ const results = [];
+ let innerSubscriber = null;
+ let activeDuringTeardown1 = null;
+ let abortedDuringTeardown1 = null;
+ let activeDuringTeardown2 = null;
+ let abortedDuringTeardown2 = null;
+
+ const source = new Observable((subscriber) => {
+ assert_true(subscriber.active);
+ assert_false(subscriber.signal.aborted);
+ results.push('subscribe() callback');
+ innerSubscriber = subscriber;
+
+ subscriber.signal.addEventListener('abort', () => {
+ assert_false(subscriber.active);
+ assert_true(subscriber.signal.aborted);
+ results.push('inner abort handler');
+ subscriber.next('next from inner abort handler');
+ subscriber.complete();
+ });
+
+ subscriber.addTeardown(() => {
+ activeDuringTeardown1 = subscriber.active;
+ abortedDuringTeardown1 = subscriber.signal.aborted;
+ results.push('teardown 1');
+ });
+
+ subscriber.addTeardown(() => {
+ activeDuringTeardown2 = subscriber.active;
+ abortedDuringTeardown2 = subscriber.signal.aborted;
+ results.push('teardown 2');
+ });
+ });
+
+ const ac = new AbortController();
+ source.subscribe({
+ // This should never get called. If it is, the array assertion below will fail.
+ next: (x) => results.push(x),
+ complete: () => results.push('complete()')
+ }, {signal: ac.signal});
+
+ ac.signal.addEventListener('abort', () => {
+ results.push('outer abort handler');
+ assert_true(ac.signal.aborted);
+ assert_false(innerSubscriber.signal.aborted);
+ });
+
+ assert_array_equals(results, ['subscribe() callback']);
+ ac.abort();
+ results.push('abort() returned');
+ assert_array_equals(results, [
+ 'subscribe() callback',
+ 'outer abort handler', 'teardown 2', 'teardown 1',
+ 'inner abort handler', 'abort() returned',
+ ]);
+ assert_false(activeDuringTeardown1, 'should not be active during teardown callback 1');
+ assert_false(activeDuringTeardown2, 'should not be active during teardown callback 2');
+ assert_true(abortedDuringTeardown1, 'should be aborted during teardown callback 1');
+ assert_true(abortedDuringTeardown2, 'should be aborted during teardown callback 2');
+}, "Unsubscription lifecycle");
+
+test(t => {
+ const source = new Observable((subscriber) => {
+ let n = 0;
+ while (!subscriber.signal.aborted) {
+ assert_true(subscriber.active);
+ subscriber.next(n++);
+ if (n > 3) {
+ assert_unreached("The subscriber should be closed by now");
+ }
+ }
+ assert_false(subscriber.active);
+ });
+
+ const ac = new AbortController();
+ const results = [];
+
+ source.subscribe({
+ next: (x) => {
+ results.push(x);
+ if (x === 2) {
+ ac.abort();
+ }
+ },
+ error: () => results.push('error'),
+ complete: () => results.push('complete')
+ }, {signal: ac.signal});
+
+ assert_array_equals(
+ results,
+ [0, 1, 2],
+ "should emit values synchronously before abort"
+ );
+}, "Aborting a subscription should stop emitting values");
+
+test(() => {
+ const error = new Error("custom error");
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable(() => {
+ throw error;
+ });
+
+ try {
+ source.subscribe();
+ } catch {
+ assert_unreached("subscriber() never throws an error");
+ }
+
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_true(errorReported.message.includes("custom error"), "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error, "Error object is equivalent");
+}, "Calling subscribe should never throw an error synchronously, initializer throws error");
+
+test(() => {
+ const error = new Error("custom error");
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable((subscriber) => {
+ subscriber.error(error);
+ });
+
+ try {
+ source.subscribe();
+ } catch {
+ assert_unreached("subscriber() never throws an error");
+ }
+
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_true(errorReported.message.includes("custom error"), "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error, "Error object is equivalent");
+}, "Calling subscribe should never throw an error synchronously, subscriber pushes error");
+
+test(() => {
+ let addTeardownCalled = false;
+ let activeDuringTeardown;
+
+ const source = new Observable((subscriber) => {
+ subscriber.addTeardown(() => {
+ addTeardownCalled = true;
+ activeDuringTeardown = subscriber.active;
+ });
+ });
+
+ const ac = new AbortController();
+ source.subscribe({}, {signal: ac.signal});
+
+ assert_false(addTeardownCalled, "Teardown is not be called upon subscription");
+ ac.abort();
+ assert_true(addTeardownCalled, "Teardown is called when subscription is aborted");
+ assert_false(activeDuringTeardown, "Teardown observers inactive subscription");
+}, "Teardown should be called when subscription is aborted");
+
+test(() => {
+ const addTeardownsCalled = [];
+ // This is used to snapshot `addTeardownsCalled` from within the subscribe
+ // callback, for assertion/comparison later.
+ let teardownsSnapshot = [];
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ subscriber.addTeardown(() => addTeardownsCalled.push("teardown 1"));
+ subscriber.addTeardown(() => addTeardownsCalled.push("teardown 2"));
+
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+
+ // We don't run the actual `assert_array_equals` here because if it fails,
+ // it won't be properly caught. This is because assertion failures throw an
+ // error, and in subscriber callback, thrown errors result in
+ // `window.onerror` handlers being called, which this test file doesn't
+ // record as an error (see the first line of this file).
+ teardownsSnapshot = addTeardownsCalled;
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: () => results.push("unreached"),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, 3, "complete"],
+ "should emit values and complete synchronously"
+ );
+
+ assert_array_equals(teardownsSnapshot, addTeardownsCalled);
+ assert_array_equals(addTeardownsCalled, ["teardown 2", "teardown 1"],
+ "Teardowns called in LIFO order synchronously after complete()");
+}, "Teardowns should be called when subscription is closed by completion");
+
+test(() => {
+ const addTeardownsCalled = [];
+ let teardownsSnapshot = [];
+ const error = new Error("error");
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ subscriber.addTeardown(() => addTeardownsCalled.push("teardown 1"));
+ subscriber.addTeardown(() => addTeardownsCalled.push("teardown 2"));
+
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.error(error);
+
+ teardownsSnapshot = addTeardownsCalled;
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (error) => results.push(error),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, 3, error],
+ "should emit values and error synchronously"
+ );
+
+ assert_array_equals(teardownsSnapshot, addTeardownsCalled);
+ assert_array_equals(addTeardownsCalled, ["teardown 2", "teardown 1"],
+ "Teardowns called in LIFO order synchronously after error()");
+}, "Teardowns should be called when subscription is closed by subscriber pushing an error");
+
+test(() => {
+ const addTeardownsCalled = [];
+ const error = new Error("error");
+ const results = [];
+
+ const source = new Observable((subscriber) => {
+ subscriber.addTeardown(() => addTeardownsCalled.push("teardown 1"));
+ subscriber.addTeardown(() => addTeardownsCalled.push("teardown 2"));
+
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ throw error;
+ });
+
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (error) => results.push(error),
+ complete: () => assert_unreached("complete should not be called"),
+ });
+
+ assert_array_equals(
+ results,
+ [1, 2, 3, error],
+ "should emit values and error synchronously"
+ );
+
+ assert_array_equals(addTeardownsCalled, ["teardown 2", "teardown 1"],
+ "Teardowns called in LIFO order synchronously after thrown error");
+}, "Teardowns should be called when subscription is closed by subscriber throwing error");
+
+test(() => {
+ const addTeardownsCalled = [];
+ const results = [];
+ let firstTeardownInvokedSynchronously = false;
+ let secondTeardownInvokedSynchronously = false;
+
+ const source = new Observable((subscriber) => {
+ subscriber.addTeardown(() => addTeardownsCalled.push("teardown 1"));
+ if (addTeardownsCalled.length === 1) {
+ firstTeardownInvokedSynchronously = true;
+ }
+ subscriber.addTeardown(() => addTeardownsCalled.push("teardown 2"));
+ if (addTeardownsCalled.length === 2) {
+ secondTeardownInvokedSynchronously = true;
+ }
+
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const ac = new AbortController();
+ ac.abort();
+ source.subscribe({
+ next: (x) => results.push(x),
+ error: (error) => results.push(error),
+ complete: () => results.push('complete')
+ }, {signal: ac.signal});
+
+ assert_array_equals(results, []);
+ assert_true(firstTeardownInvokedSynchronously, "First teardown callback is invoked during addTeardown()");
+ assert_true(secondTeardownInvokedSynchronously, "Second teardown callback is invoked during addTeardown()");
+ assert_array_equals(addTeardownsCalled, ["teardown 1", "teardown 2"],
+ "Teardowns called synchronously upon addition end up in FIFO order");
+}, "Teardowns should be called synchronously during addTeardown() if the subscription is inactive");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-constructor.window.js b/testing/web-platform/tests/dom/observable/tentative/observable-constructor.window.js
new file mode 100644
index 0000000000..d2b597c819
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-constructor.window.js
@@ -0,0 +1,127 @@
+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 => {
+ // Hang this off of the main document's global, so the child can easily reach
+ // it.
+ window.results = [];
+ const contentWin = await loadIframeAndReturnContentWindow();
+
+ contentWin.eval(`
+ // Get a reference to the parent result array before we detach and lose
+ // access to the parent.
+ const parentResults = parent.results;
+
+ const source = new Observable((subscriber) => {
+ parentResults.push("subscribe");
+ // Detach the iframe and push a value to the subscriber/Observer.
+ window.frameElement.remove();
+ parentResults.push("detached");
+ subscriber.next("next");
+ subscriber.complete();
+ subscriber.error("error");
+ });
+ source.subscribe({
+ next: v => {
+ // Should never run.
+ parentResults.push(v);
+ },
+ complete: () => {
+ // Should never run.
+ parentResults.push("complete");
+ },
+ erorr: e => {
+ // Should never run.
+ parentResults.push(e);
+ }
+ });
+ `);
+
+ assert_array_equals(results, ["subscribe", "detached"]);
+}, "No observer handlers can be invoked in detached document");
+
+promise_test(async t => {
+ const contentWin = await loadIframeAndReturnContentWindow();
+
+ // Set a global error handler on the iframe document's window, and verify that
+ // it is never called (because the thing triggering the error happens when the
+ // document is detached, and "reporting the exception" relies on an attached
+ // document).
+ contentWin.addEventListener("error",
+ t.unreached_func("Error should not be called"), { once: true });
+
+ contentWin.eval(`
+ const source = new Observable((subscriber) => {
+ // Detach the iframe and push an error, which would normally "report the
+ // exception", since this subscriber did not specify an error handler.
+ window.frameElement.remove();
+ subscriber.error("this is an error that should not be reported");
+ });
+ source.subscribe();
+ `);
+}, "Subscriber.error() does not \"report the exception\" even when an " +
+ "`error()` handler is not present, when it is invoked in a detached document");
+
+promise_test(async t => {
+ // Make this available off the global so the child can reach it.
+ window.results = [];
+ const contentWin = await loadIframeAndReturnContentWindow();
+
+ // Set a global error handler on the iframe document's window, and verify that
+ // it is never called (because the thing triggering the error happens when the
+ // document is detached, and "reporting the exception" relies on an attached
+ // document).
+ contentWin.addEventListener("error",
+ t.unreached_func("Error should not be called"), { once: true });
+
+ contentWin.eval(`
+ const parentResults = parent.results;
+ const source = new Observable((subscriber) => {
+ // This should never run.
+ parentResults.push('subscribe');
+ });
+
+ // Detach the iframe and try to subscribe.
+ window.frameElement.remove();
+ parentResults.push('detached');
+ source.subscribe();
+ `);
+
+ assert_array_equals(results, ["detached"], "Subscribe callback is never invoked");
+}, "Cannot subscribe to an Observable in a detached document");
+
+promise_test(async t => {
+ // Make this available off the global so the child can reach it.
+ window.results = [];
+ const contentWin = await loadIframeAndReturnContentWindow();
+
+ contentWin.eval(`
+ const parentResults = parent.results;
+ const event_target = new EventTarget();
+ // Set up two event listeners, both of which will mutate |parentResults|:
+ // 1. A traditional event listener
+ // 2. An observable
+ event_target.addEventListener('customevent', e => parentResults.push(e));
+ const source = event_target.on('customevent');
+ source.subscribe(e => parentResults.push(e));
+
+ // Detach the iframe and fire an event at the event target. The parent will
+ // confirm that the observable's next handler did not get invoked, because
+ // the window is detached.
+ const event = new Event('customevent');
+ window.frameElement.remove();
+ parentResults.push('detached');
+ event_target.dispatchEvent(event);
+ `);
+
+ assert_array_equals(results, ["detached"], "Subscribe callback is never invoked");
+}, "Observable from EventTarget does not get notified for events in detached documents");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-event-target.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-event-target.any.js
new file mode 100644
index 0000000000..0f7ace2acc
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-event-target.any.js
@@ -0,0 +1,71 @@
+test(() => {
+ const target = new EventTarget();
+ assert_implements(target.on, "The EventTarget interface has an `on` method");
+ assert_equals(typeof target.on, "function",
+ "EventTarget should have the on method");
+
+ const testEvents = target.on("test");
+ assert_true(testEvents instanceof Observable,
+ "EventTarget.on returns an Observable");
+
+ const results = [];
+ testEvents.subscribe({
+ next: value => results.push(value),
+ error: () => results.push("error"),
+ complete: () => results.push("complete"),
+ });
+
+ assert_array_equals(results, [],
+ "Observable does not emit events until event is fired");
+
+ const event = new Event("test");
+ target.dispatchEvent(event);
+ assert_array_equals(results, [event]);
+
+ target.dispatchEvent(event);
+ assert_array_equals(results, [event, event]);
+}, "EventTarget.on() returns an Observable");
+
+test(() => {
+ const target = new EventTarget();
+ const testEvents = target.on("test");
+ const ac = new AbortController();
+ const results = [];
+ testEvents.subscribe({
+ next: (value) => results.push(value),
+ error: () => results.push('error'),
+ complete: () => results.complete('complete'),
+ }, { signal: ac.signal });
+
+ assert_array_equals(results, [],
+ "Observable does not emit events until event is fired");
+
+ const event1 = new Event("test");
+ const event2 = new Event("test");
+ const event3 = new Event("test");
+ target.dispatchEvent(event1);
+ target.dispatchEvent(event2);
+
+ assert_array_equals(results, [event1, event2]);
+
+ ac.abort();
+ target.dispatchEvent(event3);
+
+ assert_array_equals(results, [event1, event2],
+ "Aborting the subscription removes the event listener and stops the " +
+ "emission of events");
+}, "Aborting the subscription should stop the emission of events");
+
+test(() => {
+ const target = new EventTarget();
+ const testEvents = target.on("test");
+ const results = [];
+ testEvents.subscribe(e => results.push(e));
+ testEvents.subscribe(e => results.push(e));
+
+ const event1 = new Event("test");
+ const event2 = new Event("test");
+ target.dispatchEvent(event1);
+ target.dispatchEvent(event2);
+ assert_array_equals(results, [event1, event1, event2, event2]);
+}, "EventTarget Observables can multicast subscriptions for event handling");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-event-target.window.js b/testing/web-platform/tests/dom/observable/tentative/observable-event-target.window.js
new file mode 100644
index 0000000000..77a137a362
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-event-target.window.js
@@ -0,0 +1,39 @@
+test(() => {
+ // See https://dom.spec.whatwg.org/#dom-event-eventphase.
+ const CAPTURING_PHASE = 1;
+ const BUBBLING_PHASE = 3;
+
+ // First, create a div underneath the `<body>` element. It will be the
+ // dispatch target for synthetic click events.
+ const target =
+ document.querySelector('body').appendChild(document.createElement('div'));
+
+ const body = document.querySelector('body');
+ const captureObservable = body.on('click', {capture: true});
+ const bubbleObservable = body.on('click', {capture: false});
+
+ const results = [];
+ captureObservable.subscribe(e => results.push(e.eventPhase));
+ bubbleObservable.subscribe(e => results.push(e.eventPhase));
+
+ target.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+
+ assert_array_equals(results, [CAPTURING_PHASE, BUBBLING_PHASE]);
+}, "EventTarget Observables can listen for events in the capturing or bubbling phase");
+
+test(() => {
+ const target = new EventTarget();
+
+ const observable = target.on('event', {passive: true});
+ observable.subscribe(event => {
+ assert_false(event.defaultPrevented);
+ // Should do nothing, since `observable` is "passive".
+ event.preventDefault();
+ assert_false(event.defaultPrevented);
+ });
+
+ // Create a cancelable event which ordinarily would be able to have its
+ // "default" prevented.
+ const event = new Event('event', {cancelable: true});
+ target.dispatchEvent(event);
+}, "EventTarget Observables can be 'passive'");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-forEach.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-forEach.any.js
new file mode 100644
index 0000000000..d0948b295e
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-forEach.any.js
@@ -0,0 +1,184 @@
+promise_test(async (t) => {
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const results = [];
+
+ const completion = source.forEach((value) => {
+ results.push(value);
+ });
+
+ assert_array_equals(results, [1, 2, 3]);
+ await completion;
+}, "forEach(): Visitor callback called synchronously for each value");
+
+promise_test(async (t) => {
+ const error = new Error("error");
+ const source = new Observable((subscriber) => {
+ throw error;
+ });
+
+ try {
+ await source.forEach(() => {
+ assert_unreached("Visitor callback is not invoked when Observable errors");
+ });
+ assert_unreached("forEach() promise does not resolve when Observable errors");
+ } catch (e) {
+ assert_equals(e, error);
+ }
+}, "Errors thrown by Observable reject the returned promise");
+
+promise_test(async (t) => {
+ const error = new Error("error");
+ const source = new Observable((subscriber) => {
+ subscriber.error(error);
+ });
+
+ try {
+ await source.forEach(() => {
+ assert_unreached("Visitor callback is not invoked when Observable errors");
+ });
+ assert_unreached("forEach() promise does not resolve when Observable errors");
+ } catch (reason) {
+ assert_equals(reason, error);
+ }
+}, "Errors pushed by Observable reject the returned promise");
+
+promise_test(async (t) => {
+ // This will be assigned when `source`'s teardown is called during
+ // unsubscription.
+ let abortReason = null;
+
+ const error = new Error("error");
+ const source = new Observable((subscriber) => {
+ // Should be called from within the second `next()` call below, when the
+ // `forEach()` visitor callback throws an error, because that triggers
+ // unsubscription from `source`.
+ subscriber.addTeardown(() => abortReason = subscriber.signal.reason);
+
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const results = [];
+
+ const completion = source.forEach((value) => {
+ results.push(value);
+ if (value === 2) {
+ throw error;
+ }
+ });
+
+ assert_array_equals(results, [1, 2]);
+ assert_equals(abortReason, error,
+ "forEach() visitor callback throwing an error triggers unsubscription " +
+ "from the source observable, with the correct abort reason");
+
+ try {
+ await completion;
+ assert_unreached("forEach() promise does not resolve when visitor throws");
+ } catch (e) {
+ assert_equals(e, error);
+ }
+}, "Errors thrown in the visitor callback reject the promise and " +
+ "unsubscribe from the source");
+
+// See https://github.com/WICG/observable/issues/96 for discussion about the
+// timing of Observable AbortSignal `abort` firing and promise rejection.
+promise_test(async t => {
+ const error = new Error('custom error');
+ let rejectionError = null;
+ let outerAbortEventMicrotaskRun = false,
+ forEachPromiseRejectionMicrotaskRun = false,
+ innerAbortEventMicrotaskRun = false;
+
+ const source = new Observable(subscriber => {
+ subscriber.signal.addEventListener('abort', () => {
+ queueMicrotask(() => {
+ assert_true(outerAbortEventMicrotaskRun,
+ "Inner abort: outer abort microtask has fired");
+ assert_true(forEachPromiseRejectionMicrotaskRun,
+ "Inner abort: forEach rejection microtask has fired");
+ assert_false(innerAbortEventMicrotaskRun,
+ "Inner abort: inner abort microtask has not fired");
+
+ innerAbortEventMicrotaskRun = true;
+ });
+ });
+ });
+
+ const controller = new AbortController();
+ controller.signal.addEventListener('abort', () => {
+ queueMicrotask(() => {
+ assert_false(outerAbortEventMicrotaskRun,
+ "Outer abort: outer abort microtask has not fired");
+ assert_false(forEachPromiseRejectionMicrotaskRun,
+ "Outer abort: forEach rejection microtask has not fired");
+ assert_false(innerAbortEventMicrotaskRun,
+ "Outer abort: inner abort microtask has not fired");
+
+ outerAbortEventMicrotaskRun = true;
+ });
+ });
+
+ const promise = source.forEach(() => {}, {signal: controller.signal}).catch(e => {
+ rejectionError = e;
+ assert_true(outerAbortEventMicrotaskRun,
+ "Promise rejection: outer abort microtask has fired");
+ assert_false(forEachPromiseRejectionMicrotaskRun,
+ "Promise rejection: forEach rejection microtask has not fired");
+ assert_false(innerAbortEventMicrotaskRun,
+ "Promise rejection: inner abort microtask has not fired");
+
+ forEachPromiseRejectionMicrotaskRun = true;
+ });
+
+ // This should trigger the following, in this order:
+ // 1. Fire the `abort` event at the outer AbortSignal, whose handler
+ // manually queues a microtask.
+ // 2. Calls "signal abort" on the outer signal's dependent signals. This
+ // queues a microtask to reject the `forEach()` promise.
+ // 3. Fire the `abort` event at the inner AbortSignal, whose handler
+ // manually queues a microtask.
+ controller.abort(error);
+
+ // After a single task, assert that everything has happened correctly (and
+ // incrementally in the right order);
+ await new Promise(resolve => {
+ t.step_timeout(resolve);
+ });
+ assert_true(outerAbortEventMicrotaskRun,
+ "Final: outer abort microtask has fired");
+ assert_true(forEachPromiseRejectionMicrotaskRun,
+ "Final: forEach rejection microtask has fired");
+ assert_true(innerAbortEventMicrotaskRun,
+ "Final: inner abort microtask has fired");
+ assert_equals(rejectionError, error, "Promise is rejected with the right " +
+ "value");
+}, "forEach visitor callback rejection microtask ordering");
+
+promise_test(async (t) => {
+ const source = new Observable((subscriber) => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const results = [];
+
+ const completion = source.forEach((value) => {
+ results.push(value);
+ });
+
+ assert_array_equals(results, [1, 2, 3]);
+
+ const completionValue = await completion;
+ assert_equals(completionValue, undefined, "Promise resolves with undefined");
+}, "forEach() promise resolves with undefined");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-forEach.window.js b/testing/web-platform/tests/dom/observable/tentative/observable-forEach.window.js
new file mode 100644
index 0000000000..71c2e17303
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-forEach.window.js
@@ -0,0 +1,59 @@
+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 => {
+ window.frameElement.remove();
+
+ // This invokes the forEach() 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.
+ subscriber.next(1);
+ });
+
+ source.forEach(value => {
+ parentResults.push(value);
+ });
+ `);
+
+ // If we got here, we didn't crash! Let's also check that `results` is empty.
+ assert_array_equals(results, []);
+}, "forEach()'s internal observer's next steps do not crash in a detached document");
+
+promise_test(async t => {
+ const contentWin = await loadIframeAndReturnContentWindow();
+
+ window.results = [];
+
+ contentWin.eval(`
+ const parentResults = parent.results;
+
+ const source = new Observable(subscriber => {
+ subscriber.next(1);
+ });
+
+ source.forEach(value => {
+ window.frameElement.remove();
+ parentResults.push(value);
+ });
+ `);
+
+ assert_array_equals(results, [1]);
+}, "forEach()'s internal observer's next steps do not crash when visitor " +
+ "callback detaches the document");
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
new file mode 100644
index 0000000000..6421777e09
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js
@@ -0,0 +1,324 @@
+// Because we test that the global error handler is called at various times.
+setup({allow_uncaught_exception: true});
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ let i = 0;
+ const interval = setInterval(() => {
+ if (i < 5) {
+ subscriber.next(++i);
+ } else {
+ subscriber.complete();
+ clearInterval(interval);
+ }
+ }, 0);
+ });
+
+ const result = await source.takeUntil(new Observable(() => {})).toArray();
+ assert_array_equals(result, [1, 2, 3, 4, 5]);
+}, "takeUntil subscribes to source Observable and mirrors it uninterrupted");
+
+promise_test(async () => {
+ const source = new Observable(() => {});
+ let notifierSubscribedTo = false;
+ const notifier = new Observable(() => notifierSubscribedTo = true);
+
+ source.takeUntil(notifier).subscribe();
+ assert_true(notifierSubscribedTo);
+}, "takeUntil subscribes to notifier");
+
+// This test is important because ordinarily, calling `subscriber.next()` does
+// not cancel a subscription associated with `subscriber`. However, for the
+// `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 => {});
+
+ let notifierSubscriberActiveBeforeNext;
+ let notifierSubscriberActiveAfterNext;
+ let teardownCalledAfterNext;
+ let notifierSignalAbortedAfterNext;
+ const notifier = new Observable(subscriber => {
+ let teardownCalled;
+ subscriber.addTeardown(() => teardownCalled = true);
+
+ // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`.
+ notifierSubscriberActiveBeforeNext = subscriber.active;
+ subscriber.next('value');
+ notifierSubscriberActiveAfterNext = subscriber.active;
+ teardownCalledAfterNext = (teardownCalled === true);
+ notifierSignalAbortedAfterNext = subscriber.signal.aborted;
+ });
+
+ let nextOrErrorCalled = false;
+ let completeCalled = false;
+ source.takeUntil(notifier).subscribe({
+ next: () => nextOrErrorCalled = true,
+ error: () => nextOrErrorCalled = true,
+ complete: () => completeCalled = true,
+ });
+ assert_true(notifierSubscriberActiveBeforeNext);
+ assert_false(notifierSubscriberActiveAfterNext);
+ assert_true(teardownCalledAfterNext);
+ assert_true(notifierSignalAbortedAfterNext);
+ assert_false(nextOrErrorCalled);
+ assert_true(completeCalled);
+}, "takeUntil: notifier next() unsubscribes to 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 => {});
+
+ let notifierSubscriberActiveBeforeNext;
+ let notifierSubscriberActiveAfterNext;
+ let teardownCalledAfterNext;
+ let notifierSignalAbortedAfterNext;
+ const notifier = new Observable(subscriber => {
+ let teardownCalled;
+ subscriber.addTeardown(() => teardownCalled = true);
+
+ // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`.
+ notifierSubscriberActiveBeforeNext = subscriber.active;
+ subscriber.error('error');
+ notifierSubscriberActiveAfterNext = subscriber.active;
+ teardownCalledAfterNext = (teardownCalled === true);
+ notifierSignalAbortedAfterNext = subscriber.signal.aborted;
+ });
+
+ let nextOrErrorCalled = false;
+ let completeCalled = false;
+ source.takeUntil(notifier).subscribe({
+ next: () => nextOrErrorCalled = true,
+ error: () => nextOrErrorCalled = true,
+ complete: () => completeCalled = true,
+ });
+ assert_true(notifierSubscriberActiveBeforeNext);
+ assert_false(notifierSubscriberActiveAfterNext);
+ assert_true(teardownCalledAfterNext);
+ assert_true(notifierSignalAbortedAfterNext);
+ assert_false(nextOrErrorCalled);
+ assert_true(completeCalled);
+}, "takeUntil: notifier error() unsubscribes to notifier");
+
+// Test that `notifier` unsubscribes from source Observable.
+promise_test(async t => {
+ const results = [];
+
+ const source = new Observable(subscriber => {
+ results.push('source subscribed');
+ subscriber.addTeardown(() => results.push('source teardown'));
+ subscriber.signal.addEventListener('abort',
+ e => results.push('source signal abort'));
+ });
+
+ let notifierTeardownCalled = false;
+ const notifier = new Observable(subscriber => {
+ results.push('notifier subscribed');
+ subscriber.addTeardown(() => {
+ results.push('notifier teardown');
+ notifierTeardownCalled = true;
+ });
+ subscriber.signal.addEventListener('abort',
+ e => results.push('notifier signal abort'));
+
+ // Asynchronously shut everything down.
+ t.step_timeout(() => subscriber.next('value'));
+ });
+
+ let nextOrErrorCalled = false;
+ let notifierTeardownCalledBeforeCompleteCallback;
+ await new Promise(resolve => {
+ source.takeUntil(notifier).subscribe({
+ next: () => nextOrErrorCalled = true,
+ error: () => nextOrErrorCalled = true,
+ complete: () => {
+ notifierTeardownCalledBeforeCompleteCallback = notifierTeardownCalled;
+ resolve();
+ },
+ });
+ });
+
+ // The outer `Observer#complete()` callback is called before any teardowns are
+ // invoked.
+ assert_false(nextOrErrorCalled);
+ // 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(notifierTeardownCalled);
+ assert_array_equals(results, [
+ "notifier subscribed",
+ "source subscribed",
+ "notifier teardown",
+ "notifier signal abort",
+ "source teardown",
+ "source signal abort"
+ ]);
+}, "takeUntil: notifier next() unsubscribes from notifier & source observable");
+
+// This test is almost identical to the above test, however instead of the
+// `notifier` Observable being the thing that causes the unsubscription from
+// `notifier` and `source`, it is the outer composite Observable's
+// `SubscribeOptions#signal` being aborted that does this.
+promise_test(async t => {
+ const results = [];
+ // This will get populated later with a function that resolves a promise.
+ let resolver;
+
+ const source = new Observable(subscriber => {
+ results.push('source subscribed');
+ subscriber.addTeardown(() => results.push('source teardown'));
+ subscriber.signal.addEventListener('abort', e => {
+ results.push('source signal abort');
+ // This should be the last thing run in the whole teardown sequence. After
+ // this, we can resolve the promise that this test is waiting on, via
+ // `resolver`. That'll wrap things up and move us on to the assertions.
+ resolver();
+ });
+ });
+
+ const notifier = new Observable(subscriber => {
+ results.push('notifier subscribed');
+ subscriber.addTeardown(() => {
+ results.push('notifier teardown');
+ });
+ subscriber.signal.addEventListener('abort',
+ e => results.push('notifier signal abort'));
+ });
+
+ let observerCallbackCalled = false;
+ await new Promise(resolve => {
+ resolver = resolve;
+ const controller = new AbortController();
+ source.takeUntil(notifier).subscribe({
+ next: () => observerCallbackCalled = true,
+ error: () => observerCallbackCalled = true,
+ complete: () => observerCallbackCalled = true,
+ }, {signal: controller.signal});
+
+ // Asynchronously shut everything down.
+ t.step_timeout(() => controller.abort());
+ });
+
+ assert_false(observerCallbackCalled);
+ assert_array_equals(results, [
+ "notifier subscribed",
+ "source subscribed",
+ "notifier teardown",
+ "notifier signal abort",
+ "source teardown",
+ "source signal abort"
+ ]);
+}, "takeUntil()'s AbortSignal unsubscribes from notifier & source observable");
+
+promise_test(async () => {
+ let sourceSubscribedTo = false;
+ const source = new Observable(subscriber => {
+ sourceSubscribedTo = true;
+ });
+
+ const notifier = new Observable(subscriber => subscriber.next('value'));
+
+ let nextOrErrorCalled = false;
+ let completeCalled = false;
+ const result = source.takeUntil(notifier).subscribe({
+ next: v => nextOrErrorCalled = true,
+ error: e => nextOrErrorCalled = true,
+ complete: () => completeCalled = true,
+ });
+
+ assert_false(sourceSubscribedTo);
+ assert_true(completeCalled);
+ assert_false(nextOrErrorCalled);
+}, "takeUntil: source never subscribed to when notifier synchronously emits a value");
+
+promise_test(async () => {
+ let sourceSubscribedTo = false;
+ const source = new Observable(subscriber => {
+ sourceSubscribedTo = true;
+ });
+
+ const notifier = new Observable(subscriber => subscriber.error('error'));
+
+ let nextOrErrorCalled = false;
+ let completeCalled = false;
+ const result = source.takeUntil(notifier).subscribe({
+ next: v => nextOrErrorCalled = true,
+ error: e => nextOrErrorCalled = true,
+ complete: () => completeCalled = true,
+ });
+
+ assert_false(sourceSubscribedTo);
+ assert_true(completeCalled);
+ assert_false(nextOrErrorCalled);
+}, "takeUntil: source never subscribed to when notifier synchronously emits error");
+
+promise_test(async () => {
+ const source = new Observable(subscriber => {
+ let i = 0;
+ const interval = setInterval(() => {
+ if (i < 5) {
+ subscriber.next(++i);
+ } else {
+ subscriber.complete();
+ clearInterval(interval);
+ }
+ }, 500);
+ });
+
+ const notifier = new Observable(subscriber => subscriber.complete());
+
+ const result = await source.takeUntil(notifier).toArray();
+ assert_array_equals(result, [1, 2, 3, 4, 5]);
+}, "takeUntil: source is uninterrupted when notifier completes, even synchronously");
+
+promise_test(async () => {
+ const results = [];
+
+ let sourceSubscriber;
+ let notifierSubscriber;
+ const source = new Observable(subscriber => sourceSubscriber = subscriber);
+ const notifier = new Observable(subscriber => notifierSubscriber = subscriber);
+
+ source.takeUntil(notifier).subscribe({
+ next: v => results.push(v),
+ complete: () => results.push("complete"),
+ });
+
+ sourceSubscriber.next(1);
+ sourceSubscriber.next(2);
+ notifierSubscriber.next('notifier value');
+ sourceSubscriber.next(3);
+
+ assert_array_equals(results, [1, 2, 'complete']);
+}, "takeUntil() mirrors the source Observable until its first next() value");
+
+promise_test(async t => {
+ let errorReported = null;
+
+ self.addEventListener("error", e => errorReported = e, { once: true });
+
+ const source = new Observable(() => {});
+ const notifier = new Observable(subscriber => {
+ t.step_timeout(() => {
+ subscriber.error('error 1');
+ subscriber.error('error 2');
+ });
+ });
+
+ let errorCallbackCalled = false;
+ await new Promise(resolve => {
+ source.takeUntil(notifier).subscribe({
+ error: e => errorCallbackCalled = true,
+ complete: () => resolve(),
+ });
+ });
+
+ assert_false(errorCallbackCalled);
+ assert_true(errorReported !== null, "Exception was reported to global");
+ assert_equals(errorReported.message, "Uncaught error 2", "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, 'error 2', "Error object is equivalent (just a string)");
+}, "takeUntil: notifier calls `Subscriber#error()` twice; second goes to global error handler");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.window.js b/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.window.js
new file mode 100644
index 0000000000..e4906d5f16
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.window.js
@@ -0,0 +1,61 @@
+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;
+}
+
+// This is a regression test to ensure there is no crash inside `takeUntil()`
+// once `notifier` detaches its document, before `source` is subscribed to.
+promise_test(async () => {
+ // Hang this off of the main document's global, so the child can easily reach
+ // it.
+ window.results = [];
+ const contentWin = await loadIframeAndReturnContentWindow();
+
+ contentWin.eval(`
+ const parentResults = parent.results;
+
+ const source = new Observable(() => parentResults.push('source subscribed'));
+ const notifier = new Observable(() => {
+ parentResults.push('notifier subscribed');
+
+ // Detach this child document.
+ window.frameElement.remove();
+ parentResults.push('notifier has detached document');
+ });
+
+ source.takeUntil(notifier).subscribe();
+ `);
+
+ assert_array_equals(results, ["notifier subscribed", "notifier has detached document"]);
+}, "takeUntil(): notifier Observable detaches document before source " +
+ "Observable would be subscribed to");
+
+promise_test(async () => {
+ window.results = [];
+ const contentWin = await loadIframeAndReturnContentWindow();
+
+ contentWin.eval(`
+ let completeSubscriber, errorSubscriber, notifierSubscriber;
+ const sourceComplete = new Observable(subscriber => completeSubscriber = subscriber);
+ const sourceError = new Observable(subscriber => errorSubscriber = subscriber);
+ const notifier = new Observable(subscriber => notifierSubscriber = subscriber);
+
+ sourceComplete.takeUntil(notifier).subscribe();
+ sourceError.takeUntil(notifier).subscribe();
+
+ // Detach this child document.
+ window.frameElement.remove();
+
+ completeSubscriber.complete();
+ errorSubscriber.error('error');
+ notifierSubscriber.error('error');
+ `);
+}, "takeUntil(): Source and notifier internal observers do not crash in a " +
+ "detached document");
diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-toArray.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-toArray.any.js
new file mode 100644
index 0000000000..9e6e3abee5
--- /dev/null
+++ b/testing/web-platform/tests/dom/observable/tentative/observable-toArray.any.js
@@ -0,0 +1,174 @@
+// Because we test that the global error handler is called at various times.
+setup({allow_uncaught_exception: true});
+
+promise_test(async () => {
+ const observable = new Observable(subscriber => {
+ subscriber.next(1);
+ subscriber.next(2);
+ subscriber.next(3);
+ subscriber.complete();
+ });
+
+ const array = await observable.toArray();
+ assert_array_equals(array, [1, 2, 3]);
+}, "toArray(): basic next/complete");
+
+promise_test(async t => {
+ let errorReported = null;
+ let innerSubscriber = null;
+ self.addEventListener('error', e => errorReported = e, {once: true});
+
+ const error = new Error("custom error");
+ const observable = new Observable(subscriber => {
+ innerSubscriber = subscriber;
+ subscriber.error(error);
+ });
+
+ try {
+ const array = await observable.toArray();
+ assert_unreached("toArray() promise must not resolve");
+ } catch (e) {
+ assert_equals(e, error);
+ assert_equals(errorReported, null);
+
+ // Calls to `error()` after the subscription is closed still report the
+ // exception.
+ innerSubscriber.error(error);
+ assert_not_equals(errorReported, null, "Exception was reported to global");
+ assert_true(errorReported.message.includes("custom error"), "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error, "Error object is equivalent");
+ }
+}, "toArray(): first error() rejects promise; subsequent error()s report the exceptions");
+
+promise_test(async t => {
+ let errorReported = null;
+ let innerSubscriber = null;
+ self.addEventListener('error', e => errorReported = e, {once: true});
+
+ const error = new Error("custom error");
+ const observable = new Observable(subscriber => {
+ innerSubscriber = subscriber;
+ subscriber.complete();
+ });
+
+ const array = await observable.toArray();
+ assert_array_equals(array, []);
+ assert_equals(errorReported, null);
+
+ // Calls to `error()` after the subscription is closed still report the
+ // exception.
+ innerSubscriber.error(error);
+ assert_not_equals(errorReported, null, "Exception was reported to global");
+ assert_true(errorReported.message.includes("custom error"), "Error message matches");
+ assert_greater_than(errorReported.lineno, 0, "Error lineno is greater than 0");
+ assert_greater_than(errorReported.colno, 0, "Error lineno is greater than 0");
+ assert_equals(errorReported.error, error, "Error object is equivalent");
+}, "toArray(): complete() resolves promise; subsequent error()s report the exceptions");
+
+promise_test(async () => {
+ // This tracks whether `postSubscriptionPromise` has had its then handler run.
+ // This helps us keep track of the timing/ordering of everything. Calling a
+ // Promise-returning operator with an aborted signal must *immediately* reject
+ // the returned Promise, which means code "awaiting" it should run before any
+ // subsequent Promise resolution/rejection handlers are run.
+ let postSubscriptionPromiseResolved = false;
+ let subscriptionImmediatelyInactive = false;
+
+ const observable = new Observable(subscriber => {
+ const inactive = !subscriber.active;
+ subscriptionImmediatelyInactive = inactive;
+ });
+
+ const rejectedPromise = observable.toArray({signal: AbortSignal.abort()})
+ .then(() => {
+ assert_unreached("Operator promise must not resolve its abort signal is " +
+ "rejected");
+ }, () => {
+ // See the documentation above. The rejection handler (i.e., this code) for
+ // immediately-aborted operator Promises runs before any later-scheduled
+ // Promise resolution/rejections.
+ assert_false(postSubscriptionPromiseResolved,
+ "Operator promise rejects before later promise");
+ });
+ const postSubscriptionPromise =
+ Promise.resolve().then(() => postSubscriptionPromiseResolved = true);
+
+ await rejectedPromise;
+}, "toArray(): Subscribing with an aborted signal returns an immediately " +
+ "rejected promise");
+
+promise_test(async () => {
+ let postSubscriptionPromiseResolved = false;
+
+ const observable = new Observable(subscriber => {});
+ const controller = new AbortController();
+ const arrayPromise = observable.toArray({signal: controller.signal})
+ .then(() => {
+ assert_unreached("Operator promise must not resolve if its abort signal " +
+ "is rejected");
+ }, () => {
+ assert_false(postSubscriptionPromiseResolved,
+ "controller.abort() synchronously rejects the operator " +
+ "Promise");
+ });
+
+ // This must synchronously reject `arrayPromise`, scheduling in the next
+ // microtask.
+ controller.abort();
+ Promise.resolve().then(value => postSubscriptionPromiseResolved = true);
+
+ await arrayPromise;
+}, "toArray(): Aborting the passed-in signal rejects the returned promise");
+
+// See https://github.com/WICG/observable/issues/96 for discussion about this.
+promise_test(async () => {
+ const results = [];
+
+ const observable = new Observable(subscriber => {
+ results.push(`Subscribed. active: ${subscriber.active}`);
+
+ subscriber.signal.addEventListener('abort', e => {
+ results.push("Inner signal abort event");
+ Promise.resolve("Inner signal Promise").then(value => results.push(value));
+ });
+
+ subscriber.addTeardown(() => {
+ results.push("Teardown");
+ Promise.resolve("Teardown Promise").then(value => results.push(value));
+ });
+ });
+
+ const controller = new AbortController();
+ controller.signal.addEventListener('abort', e => {
+ results.push("Outer signal abort event");
+ Promise.resolve("Outer signal Promise").then(value => results.push(value));
+ });
+
+ // Subscribe.
+ observable.toArray({signal: controller.signal});
+ controller.abort();
+
+ assert_array_equals(results, [
+ "Subscribed. active: true",
+ "Outer signal abort event",
+ "Teardown",
+ "Inner signal abort event",
+ ], "Events and teardowns are fired in the right ordered");
+
+ // Everything microtask above should be queued up by now, so queue one more
+ // final microtask that will run after all of the others, wait for it, and the
+ // check `results` is right.
+ await Promise.resolve();
+ assert_array_equals(results, [
+ "Subscribed. active: true",
+ "Outer signal abort event",
+ "Teardown",
+ "Inner signal abort event",
+ "Outer signal Promise",
+ "Teardown Promise",
+ "Inner signal Promise",
+ ], "Promises resolve in the right order");
+}, "Operator Promise abort ordering");
+