From d8bbc7858622b6d9c278469aab701ca0b609cddf Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:35:49 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../observable/tentative/observable-first.any.js | 114 +++++++ .../observable/tentative/observable-flatMap.any.js | 315 ++++++++++++++++++ .../observable/tentative/observable-from.any.js | 354 +++++++++++++++++++++ .../observable/tentative/observable-last.any.js | 113 +++++++ .../tentative/observable-switchMap.any.js | 252 +++++++++++++++ 5 files changed, 1148 insertions(+) create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-first.any.js create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-from.any.js create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-last.any.js create mode 100644 testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js (limited to 'testing/web-platform/tests/dom/observable') diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js new file mode 100644 index 0000000000..7c99066dc2 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js @@ -0,0 +1,114 @@ +promise_test(async () => { + const results = []; + + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('teardown')); + subscriber.next(1); + results.push(subscriber.active ? 'active' : 'inactive'); + results.push(subscriber.signal.aborted ? 'aborted' : 'not aborted') + + // Ignored. + subscriber.next(2); + subscriber.complete(); + }); + + const value = await source.first(); + + assert_array_equals(results, ['teardown', 'inactive', 'aborted']); + assert_equals(value, 1, + "Promise resolves with the first value from the source Observable"); +}, "first(): Promise resolves with the first value from the source Observable"); + +promise_test(async () => { + const error = new Error("error from source"); + const source = new Observable(subscriber => { + subscriber.error(error); + }); + + let rejection; + try { + await source.first(); + } catch (e) { + rejection = e; + } + + assert_equals(rejection, error, "Promise rejects with source Observable error"); +}, "first(): Promise rejects with the error emitted from the source Observable"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.complete(); + }); + + let rejection; + try { + await source.first(); + } catch (e) { + rejection = e; + } + + assert_true(rejection instanceof RangeError, + "Upon complete(), first() Promise rejects with RangeError"); + assert_equals(rejection.message, "No values in Observable"); +}, "first(): Promise rejects with RangeError when source Observable " + + "completes without emitting any values"); + +promise_test(async () => { + const source = new Observable(subscriber => {}); + + const controller = new AbortController(); + const promise = source.first({ signal: controller.signal }); + + controller.abort(); + + let rejection; + try { + await promise; + } catch (e) { + rejection = e; + } + + assert_true(rejection instanceof DOMException, + "Promise rejects with a DOMException for abortion"); + assert_equals(rejection.name, "AbortError", + "Rejected with 'AbortError' DOMException"); + assert_equals(rejection.message, "signal is aborted without reason"); +}, "first(): Aborting a signal rejects the Promise with an AbortError DOMException"); + +promise_test(async () => { + const results = []; + + const source = new Observable(subscriber => { + results.push("source subscribe"); + subscriber.addTeardown(() => results.push("source teardown")); + subscriber.signal.addEventListener("abort", () => results.push("source abort")); + results.push("before source next 1"); + subscriber.next(1); + results.push("after source next 1"); + }); + + results.push("calling first"); + const promise = source.first(); + + assert_array_equals(results, [ + "calling first", + "source subscribe", + "before source next 1", + "source teardown", + "source abort", + "after source next 1" + ], "Array values after first() is called"); + + const firstValue = await promise; + results.push(`first resolved with: ${firstValue}`); + + assert_array_equals(results, [ + "calling first", + "source subscribe", + "before source next 1", + "source teardown", + "source abort", + "after source next 1", + "first resolved with: 1", + ], "Array values after Promise is awaited"); +}, "first(): Lifecycle"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js new file mode 100644 index 0000000000..7cbfa6cb60 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js @@ -0,0 +1,315 @@ +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + let projectionCalls = 0; + + const results = []; + + const flattened = source.flatMap(value => { + projectionCalls++; + return new Observable((subscriber) => { + subscriber.next(value * 10); + subscriber.next(value * 100); + subscriber.complete(); + }); + }); + + assert_true(flattened instanceof Observable, "flatMap() returns an Observable"); + assert_equals(projectionCalls, 0, + "Projection is not called until subscription starts"); + + flattened.subscribe({ + next: v => results.push(v), + error: () => results.push("error"), + complete: () => results.push("complete"), + }); + + assert_equals(projectionCalls, 3, + "Mapper is called three times, once for each source Observable value"); + assert_array_equals(results, [10, 100, 20, 200, 30, 300, "complete"], + "flatMap() results are correct"); +}, "flatMap(): Flattens simple source Observable properly"); + +test(() => { + const error = new Error("error"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.error(error); + subscriber.next(3); + }); + + const flattened = source.flatMap(value => { + return new Observable(subscriber => { + subscriber.next(value * 10); + subscriber.next(value * 100); + subscriber.complete(); + }); + }); + + const results = []; + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [10, 100, 20, 200, error], + "Source error is passed through to the flatMap() Observable"); +}, "flatMap(): Returned Observable passes through source Observable errors"); + +test(() => { + const results = []; + const error = new Error("error"); + const source = new Observable(subscriber => { + subscriber.next(1); + results.push(subscriber.active ? "active" : "inactive"); + subscriber.next(2); + results.push(subscriber.active ? "active" : "inactive"); + subscriber.next(3); + subscriber.complete(); + }); + + const flattened = source.flatMap((value) => { + return new Observable((subscriber) => { + subscriber.next(value * 10); + subscriber.next(value * 100); + if (value === 2) { + subscriber.error(error); + } else { + subscriber.complete(); + } + }); + }); + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [10, 100, "active", 20, 200, error, "inactive"], + "Inner subscription error gets surfaced"); +}, "flatMap(): Outer Subscription synchronously becomes inactive when an " + + "'inner' Observable emits an error"); + +test(() => { + const results = []; + const error = new Error("error"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + results.push(subscriber.active ? "active" : "inactive"); + subscriber.complete(); + }); + + const flattened = source.flatMap(value => { + if (value === 3) { + throw error; + } + return new Observable(subscriber => { + subscriber.next(value * 10); + subscriber.next(value * 100); + subscriber.complete(); + }); + }); + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [10, 100, 20, 200, error, "inactive"], + "Inner subscriber thrown error gets surfaced"); +}, "flatMap(): Outer Subscription synchronously becomes inactive when an " + + "'inner' Observable throws an error"); + +test(() => { + const source = createTestSubject(); + const inner1 = createTestSubject(); + const inner2 = createTestSubject(); + + const flattened = source.flatMap(value => { + if (value === 1) { + return inner1; + } + + return inner2; + }); + + const results = []; + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, []); + + source.next(1); + assert_equals(inner1.subscriberCount(), 1, "inner1 gets subscribed to"); + + source.next(2); + assert_equals(inner2.subscriberCount(), 0, + "inner2 is queued, not subscribed to until inner1 completes"); + + assert_array_equals(results, []); + + inner1.next(100); + inner1.next(101); + + assert_array_equals(results, [100, 101]); + + inner1.complete(); + assert_equals(inner1.subscriberCount(), 0, + "inner1 becomes inactive once it completes"); + assert_equals(inner2.subscriberCount(), 1, + "inner2 gets un-queued and subscribed to once inner1 completes"); + + inner2.next(200); + inner2.next(201); + assert_array_equals(results, [100, 101, 200, 201]); + + inner2.complete(); + assert_equals(inner2.subscriberCount(), 0, + "inner2 becomes inactive once it completes"); + assert_equals(source.subscriberCount(), 1, + "source is not unsubscribed from yet, since it has not completed"); + assert_array_equals(results, [100, 101, 200, 201]); + + source.complete(); + assert_equals(source.subscriberCount(), 0, + "source unsubscribed from after it completes"); + + assert_array_equals(results, [100, 101, 200, 201, "complete"]); +}, "flatMap(): result Observable does not complete until source and inner " + + "Observables all complete"); + +test(() => { + const source = createTestSubject(); + const inner1 = createTestSubject(); + const inner2 = createTestSubject(); + + const flattened = source.flatMap(value => { + if (value === 1) { + return inner1; + } + + return inner2; + }); + + const results = []; + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, []); + + source.next(1); + source.next(2); + assert_equals(inner1.subscriberCount(), 1, "inner1 gets subscribed to"); + assert_equals(inner2.subscriberCount(), 0, + "inner2 is queued, not subscribed to until inner1 completes"); + + assert_array_equals(results, []); + + // Before `inner1` pushes any values, we first complete the source Observable. + // This will not fire completion of the Observable returned from `flatMap()`, + // because there are two values (corresponding to inner Observables) that are + // queued to the inner queue that need to be processed first. Once the last + // one of *those* completes (i.e., `inner2.complete()` further down), then the + // returned Observable can finally complete. + source.complete(); + assert_equals(source.subscriberCount(), 0, + "source becomes inactive once it completes"); + + inner1.next(100); + inner1.next(101); + + assert_array_equals(results, [100, 101]); + + inner1.complete(); + assert_array_equals(results, [100, 101], + "Outer completion not triggered after inner1 completes"); + assert_equals(inner2.subscriberCount(), 1, + "inner2 gets un-queued and subscribed after inner1 completes"); + + inner2.next(200); + inner2.next(201); + assert_array_equals(results, [100, 101, 200, 201]); + + inner2.complete(); + assert_equals(inner2.subscriberCount(), 0, + "inner2 becomes inactive once it completes"); + assert_array_equals(results, [100, 101, 200, 201, "complete"]); +}, "flatMap(): result Observable does not complete after source Observable " + + "completes while there are still queued inner Observables to process " + + "Observables all complete"); + +test(() => { + const source = createTestSubject(); + const inner = createTestSubject(); + const result = source.flatMap(() => inner); + + const ac = new AbortController(); + + result.subscribe({}, { signal: ac.signal, }); + + source.next(1); + + assert_equals(inner.subscriberCount(), 1, + "inner Observable subscribed to once source emits it"); + + ac.abort(); + + assert_equals(source.subscriberCount(), 0, + "source unsubscribed from, once outer signal is aborted"); + + assert_equals(inner.subscriberCount(), 0, + "inner Observable unsubscribed from once the outer Observable is " + + "subscribed from, as a result of the outer signal being aborted"); +}, "flatMap(): source and inner active Observables are both unsubscribed " + + "from once the outer subscription signal is aborted"); + +// A helper function to create an Observable that can be externally controlled +// and examined for testing purposes. +function createTestSubject() { + const subscribers = new Set(); + const subject = new Observable(subscriber => { + subscribers.add(subscriber); + subscriber.addTeardown(() => subscribers.delete(subscriber)); + }); + + subject.next = value => { + for (const subscriber of Array.from(subscribers)) { + subscriber.next(value); + } + }; + subject.error = error => { + for (const subscriber of Array.from(subscribers)) { + subscriber.error(error); + } + }; + subject.complete = () => { + for (const subscriber of Array.from(subscribers)) { + subscriber.complete(); + } + }; + subject.subscriberCount = () => { + return subscribers.size; + }; + + return subject; +} diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js new file mode 100644 index 0000000000..80408ddced --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js @@ -0,0 +1,354 @@ +// Because we test that the global error handler is called at various times. +setup({allow_uncaught_exception: true}); + +test(() => { + assert_equals(typeof Observable.from, "function", + "Observable.from() is a function"); +}, "from(): Observable.from() is a function"); + +test(() => { + assert_throws_js(TypeError, () => Observable.from(10), + "Number cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from(true), + "Boolean cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from("String"), + "String cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from({a: 10}), + "Object cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from(Symbol.iterator), + "Bare Symbol.iterator cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from(Promise), + "Promise constructor cannot convert to an Observable"); +}, "from(): Failed conversions"); + +test(() => { + const target = new EventTarget(); + const observable = target.on('custom'); + const from_observable = Observable.from(observable); + assert_equals(observable, from_observable); +}, "from(): Given an observable, it returns that exact observable"); + +test(() => { + let completeCalled = false; + const results = []; + const array = [1, 2, 3, 'a', new Date(), 15, [12]]; + const observable = Observable.from(array); + observable.subscribe({ + next: v => results.push(v), + error: e => assert_unreached('error is not called'), + complete: () => completeCalled = true + }); + + assert_array_equals(results, array); + assert_true(completeCalled); +}, "from(): Given an array"); + +test(() => { + const iterable = { + [Symbol.iterator]() { + let n = 0; + return { + next() { + n++; + if (n <= 3) { + return { value: n, done: false }; + } + return { value: undefined, done: true }; + }, + }; + }, + }; + + const observable = Observable.from(iterable); + + assert_true(observable instanceof Observable, "Observable.from() returns an Observable"); + + const results = []; + + observable.subscribe({ + next: (value) => results.push(value), + error: () => assert_unreached("should not error"), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, 3, "complete"], + "Subscription pushes iterable values out to Observable"); + + // A second subscription should restart iteration. + observable.subscribe({ + next: (value) => results.push(value), + error: () => assert_unreached("should not error"), + complete: () => results.push("complete2"), + }); + + assert_array_equals(results, [1, 2, 3, "complete", 1, 2, 3, "complete2"], + "Subscribing again causes another fresh iteration on an un-exhausted iterable"); +}, "from(): Iterable converts to Observable"); + +// The result of the @@iterator method of the converted object is called: +// 1. Once on conversion (to test that the value is an iterable). +// 2. Once on subscription, to re-pull the iterator implementation from the +// raw JS object that the Observable owns once synchronous iteration is +// about to begin. +test(() => { + let numTimesSymbolIteratorCalled = 0; + let numTimesNextCalled = 0; + + const iterable = { + [Symbol.iterator]() { + numTimesSymbolIteratorCalled++; + return { + next() { + numTimesNextCalled++; + return {value: undefined, done: true}; + } + }; + } + }; + + const observable = Observable.from(iterable); + + assert_equals(numTimesSymbolIteratorCalled, 1, + "Observable.from(iterable) invokes the @@iterator method getter once"); + assert_equals(numTimesNextCalled, 0, + "Iterator next() is not called until subscription"); + + // Override iterable's `[Symbol.iterator]` protocol with an error-throwing + // function. We assert that on subscription, this method (the new `@@iterator` + // implementation), is called because only the raw JS object gets stored in + // the Observable that results in conversion. This raw value must get + // re-converted to an iterable once iteration is about to start. + const customError = new Error('@@iterator override error'); + iterable[Symbol.iterator] = () => { + throw customError; + }; + + let thrownError = null; + observable.subscribe({ + error: e => thrownError = e, + }); + + assert_equals(thrownError, customError, + "Error thrown from next() is passed to the error() handler"); + + assert_equals(numTimesSymbolIteratorCalled, 1, + "Subscription re-invokes @@iterator method, which now is a different " + + "method that does *not* increment our assertion value"); + assert_equals(numTimesNextCalled, 0, "Iterator next() is never called"); +}, "from(): [Symbol.iterator] side-effects (one observable)"); + +// Similar to the above test, but with more Observables! +test(() => { + let numTimesSymbolIteratorCalled = 0; + let numTimesNextCalled = 0; + + const iterable = { + [Symbol.iterator]() { + numTimesSymbolIteratorCalled++; + return { + next() { + numTimesNextCalled++; + return {value: undefined, done: true}; + } + }; + } + }; + + const obs1 = Observable.from(iterable); + const obs2 = Observable.from(iterable); + const obs3 = Observable.from(iterable); + const obs4 = Observable.from(obs3); + + assert_equals(numTimesSymbolIteratorCalled, 3, "Observable.from(iterable) invokes the iterator method getter once"); + assert_equals(numTimesNextCalled, 0, "Iterator next() is not called until subscription"); + + iterable[Symbol.iterator] = () => { + throw new Error('Symbol.iterator override error'); + }; + + let errorCount = 0; + + const observer = {error: e => errorCount++}; + obs1.subscribe(observer); + obs2.subscribe(observer); + obs3.subscribe(observer); + obs4.subscribe(observer); + assert_equals(errorCount, 4, + "Error-throwing `@@iterator` implementation is called once per " + + "subscription"); + + assert_equals(numTimesSymbolIteratorCalled, 3, + "Subscription re-invokes the iterator method getter once"); + assert_equals(numTimesNextCalled, 0, "Iterator next() is never called"); +}, "from(): [Symbol.iterator] side-effects (many observables)"); + +test(() => { + const customError = new Error('@@iterator next() error'); + const iterable = { + [Symbol.iterator]() { + return { + next() { + throw customError; + } + }; + } + }; + + let thrownError = null; + Observable.from(iterable).subscribe({ + error: e => thrownError = e, + }); + + assert_equals(thrownError, customError, + "Error thrown from next() is passed to the error() handler"); +}, "from(): [Symbol.iterator] next() throws error"); + +promise_test(async () => { + const promise = Promise.resolve('value'); + const observable = Observable.from(promise); + + assert_true(observable instanceof Observable, "Converts to Observable"); + + const results = []; + + observable.subscribe({ + next: (value) => results.push(value), + error: () => assert_unreached("error() is not called"), + complete: () => results.push("complete()"), + }); + + assert_array_equals(results, [], "Observable does not emit synchronously"); + + await promise; + + assert_array_equals(results, ["value", "complete()"], "Observable emits and completes after Promise resolves"); +}, "from(): Converts Promise to Observable"); + +promise_test(async t => { + let unhandledRejectionHandlerCalled = false; + const unhandledRejectionHandler = () => { + unhandledRejectionHandlerCalled = true; + }; + + self.addEventListener("unhandledrejection", unhandledRejectionHandler); + t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler)); + + const promise = Promise.reject("reason"); + const observable = Observable.from(promise); + + assert_true(observable instanceof Observable, "Converts to Observable"); + + const results = []; + + observable.subscribe({ + next: (value) => assert_unreached("next() not called"), + error: (error) => results.push(error), + complete: () => assert_unreached("complete() not called"), + }); + + assert_array_equals(results, [], "Observable does not emit synchronously"); + + let catchBlockEntered = false; + try { + await promise; + } catch { + catchBlockEntered = true; + } + + assert_true(catchBlockEntered, "Catch block entered"); + assert_false(unhandledRejectionHandlerCalled, "No unhandledrejection event"); + assert_array_equals(results, ["reason"], + "Observable emits error() after Promise rejects"); +}, "from(): Converts rejected Promise to Observable. No " + + "`unhandledrejection` event when error is handled by subscription"); + +promise_test(async t => { + let unhandledRejectionHandlerCalled = false; + const unhandledRejectionHandler = () => { + unhandledRejectionHandlerCalled = true; + }; + + self.addEventListener("unhandledrejection", unhandledRejectionHandler); + t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler)); + + let errorReported = null; + self.addEventListener("error", e => errorReported = e, { once: true }); + + let catchBlockEntered = false; + try { + const promise = Promise.reject("custom reason"); + const observable = Observable.from(promise); + + observable.subscribe(); + await promise; + } catch { + catchBlockEntered = true; + } + + assert_true(catchBlockEntered, "Catch block entered"); + assert_false(unhandledRejectionHandlerCalled, + "No unhandledrejection event, because error got reported to global"); + assert_not_equals(errorReported, null, "Error was reported to the global"); + + assert_true(errorReported.message.includes("custom reason"), + "Error message matches"); + assert_equals(errorReported.lineno, 0, "Error lineno is 0"); + assert_equals(errorReported.colno, 0, "Error lineno is 0"); + assert_equals(errorReported.error, "custom reason", + "Error object is equivalent"); +}, "from(): Rejections not handled by subscription are reported to the " + + "global, and still not sent as an unhandledrejection event"); + +test(() => { + const results = []; + const observable = new Observable(subscriber => { + subscriber.next('from Observable'); + subscriber.complete(); + }); + + observable[Symbol.iterator] = () => { + results.push('Symbol.iterator() called'); + return { + next() { + return {value: 'from @@iterator', done: true}; + } + }; + }; + + Observable.from(observable).subscribe({ + next: v => results.push(v), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["from Observable", "complete"]); +}, "from(): Observable that implements @@iterator protocol gets converted " + + "as an Observable, not iterator"); + +test(() => { + const results = []; + const promise = new Promise(resolve => { + resolve('from Promise'); + }); + + promise[Symbol.iterator] = () => { + let done = false; + return { + next() { + if (!done) { + done = true; + return {value: 'from @@iterator', done: false}; + } else { + return {value: undefined, done: true}; + } + } + }; + }; + + Observable.from(promise).subscribe({ + next: v => results.push(v), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["from @@iterator", "complete"]); +}, "from(): Promise that implements @@iterator protocol gets converted as " + + "an iterable, not Promise"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js new file mode 100644 index 0000000000..cd39a3700a --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js @@ -0,0 +1,113 @@ +promise_test(async () => { + const source = new Observable(subscriber => { + // Never exposed to the `last()` promise. + subscriber.next(1); + + subscriber.next(2); + subscriber.complete(); + }); + + const value = await source.last(); + + assert_equals(value, 2); +}, "last(): Promise resolves to last value"); + +promise_test(async () => { + const error = new Error("error from source"); + const source = new Observable(subscriber => { + subscriber.error(error); + }); + + let rejection = null; + try { + await source.last(); + } catch (e) { + rejection = e; + } + + assert_equals(rejection, error); +}, "last(): Promise rejects with emitted error"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.complete(); + }); + + let rejection = null; + try { + await source.last(); + } catch (e) { + rejection = e; + } + + assert_true(rejection instanceof RangeError, + "Promise rejects with RangeError"); + assert_equals(rejection.message, "No values in Observable"); +}, "last(): Promise rejects with RangeError when source Observable " + + "completes without emitting any values"); + +promise_test(async () => { + const source = new Observable(subscriber => {}); + + const controller = new AbortController(); + const promise = source.last({ signal: controller.signal }); + + controller.abort(); + + let rejection = null; + try { + await promise; + } catch (e) { + rejection = e; + } + + assert_true(rejection instanceof DOMException, + "Promise rejects with a DOMException for abortion"); + assert_equals(rejection.name, "AbortError", + "Rejected with 'AbortError' DOMException"); + assert_equals(rejection.message, "signal is aborted without reason"); +}, "last(): Aborting a signal rejects the Promise with an AbortError DOMException"); + +promise_test(async () => { + const results = []; + const source = new Observable(subscriber => { + results.push("source subscribe"); + subscriber.addTeardown(() => results.push("source teardown")); + subscriber.signal.addEventListener("abort", () => results.push("source abort")); + results.push("before source next 1"); + subscriber.next(1); + results.push("after source next 1"); + results.push("before source complete"); + subscriber.complete(); + results.push("after source complete"); + }); + + results.push("calling last"); + const promise = source.last(); + + assert_array_equals(results, [ + "calling last", + "source subscribe", + "before source next 1", + "after source next 1", + "before source complete", + "source teardown", + "source abort", + "after source complete", + ], "Array values after last() is called"); + + const lastValue = await promise; + results.push(`last resolved with: ${lastValue}`); + + assert_array_equals(results, [ + "calling last", + "source subscribe", + "before source next 1", + "after source next 1", + "before source complete", + "source teardown", + "source abort", + "after source complete", + "last resolved with: 1", + ], "Array values after Promise is awaited"); +}, "last(): Lifecycle"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js new file mode 100644 index 0000000000..836a39a68e --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js @@ -0,0 +1,252 @@ +test(() => { + const source = createTestSubject(); + const inner1 = createTestSubject(); + const inner2 = createTestSubject(); + + const result = source.switchMap((value, index) => { + if (value === 1) { + return inner1; + } + if (value === 2) { + return inner2; + } + throw new Error("invalid "); + }); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_equals(source.subscriberCount(), 1, + "source observable is subscribed to"); + + source.next(1); + assert_equals(inner1.subscriberCount(), 1, + "inner1 observable is subscribed to"); + + inner1.next("1a"); + assert_array_equals(results, ["1a"]); + + inner1.next("1b"); + assert_array_equals(results, ["1a", "1b"]); + + source.next(2); + assert_equals(inner1.subscriberCount(), 0, + "inner1 observable is unsubscribed from"); + assert_equals(inner2.subscriberCount(), 1, + "inner2 observable is subscribed to"); + + inner2.next("2a"); + assert_array_equals(results, ["1a", "1b", "2a"]); + + inner2.next("2b"); + assert_array_equals(results, ["1a", "1b", "2a", "2b"]); + + inner2.complete(); + assert_array_equals(results, ["1a", "1b", "2a", "2b"]); + + source.complete(); + assert_array_equals(results, ["1a", "1b", "2a", "2b", "complete"]); +}, "switchMap(): result subscribes to one inner observable at a time, " + + "unsubscribing from the previous active one when a new one replaces it"); + +test(() => { + const source = createTestSubject(); + const inner = createTestSubject(); + + const result = source.switchMap(() => inner); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_equals(source.subscriberCount(), 1, + "source observable is subscribed to"); + assert_equals(inner.subscriberCount(), 0, + "inner observable is not subscribed to"); + + source.next(1); + assert_equals(inner.subscriberCount(), 1, + "inner observable is subscribed to"); + + inner.next("a"); + assert_array_equals(results, ["a"]); + + inner.next("b"); + assert_array_equals(results, ["a", "b"]); + + source.complete(); + assert_array_equals(results, ["a", "b"], + "Result observable does not complete when source observable completes, " + + "because inner is still active"); + + inner.next("c"); + assert_array_equals(results, ["a", "b", "c"]); + + inner.complete(); + assert_array_equals(results, ["a", "b", "c", "complete"], + "Result observable completes when inner observable completes, because " + + "source is already complete"); +}, "switchMap(): result does not complete when the source observable " + + "completes, if the inner observable is still active"); + +test(() => { + const source = createTestSubject(); + + const e = new Error('thrown from mapper'); + const result = source.switchMap(() => { + throw e; + }); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_equals(source.subscriberCount(), 1, + "source observable is subscribed to"); + + source.next(1); + assert_array_equals(results, [e]); + assert_equals(source.subscriberCount(), 0, + "source observable is unsubscribed from"); +}, "switchMap(): result emits an error if Mapper callback throws an error"); + +test(() => { + const source = createTestSubject(); + const inner = createTestSubject(); + + const result = source.switchMap(() => inner); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + source.next(1); + inner.next("a"); + assert_array_equals(results, ["a"]); + + const e = new Error('error from source'); + source.error(e); + assert_array_equals(results, ["a", e], + "switchMap result emits an error if the source emits an error"); + assert_equals(inner.subscriberCount(), 0, + "inner observable is unsubscribed from"); + assert_equals(source.subscriberCount(), 0, + "source observable is unsubscribed from"); +}, "switchMap(): result emits an error if the source observable emits an " + + "error"); + +test(() => { + const source = createTestSubject(); + const inner = createTestSubject(); + + const result = source.switchMap(() => inner); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + source.next(1); + inner.next("a"); + assert_array_equals(results, ["a"]); + + const e = new Error("error from inner"); + inner.error(e); + assert_array_equals(results, ["a", e], + "result emits an error if the inner observable emits an error"); + assert_equals(inner.subscriberCount(), 0, + "inner observable is unsubscribed from"); + assert_equals(source.subscriberCount(), 0, + "source observable is unsubscribed from"); +}, "switchMap(): result emits an error if the inner observable emits an error"); + +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.addTeardown(() => { + results.push('source teardown'); + }); + subscriber.signal.onabort = e => { + results.push('source onabort'); + }; + }); + + const inner = new Observable(subscriber => { + subscriber.addTeardown(() => { + results.push('inner teardown'); + }); + subscriber.signal.onabort = () => { + results.push('inner onabort'); + }; + }); + + const result = source.switchMap(() => inner); + + const ac = new AbortController(); + result.subscribe({ + next: v => results.push(v), + error: e => results.error(e), + complete: () => results.complete("complete"), + }, {signal: ac.signal}); + + ac.abort(); + assert_array_equals(results, [ + "source teardown", + "source onabort", + "inner teardown", + "inner onabort", + ], "Unsubscription order is correct"); +}, "switchMap(): should unsubscribe in the correct order when user aborts " + + "the subscription"); + +// A helper function to create an Observable that can be externally controlled +// and examined for testing purposes. +function createTestSubject() { + const subscribers = new Set(); + const subject = new Observable(subscriber => { + subscribers.add(subscriber); + subscriber.addTeardown(() => subscribers.delete(subscriber)); + }); + + subject.next = value => { + for (const subscriber of Array.from(subscribers)) { + subscriber.next(value); + } + }; + subject.error = error => { + for (const subscriber of Array.from(subscribers)) { + subscriber.error(error); + } + }; + subject.complete = () => { + for (const subscriber of Array.from(subscribers)) { + subscriber.complete(); + } + }; + subject.subscriberCount = () => { + return subscribers.size; + }; + + return subject; +} -- cgit v1.2.3