From 766704fb30b98fc9fa09cae495420f9346bfcfbe Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Fri, 30 Aug 2024 22:59:02 +0000 Subject: [PATCH] Bug 1915752 [wpt PR 47877] - DOM: Implement the `Observable#catch()` operator, a=testonly Automatic update from web-platform-tests DOM: Implement the `Observable#catch()` operator See https://github.com/WICG/observable/pull/172. R=masonf@chromium.org Bug: 40282760 Change-Id: I6b117b34ddcbed0a7ac18d4c66121350b0e864c2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5823731 Commit-Queue: Dominic Farolino Reviewed-by: Mason Freed Cr-Commit-Position: refs/heads/main@{#1348932} -- wpt-commits: 7a69b2e3e487c988cf3379ede29b945aecbe3162 wpt-pr: 47877 --- .../tentative/observable-catch.any.js | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-catch.any.js diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-catch.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-catch.any.js new file mode 100644 index 000000000000..a1555225b46a --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-catch.any.js @@ -0,0 +1,255 @@ +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const caughtObservable = source.catch(() => { + assert_unreached("catch() is not called"); + }); + + const results = []; + + caughtObservable.subscribe({ + next: value => results.push(value), + complete: () => results.push('complete') + }); + + assert_array_equals(results, [1, 2, 3, 'complete']); +}, "catch(): Returns an Observable that is a pass-through for next()/complete()"); + +test(() => { + let sourceError = new Error("from the source"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.error(sourceError); + }); + + const caughtObservable = source.catch(error => { + assert_equals(error, sourceError); + return new Observable(subscriber => { + subscriber.next(3); + subscriber.complete(); + }); + }); + + const results = []; + + caughtObservable.subscribe({ + next: value => results.push(value), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, 3, 'complete']); +}, "catch(): Handle errors from source and flatten to a new Observable"); + +test(() => { + const sourceError = new Error("from the source"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.error(sourceError); + }); + + const catchCallbackError = new Error("from the catch callback"); + const caughtObservable = source.catch(error => { + assert_equals(error, sourceError); + throw catchCallbackError; + }); + + const results = []; + + caughtObservable.subscribe({ + next: value => results.push(value), + error: error => { + results.push(error); + }, + complete: () => results.push('complete'), + }); + + assert_array_equals(results, [1, 2, catchCallbackError]); +}, "catch(): Errors thrown in the catch() callback are sent to the consumer's error handler"); + +test(() => { + // A common use case is logging and keeping the stream alive. + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const flatteningError = new Error("from the flattening operation"); + function errorsOnTwo(value) { + return new Observable(subscriber => { + if (value === 2) { + subscriber.error(flatteningError); + } else { + subscriber.next(value); + subscriber.complete(); + } + }); + } + + const results = []; + + source.flatMap(value => errorsOnTwo(value) + .catch(error => { + results.push(error); + // This empty array converts to an Observable which automatically + // completes. + return []; + }) + ).subscribe({ + next: value => results.push(value), + complete: () => results.push("complete") + }); + + assert_array_equals(results, [1, flatteningError, 3, "complete"]); +}, "catch(): CatchHandler can return an empty iterable"); + +promise_test(async () => { + const sourceError = new Error("from the source"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.error(sourceError); + }); + + const caughtObservable = source.catch(error => { + assert_equals(error, sourceError); + return Promise.resolve(error.message); + }); + + const results = await caughtObservable.toArray(); + + assert_array_equals(results, [1, 2, "from the source"]); +}, "catch(): CatchHandler can return a Promise"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.error(new Error('from the source')); + }); + + const caughtObservable = source.catch(async function* (error) { + assert_true(error instanceof Error); + assert_equals(error.message, 'from the source'); + yield 3; + }); + + const results = await caughtObservable.toArray(); + + assert_array_equals(results, [1, 2, 3], 'catch(): should handle returning an observable'); +}, 'catch(): should handle returning an async iterable'); + +test(() => { + const sourceError = new Error("from the source"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.error(sourceError); + }); + + const caughtObservable = source.catch(error => { + assert_equals(error, sourceError); + // Primitive values like this are not convertible to an Observable, via the + // `from()` semantics. + return 3; + }); + + const results = []; + + caughtObservable.subscribe({ + next: value => results.push(value), + error: error => { + assert_true(error instanceof TypeError); + results.push("TypeError"); + }, + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, "TypeError"]); +}, "catch(): CatchHandler emits an error if the value returned is not " + + "convertible to an Observable"); + +test(() => { + const source = new Observable(subscriber => { + susbcriber.error(new Error("from the source")); + }); + + const results = []; + + const innerSubscriptionError = new Error("CatchHandler subscription error"); + const catchObservable = source.catch(() => { + results.push('CatchHandler invoked'); + return new Observable(subscriber => { + throw innerSubscriptionError; + }); + }); + + catchObservable.subscribe({ + error: e => { + results.push(e); + } + }); + + assert_array_equals(results, ['CatchHandler invoked', innerSubscriptionError]); +}, "catch(): CatchHandler returns an Observable that throws immediately on " + + "subscription"); + +// This test asserts that the relationship between (a) the AbortSignal passed +// into `subscribe()` and (b) the AbortSignal associated with the Observable +// returned from `catch()`'s CatchHandler is not a "dependent" relationship. +// This is important because Observables have moved away from the "dependent +// abort signal" infrastructure in https://github.com/WICG/observable/pull/154, +// and this test asserts so. +// +// Here are all of the associated Observables and signals in this test: +// 1. Raw outer signal passed into `subscribe()` +// 2. catchObservable's inner Subscriber's signal +// a. Per the above PR, and Subscriber's initialization logic [1], this +// signal is set to abort in response to (1)'s abort algorithms. This +// means its "abort" event gets fired before (1)'s. +// 3. Inner CatchHandler-returned Observable's Subscriber's signal +// a. Also per [1], this is set to abort in response to (2)'s abort +// algorithms, since we subscribe to this "inner Observable" with (2)'s +// signal as the `SubscribeOptions#signal`. +// +// (1), (2), and (3) above all form an abort chain: +// (1) --> (2) --> (3) +// +// …such that when (1) aborts, its abort algorithms immediately abort (2), +// whose abort algorithms immediately abort (3). Finally on the way back up the +// chain, (3)'s `abort` event is fired, (2)'s `abort` event is fired, and then +// (1)'s `abort` event is fired. This ordering of abort events is what this test +// ensures. +// +// [1]: https://wicg.github.io/observable/#ref-for-abortsignal-add +test(() => { + const results = []; + const source = new Observable(subscriber => + susbcriber.error(new Error("from the source"))); + + const catchObservable = source.catch(() => { + return new Observable(subscriber => { + subscriber.addTeardown(() => results.push('inner teardown')); + subscriber.signal.addEventListener('abort', + e => results.push('inner signal abort')); + + // No values or completion. We'll just wait for the subscriber to abort + // its subscription. + }); + }); + + const ac = new AbortController(); + ac.signal.addEventListener('abort', e => results.push('outer signal abort')); + catchObservable.subscribe({}, {signal: ac.signal}); + ac.abort(); + + assert_array_equals(results, ['inner signal abort', 'inner teardown', 'outer signal abort']); +}, "catch(): Abort order between outer AbortSignal and inner CatchHandler subscriber's AbortSignal");