summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js')
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js935
1 files changed, 935 insertions, 0 deletions
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");