From 086c044dc34dfc0f74fbe41f4ecb402b2cd34884 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:13:33 +0200 Subject: Merging upstream version 125.0.1. Signed-off-by: Daniel Baumann --- .../tentative/observable-constructor.any.js | 35 ++++- .../observable/tentative/observable-drop.any.js | 152 +++++++++++++++++++ .../observable/tentative/observable-filter.any.js | 105 +++++++++++++ .../dom/observable/tentative/observable-map.any.js | 166 +++++++++++++++++++++ .../observable/tentative/observable-map.window.js | 40 +++++ .../observable/tentative/observable-take.any.js | 108 ++++++++++++++ .../tentative/observable-takeUntil.any.js | 130 +++++++++------- 7 files changed, 679 insertions(+), 57 deletions(-) create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-drop.any.js create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-map.any.js create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-map.window.js create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-take.any.js (limited to 'testing/web-platform/tests/dom/observable/tentative') diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js index f108e902b3..2cd2ee2b66 100644 --- a/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js +++ b/testing/web-platform/tests/dom/observable/tentative/observable-constructor.any.js @@ -235,14 +235,18 @@ test(t => { source.subscribe({ complete: () => { - activeDuringComplete = innerSubscriber.active - abortedDuringComplete = innerSubscriber.active + activeDuringComplete = innerSubscriber.active; + abortedDuringComplete = innerSubscriber.signal.aborted; } }); assert_true(activeBeforeComplete, "Subscription is active before complete"); assert_false(abortedBeforeComplete, "Subscription is not aborted before complete"); - assert_false(activeDuringComplete, "Subscription is not active during complete"); - assert_false(abortedDuringComplete, "Subscription is not aborted during complete"); + assert_false(activeDuringComplete, + "Subscription becomes inactive during Subscriber#complete(), just " + + "before Observer#complete() callback is invoked"); + assert_true(abortedDuringComplete, + "Subscription's signal is aborted during Subscriber#complete(), just " + + "before Observer#complete() callback is invoked"); assert_false(activeAfterComplete, "Subscription is not active after complete"); assert_true(abortedAfterComplete, "Subscription is aborted after complete"); }, "Subscription is inactive after complete()"); @@ -269,13 +273,18 @@ test(t => { source.subscribe({ error: () => { - activeDuringError = innerSubscriber.active + activeDuringError = innerSubscriber.active; + abortedDuringError = innerSubscriber.signal.aborted; } }); assert_true(activeBeforeError, "Subscription is active before error"); assert_false(abortedBeforeError, "Subscription is not aborted before error"); - assert_false(activeDuringError, "Subscription is not active during error"); - assert_false(abortedDuringError, "Subscription is not aborted during error"); + assert_false(activeDuringError, + "Subscription becomes inactive during Subscriber#error(), just " + + "before Observer#error() callback is invoked"); + assert_true(abortedDuringError, + "Subscription's signal is aborted during Subscriber#error(), just " + + "before Observer#error() callback is invoked"); assert_false(activeAfterError, "Subscription is not active after error"); assert_true(abortedAfterError, "Subscription is not aborted after error"); }, "Subscription is inactive after error()"); @@ -690,6 +699,18 @@ test(() => { assert_true(abortedDuringTeardown2, 'should be aborted during teardown callback 2'); }, "Unsubscription lifecycle"); +test(t => { + let innerSubscriber = null; + const source = new Observable(subscriber => { + innerSubscriber = subscriber; + subscriber.error('calling error()'); + }); + + source.subscribe(); + assert_equals(innerSubscriber.signal.reason, "calling error()", + "Reason is set correctly"); +}, "Subscriber#error() value is stored as Subscriber's AbortSignal's reason"); + test(t => { const source = new Observable((subscriber) => { let n = 0; diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-drop.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-drop.any.js new file mode 100644 index 0000000000..4b15fedfd3 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-drop.any.js @@ -0,0 +1,152 @@ +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.next(4); + subscriber.complete(); + }); + + const results = []; + + source.drop(2).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [3, 4, "complete"]); +}, "drop(): Observable should skip the first n values from the source " + + "observable, then pass through the rest of the values and completion"); + +test(() => { + const error = new Error('source error'); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.next(4); + subscriber.error(error); + }); + + const results = []; + + source.drop(2).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [3, 4, error]); +}, "drop(): Observable passes through errors from source Observable"); + +test(() => { + const error = new Error('source error'); + const source = new Observable(subscriber => { + subscriber.error(error); + subscriber.next(1); + }); + + const results = []; + + source.drop(2).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [error]); +}, "drop(): Observable passes through errors from source observable even " + + "before drop count is met"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.complete(); + }); + + const results = []; + + source.drop(2).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["complete"]); +}, "drop(): Observable passes through completions from source observable even " + + "before drop count is met"); + +test(() => { + let sourceTeardownCalled = false; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => sourceTeardownCalled = true); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.next(4); + subscriber.next(5); + subscriber.complete(); + }); + + const results = []; + + const controller = new AbortController(); + + source.drop(2).subscribe({ + next: v => { + results.push(v); + if (v === 3) { + controller.abort(); + } + }, + error: (e) => results.push(e), + complete: () => results.push("complete"), + }, {signal: controller.signal}); + + assert_true(sourceTeardownCalled, + "Aborting outer observable unsubscribes the source observable"); + assert_array_equals(results, [3]); +}, "drop(): Unsubscribing from the Observable returned by drop() also " + + "unsubscribes from the source Observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const results = []; + + source.drop(0).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, 3, "complete"], + "Source Observable is mirrored"); +}, "drop(): A drop amount of 0 simply mirrors the source Observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const results = []; + + // Passing `-1` here is subject to the Web IDL integer conversion semantics, + // which converts the drop amount to the maximum of `18446744073709551615`. + source.drop(-1).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["complete"], "Source Observable is mirrored"); +}, "drop(): Passing negative value wraps to maximum value "); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js new file mode 100644 index 0000000000..8a49bcf467 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-filter.any.js @@ -0,0 +1,105 @@ +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.next(4); + subscriber.complete(); + }); + + const results = []; + + source + .filter(value => value % 2 === 0) + .subscribe({ + next: v => results.push(v), + error: () => results.push("error"), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [2, 4, "complete"]); +}, "filter(): Returned Observable filters out results based on predicate"); + +test(() => { + const error = new Error("error while filtering"); + const results = []; + let teardownCalled = false; + + const source = new Observable(subscriber => { + subscriber.addTeardown(() => teardownCalled = true); + subscriber.next(1); + assert_true(teardownCalled, "Teardown called once map unsubscribes due to error"); + assert_false(subscriber.active, "Unsubscription makes Subscriber inactive"); + subscriber.next(2); + subscriber.complete(); + }); + + source + .filter(() => { + throw error; + }) + .subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [error]); +}, "filter(): Errors thrown in filter predicate are emitted to Observer error() handler"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.complete(); + subscriber.next(2); + }); + + let predicateCalls = 0; + const results = []; + source.filter(v => ++predicateCalls).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push('complete'), + }); + + assert_equals(predicateCalls, 1, "Predicate is not called after complete()"); + assert_array_equals(results, [1, "complete"]); +}, "filter(): Passes complete() through from source Observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.error('error'); + subscriber.next(2); + }); + + let predicateCalls = 0; + const results = []; + source.map(v => ++predicateCalls).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push('complete'), + }); + + assert_equals(predicateCalls, 1, "Predicate is not called after error()"); + assert_array_equals(results, [1, "error"]); +}, "filter(): Passes error() through from source Observable"); + +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('source teardown')); + subscriber.signal.addEventListener('abort', + () => results.push('source abort event')); + + subscriber.complete(); + }); + + source.filter(() => results.push('filter predicate called')).subscribe({ + complete: () => results.push('filter observable complete'), + }); + + assert_array_equals(results, + ['source teardown', 'source abort event', 'filter observable complete']); +}, "filter(): Upon source completion, source Observable teardown sequence " + + "happens after downstream filter complete() is called"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-map.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-map.any.js new file mode 100644 index 0000000000..275505fb5d --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-map.any.js @@ -0,0 +1,166 @@ +test(() => { + const results = []; + const indices = []; + const source = new Observable((subscriber) => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const mapped = source.map((value, i) => { + indices.push(i); + return value * 2; + }); + + assert_true(mapped instanceof Observable, "map() returns an Observable"); + + assert_array_equals(results, [], "Does not map until subscribed (values)"); + assert_array_equals(indices, [], "Does not map until subscribed (indices)"); + + mapped.subscribe({ + next: (value) => results.push(value), + error: () => results.push('error'), + complete: () => results.push('complete'), + }); + + assert_array_equals(results, [2, 4, 6, 'complete']); + assert_array_equals(indices, [0, 1, 2]); +}, "map(): Maps values correctly"); + +test(() => { + const error = new Error("error"); + const results = []; + let teardownCalled = false; + + const source = new Observable((subscriber) => { + subscriber.addTeardown(() => teardownCalled = true); + + subscriber.next(1); + assert_false(teardownCalled, + "Teardown not called until until map unsubscribes due to error"); + subscriber.next(2); + assert_true(teardownCalled, "Teardown called once map unsubscribes due to error"); + assert_false(subscriber.active, "Unsubscription makes Subscriber inactive"); + subscriber.next(3); + subscriber.complete(); + }); + + const mapped = source.map((value) => { + if (value === 2) { + throw error; + } + return value * 2; + }); + + mapped.subscribe({ + next: (value) => results.push(value), + error: (error) => results.push(error), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [2, error], + "Mapper errors are emitted to Observer error() handler"); +}, "map(): Mapper errors are emitted to Observer error() handler"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.complete(); + subscriber.next(2); + }); + + let mapperCalls = 0; + const results = []; + source.map(v => { + mapperCalls++; + return v * 2; + }).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push('complete'), + }); + + assert_equals(mapperCalls, 1, "Mapper is not called after complete()"); + assert_array_equals(results, [2, "complete"]); +}, "map(): Passes complete() through from source Observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.error('error'); + subscriber.next(2); + }); + + let mapperCalls = 0; + const results = []; + source.map(v => { + mapperCalls++; + return v * 2; + }).subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push('complete'), + }); + + assert_equals(mapperCalls, 1, "Mapper is not called after error()"); + assert_array_equals(results, [2, "error"]); +}, "map(): Passes error() through from source Observable"); + +// This is mostly ensuring that the ordering in +// https://wicg.github.io/observable/#dom-subscriber-complete is consistent. +// +// That is, the `Subscriber#complete()` method *first* closes itself and signals +// abort on its own `Subscriber#signal()` and *then* calls whatever supplied +// completion algorithm exists. In the case of `map()`, the "supplied completion +// algorithm" is simply a set of internal observer steps that call +// `Subscriber#complete()` on the *outer* mapper's Observer. This means the +// outer Observer is notified of completion *after* the source Subscriber's +// signal is aborted / torn down. +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('source teardown')); + subscriber.signal.addEventListener('abort', + () => results.push('source abort event')); + + subscriber.complete(); + }); + + source.map(() => results.push('mapper called')).subscribe({ + complete: () => results.push('map observable complete'), + }); + + assert_array_equals(results, + ['source teardown', 'source abort event', 'map observable complete']); +}, "map(): Upon source completion, source Observable teardown sequence " + + "happens before downstream mapper complete() is called"); + +test(() => { + const results = []; + let sourceSubscriber = null; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('source teardown')); + sourceSubscriber = subscriber; + + subscriber.next(1); + }); + + const controller = new AbortController(); + source.map(v => v * 2).subscribe({ + next: v => { + results.push(v); + + // Triggers unsubscription to `source`. + controller.abort(); + + // Does nothing, since `source` is already torn down. + sourceSubscriber.next(100); + }, + complete: () => results.push('mapper complete'), + error: e => results.push('mapper error'), + }, {signal: controller.signal}); + + assert_array_equals(results, [2, 'source teardown']); +}, "map(): Map observable unsubscription causes source Observable " + + "unsubscription. Mapper Observer's complete()/error() are not called"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-map.window.js b/testing/web-platform/tests/dom/observable/tentative/observable-map.window.js new file mode 100644 index 0000000000..06bf2e26b5 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-map.window.js @@ -0,0 +1,40 @@ +async function loadIframeAndReturnContentWindow() { + // Create and attach an iframe. + const iframe = document.createElement('iframe'); + const iframeLoadPromise = new Promise((resolve, reject) => { + iframe.onload = resolve; + iframe.onerror = reject; + }); + document.body.append(iframe); + await iframeLoadPromise; + return iframe.contentWindow; +} + +promise_test(async t => { + const contentWin = await loadIframeAndReturnContentWindow(); + + window.results = []; + + contentWin.eval(` + const parentResults = parent.results; + + const source = new Observable(subscriber => { + // Detach the document before calling next(). + window.frameElement.remove(); + + // This invokes the map() operator's internal observer's next steps, + // which at least in Chromium, must have a special "context is detached" + // check to early-return, so as to not crash before invoking the "mapper" + // callback supplied to the map() operator. + subscriber.next(1); + }); + + source.map(value => { + parentResults.push(value); + }).subscribe(v => parentResults.push(v)); + `); + + // If we got here, we didn't crash! Let's also check that `results` is empty. + assert_array_equals(results, []); +}, "map()'s internal observer's next steps do not crash in a detached document"); + diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js new file mode 100644 index 0000000000..8350d0214c --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js @@ -0,0 +1,108 @@ +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push("source teardown")); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const result = source.take(2); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, "source teardown", "complete"]); +}, "take(): Takes the first N values from the source observable, then completes"); + +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push("source teardown")); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const result = source.take(5); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, 3, "source teardown", "complete"], + "complete() is immediately forwarded"); +}, "take(): Forwards complete()s that happen before the take count is met, " + + "and unsubscribes from source Observable"); + +test(() => { + const results = []; + const error = new Error('source error'); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.error(error); + }); + + const result = source.take(100); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, error], "Errors are forwarded"); +}, "take(): Should forward errors from the source observable"); + +test(() => { + const results = []; + const source = new Observable((subscriber) => { + results.push("source subscribe"); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const result = source.take(0); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["complete"]); +}, "take(): take(0) should not subscribe to the source observable, and " + + "should return an observable that immediately completes"); + +test(() => { + const results = []; + const source = new Observable((subscriber) => { + results.push("source subscribe"); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + // Per WebIDL, `-1` passed into an `unsigned long long` gets wrapped around + // into the maximum value (18446744073709551615), which means the `result` + // Observable captures everything that `source` does. + const result = source.take(-1); + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["source subscribe", 1, 2, 3, "complete"]); +}, "take(): Negative count is treated as maximum value"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js index 6421777e09..2895dd31e3 100644 --- a/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js +++ b/testing/web-platform/tests/dom/observable/tentative/observable-takeUntil.any.js @@ -32,74 +32,102 @@ promise_test(async () => { // `takeUntil()` operator, the spec responds to `notifier`'s `next()` by // unsubscribing from `notifier`, which is what this test asserts. promise_test(async () => { - const source = new Observable(subscriber => {}); + const results = []; + const source = new Observable(subscriber => { + results.push('source subscribe callback'); + subscriber.addTeardown(() => results.push('source teardown')); + }); - let notifierSubscriberActiveBeforeNext; - let notifierSubscriberActiveAfterNext; - let teardownCalledAfterNext; - let notifierSignalAbortedAfterNext; const notifier = new Observable(subscriber => { - let teardownCalled; - subscriber.addTeardown(() => teardownCalled = true); + subscriber.addTeardown(() => results.push('notifier teardown')); + results.push('notifier subscribe callback'); // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`. - notifierSubscriberActiveBeforeNext = subscriber.active; + results.push(`notifer active before next(): ${subscriber.active}`); subscriber.next('value'); - notifierSubscriberActiveAfterNext = subscriber.active; - teardownCalledAfterNext = (teardownCalled === true); - notifierSignalAbortedAfterNext = subscriber.signal.aborted; + results.push(`notifer active after next(): ${subscriber.active}`); }); - let nextOrErrorCalled = false; - let completeCalled = false; source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, - complete: () => completeCalled = true, + next: () => results.push('takeUntil() next callback'), + error: e => results.push(`takeUntil() error callback: ${error}`), + complete: () => results.push('takeUntil() complete callback'), }); - assert_true(notifierSubscriberActiveBeforeNext); - assert_false(notifierSubscriberActiveAfterNext); - assert_true(teardownCalledAfterNext); - assert_true(notifierSignalAbortedAfterNext); - assert_false(nextOrErrorCalled); - assert_true(completeCalled); -}, "takeUntil: notifier next() unsubscribes to notifier"); + assert_array_equals(results, [ + 'notifier subscribe callback', + 'notifer active before next(): true', + 'notifier teardown', + 'takeUntil() complete callback', + 'notifer active after next(): false', + ]); +}, "takeUntil: notifier next() unsubscribes from notifier"); // This test is identical to the one above, with the exception being that the // `notifier` calls `subscriber.error()` instead `subscriber.next()`. promise_test(async () => { - const source = new Observable(subscriber => {}); + const results = []; + const source = new Observable(subscriber => { + results.push('source subscribe callback'); + subscriber.addTeardown(() => results.push('source teardown')); + }); - let notifierSubscriberActiveBeforeNext; - let notifierSubscriberActiveAfterNext; - let teardownCalledAfterNext; - let notifierSignalAbortedAfterNext; const notifier = new Observable(subscriber => { - let teardownCalled; - subscriber.addTeardown(() => teardownCalled = true); + subscriber.addTeardown(() => results.push('notifier teardown')); + results.push('notifier subscribe callback'); // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`. - notifierSubscriberActiveBeforeNext = subscriber.active; + results.push(`notifer active before error(): ${subscriber.active}`); subscriber.error('error'); - notifierSubscriberActiveAfterNext = subscriber.active; - teardownCalledAfterNext = (teardownCalled === true); - notifierSignalAbortedAfterNext = subscriber.signal.aborted; + results.push(`notifer active after error(): ${subscriber.active}`); }); - let nextOrErrorCalled = false; - let completeCalled = false; source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, - complete: () => completeCalled = true, + next: () => results.push('takeUntil() next callback'), + error: e => results.push(`takeUntil() error callback: ${error}`), + complete: () => results.push('takeUntil() complete callback'), }); - assert_true(notifierSubscriberActiveBeforeNext); - assert_false(notifierSubscriberActiveAfterNext); - assert_true(teardownCalledAfterNext); - assert_true(notifierSignalAbortedAfterNext); - assert_false(nextOrErrorCalled); - assert_true(completeCalled); -}, "takeUntil: notifier error() unsubscribes to notifier"); + + assert_array_equals(results, [ + 'notifier subscribe callback', + 'notifer active before error(): true', + 'notifier teardown', + 'takeUntil() complete callback', + 'notifer active after error(): false', + ]); +}, "takeUntil: notifier error() unsubscribes from notifier"); +// This test is identical to the above except it `throw`s instead of calling +// `Subscriber#error()`. +promise_test(async () => { + const results = []; + const source = new Observable(subscriber => { + results.push('source subscribe callback'); + subscriber.addTeardown(() => results.push('source teardown')); + }); + + const notifier = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('notifier teardown')); + + results.push('notifier subscribe callback'); + // Calling `next()` causes `takeUntil()` to unsubscribe from `notifier`. + results.push(`notifer active before throw: ${subscriber.active}`); + throw new Error('custom error'); + // Won't run: + results.push(`notifer active after throw: ${subscriber.active}`); + }); + + source.takeUntil(notifier).subscribe({ + next: () => results.push('takeUntil() next callback'), + error: e => results.push(`takeUntil() error callback: ${error}`), + complete: () => results.push('takeUntil() complete callback'), + }); + + assert_array_equals(results, [ + 'notifier subscribe callback', + 'notifer active before throw: true', + 'notifier teardown', + 'takeUntil() complete callback', + ]); +}, "takeUntil: notifier throw Error unsubscribes from notifier"); // Test that `notifier` unsubscribes from source Observable. promise_test(async t => { @@ -130,9 +158,10 @@ promise_test(async t => { let notifierTeardownCalledBeforeCompleteCallback; await new Promise(resolve => { source.takeUntil(notifier).subscribe({ - next: () => nextOrErrorCalled = true, - error: () => nextOrErrorCalled = true, + next: () => {nextOrErrorCalled = true; results.push('next callback');}, + error: () => {nextOrErrorCalled = true; results.push('error callback');}, complete: () => { + results.push('complete callback'); notifierTeardownCalledBeforeCompleteCallback = notifierTeardownCalled; resolve(); }, @@ -145,7 +174,7 @@ promise_test(async t => { // The notifier/source teardowns are not called by the time the outer // `Observer#complete()` callback is invoked, but they are all run *after* // (i.e., before `notifier`'s `subscriber.next()` returns internally). - assert_false(notifierTeardownCalledBeforeCompleteCallback); + assert_true(notifierTeardownCalledBeforeCompleteCallback); assert_true(notifierTeardownCalled); assert_array_equals(results, [ "notifier subscribed", @@ -153,7 +182,8 @@ promise_test(async t => { "notifier teardown", "notifier signal abort", "source teardown", - "source signal abort" + "source signal abort", + "complete callback", ]); }, "takeUntil: notifier next() unsubscribes from notifier & source observable"); -- cgit v1.2.3