Skip to content

Commit

Permalink
Bug 1915752 [wpt PR 47877] - DOM: Implement the Observable#catch()
Browse files Browse the repository at this point in the history
…operator, a=testonly

Automatic update from web-platform-tests
DOM: Implement the `Observable#catch()` operator

See WICG/observable#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 <dom@chromium.org>
Reviewed-by: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1348932}

--

wpt-commits: 7a69b2e3e487c988cf3379ede29b945aecbe3162
wpt-pr: 47877
  • Loading branch information
domfarolino authored and moz-wptsync-bot committed Aug 31, 2024
1 parent 13f363e commit 766704f
Showing 1 changed file with 255 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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");

0 comments on commit 766704f

Please sign in to comment.