| // 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.when('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"); |
| |
| // This test, and the variants below it, test the web-observable side-effects of |
| // converting an iterable object to an Observable. Specifically, it tracks |
| // exactly when the %Symbol.iterator% method is *retrieved* from the object, |
| // invoked, and what its error-throwing side-effects are. |
| // |
| // Even more specifically, we assert that the %Symbol.iterator% method is |
| // retrieved a single time when converting to an Observable, and then again when |
| // subscribing to the converted Observable. This makes it possible for the |
| // %Symbol.iterator% method getter to change return values in between conversion |
| // and subscription. See https://github.com/WICG/observable/issues/127 for |
| // related discussion. |
| test(() => { |
| const results = []; |
| |
| const iterable = { |
| get [Symbol.iterator]() { |
| results.push("[Symbol.iterator] method GETTER"); |
| return function() { |
| results.push("[Symbol.iterator implementation]"); |
| return { |
| get next() { |
| results.push("next() method GETTER"); |
| return function() { |
| results.push("next() implementation"); |
| return {value: undefined, done: true}; |
| }; |
| }, |
| }; |
| }; |
| }, |
| }; |
| |
| const observable = Observable.from(iterable); |
| assert_array_equals(results, ["[Symbol.iterator] method GETTER"]); |
| |
| let thrownError = null; |
| observable.subscribe(); |
| assert_array_equals(results, [ |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator implementation]", |
| "next() method GETTER", |
| "next() implementation" |
| ]); |
| }, "from(): [Symbol.iterator] side-effects (one observable)"); |
| |
| // This tests that once `Observable.from()` detects a non-null and non-undefined |
| // `[Symbol.iterator]` property, we've committed to converting as an iterable. |
| // If the value of that property is then not callable, we don't silently move on |
| // to the next conversion type — we throw a TypeError. |
| // |
| // That's because that's what TC39's `GetMethod()` [1] calls for, which is what |
| // `Observable.from()` first uses in the iterable conversion branch [2]. |
| // |
| // [1]: https://tc39.es/ecma262/multipage/abstract-operations.html#sec-getmethod |
| // [2]: http://wicg.github.io/observable/#from-iterable-conversion |
| test(() => { |
| let results = []; |
| const iterable = { |
| [Symbol.iterator]: 10, |
| }; |
| |
| let errorThrown = null; |
| try { |
| Observable.from(iterable); |
| } catch(e) { |
| errorThrown = e; |
| } |
| |
| assert_true(errorThrown instanceof TypeError); |
| }, "from(): [Symbol.iterator] not callable"); |
| |
| test(() => { |
| let results = []; |
| const iterable = { |
| calledOnce: false, |
| get [Symbol.iterator]() { |
| if (this.calledOnce) { |
| // Return a non-callable primitive the second time `@@iterator` is |
| // called. |
| return 10; |
| } |
| |
| this.calledOnce = true; |
| return this.validImplementation; |
| }, |
| validImplementation: () => { |
| return { |
| next() { return {done: true}; } |
| } |
| } |
| }; |
| |
| let errorThrown = null; |
| |
| const observable = Observable.from(iterable); |
| observable.subscribe({ |
| next: v => results.push("should not be called"), |
| error: e => { |
| errorThrown = e; |
| results.push(e); |
| }, |
| }); |
| |
| assert_array_equals(results, [errorThrown], |
| "An error was plumbed through the Observable"); |
| assert_true(errorThrown instanceof TypeError); |
| }, "from(): [Symbol.iterator] not callable AFTER SUBSCRIBE throws"); |
| |
| test(() => { |
| let results = []; |
| const iterable = { |
| calledOnce: false, |
| validImplementation: () => { |
| return { |
| next() { return {done: true}; } |
| } |
| }, |
| get [Symbol.iterator]() { |
| if (this.calledOnce) { |
| // Return null the second time `@@iterator` is called. |
| return null; |
| } |
| |
| this.calledOnce = true; |
| return this.validImplementation; |
| } |
| }; |
| |
| let errorThrown = null; |
| |
| const observable = Observable.from(iterable); |
| observable.subscribe({ |
| next: v => results.push("should not be called"), |
| error: e => { |
| errorThrown = e; |
| results.push(e); |
| }, |
| }); |
| |
| assert_array_equals(results, [errorThrown], |
| "An error was plumbed through the Observable"); |
| assert_true(errorThrown instanceof TypeError); |
| }, "from(): [Symbol.iterator] returns null AFTER SUBSCRIBE throws"); |
| |
| test(() => { |
| let results = []; |
| const customError = new Error("@@iterator override error"); |
| |
| const iterable = { |
| numTimesCalled: 0, |
| |
| // The first time this getter is called, it returns a legitimate function |
| // that, when called, returns an iterator. Every other time it returns an |
| // error-throwing function that does not return an iterator. |
| get [Symbol.iterator]() { |
| this.numTimesCalled++; |
| results.push("[Symbol.iterator] method GETTER"); |
| |
| if (this.numTimesCalled === 1) { |
| return this.validIteratorImplementation; |
| } else { |
| return this.errorThrowingIteratorImplementation; |
| } |
| }, |
| |
| validIteratorImplementation: function() { |
| results.push("[Symbol.iterator implementation]"); |
| return { |
| get next() { |
| results.push("next() method GETTER"); |
| return function() { |
| results.push("next() implementation"); |
| return {value: undefined, done: true}; |
| } |
| } |
| }; |
| }, |
| errorThrowingIteratorImplementation: function() { |
| results.push("Error-throwing [Symbol.iterator] implementation"); |
| throw customError; |
| }, |
| }; |
| |
| const observable = Observable.from(iterable); |
| assert_array_equals(results, [ |
| "[Symbol.iterator] method GETTER", |
| ]); |
| |
| // 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. |
| |
| let thrownError = null; |
| observable.subscribe({ |
| error: e => thrownError = e, |
| }); |
| |
| assert_equals(thrownError, customError, |
| "Error thrown from next() is passed to the error() handler"); |
| assert_array_equals(results, [ |
| // Old: |
| "[Symbol.iterator] method GETTER", |
| // New: |
| "[Symbol.iterator] method GETTER", |
| "Error-throwing [Symbol.iterator] implementation" |
| ]); |
| }, "from(): [Symbol.iterator] is not cached"); |
| |
| // Similar to the above test, but with more Observables! |
| test(() => { |
| const results = []; |
| let numTimesSymbolIteratorCalled = 0; |
| let numTimesNextCalled = 0; |
| |
| const iterable = { |
| get [Symbol.iterator]() { |
| results.push("[Symbol.iterator] method GETTER"); |
| return this.internalIteratorImplementation; |
| }, |
| set [Symbol.iterator](func) { |
| this.internalIteratorImplementation = func; |
| }, |
| |
| internalIteratorImplementation: function() { |
| results.push("[Symbol.iterator] implementation"); |
| return { |
| get next() { |
| results.push("next() method GETTER"); |
| return function() { |
| results.push("next() implementation"); |
| 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(obs3, obs4); |
| |
| assert_array_equals(results, [ |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] method GETTER", |
| ]); |
| |
| obs1.subscribe(); |
| assert_array_equals(results, [ |
| // Old: |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] method GETTER", |
| // New: |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] implementation", |
| "next() method GETTER", |
| "next() implementation", |
| ]); |
| |
| iterable[Symbol.iterator] = () => { |
| results.push("Error-throwing [Symbol.iterator] implementation"); |
| throw new Error('Symbol.iterator override error'); |
| }; |
| |
| let errorCount = 0; |
| |
| const observer = {error: e => errorCount++}; |
| obs2.subscribe(observer); |
| obs3.subscribe(observer); |
| obs4.subscribe(observer); |
| assert_equals(errorCount, 3, |
| "Error-throwing `@@iterator` implementation is called once per " + |
| "subscription"); |
| |
| assert_array_equals(results, [ |
| // Old: |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] method GETTER", |
| "[Symbol.iterator] implementation", |
| "next() method GETTER", |
| "next() implementation", |
| // New: |
| "[Symbol.iterator] method GETTER", |
| "Error-throwing [Symbol.iterator] implementation", |
| "[Symbol.iterator] method GETTER", |
| "Error-throwing [Symbol.iterator] implementation", |
| "[Symbol.iterator] method GETTER", |
| "Error-throwing [Symbol.iterator] implementation", |
| ]); |
| }, "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"); |
| |
| // When the [Symbol.iterator] method on a given object is undefined, we don't |
| // try to convert the object to an Observable via the iterable protocol. The |
| // Observable specification *also* does the same thing if the [Symbol.iterator] |
| // method is *null*. That is, in that case we also skip the conversion via |
| // iterable protocol, and continue to try and convert the object as another type |
| // (in this case, a Promise). |
| promise_test(async () => { |
| const promise = new Promise(resolve => resolve('from Promise')); |
| assert_equals(promise[Symbol.iterator], undefined); |
| promise[Symbol.iterator] = null; |
| assert_equals(promise[Symbol.iterator], null); |
| |
| const value = await new Promise(resolve => { |
| Observable.from(promise).subscribe(value => resolve(value)); |
| }); |
| |
| assert_equals(value, 'from Promise'); |
| }, "from(): Promise whose [Symbol.iterator] returns null converts as Promise"); |
| |
| // This is a more sensitive test, which asserts that even just trying to reach |
| // for the [Symbol.iterator] method on an object whose *getter* for the |
| // [Symbol.iterator] method throws an error, results in `Observable#from()` |
| // rethrowing that error. |
| test(() => { |
| const error = new Error('thrown from @@iterator getter'); |
| const obj = { |
| get [Symbol.iterator]() { |
| throw error; |
| } |
| } |
| |
| try { |
| Observable.from(obj); |
| assert_unreached("from() conversion throws"); |
| } catch(e) { |
| assert_equals(e, error); |
| } |
| }, "from(): Rethrows the error when Converting an object whose @@iterator " + |
| "method *getter* throws an error"); |
| |
| // This test exercises the line of spec prose that says: |
| // |
| // "If |asyncIteratorMethodRecord|'s [[Value]] is undefined or null, then jump |
| // to the step labeled 'From iterable'." |
| test(() => { |
| const sync_iterable = { |
| [Symbol.asyncIterator]: null, |
| [Symbol.iterator]() { |
| return { |
| value: 0, |
| next() { |
| if (this.value === 2) |
| return {value: undefined, done: true}; |
| else |
| return {value: this.value++, done: false}; |
| } |
| } |
| }, |
| }; |
| |
| const results = []; |
| const source = Observable.from(sync_iterable).subscribe(v => results.push(v)); |
| assert_array_equals(results, [0, 1]); |
| }, "from(): Async iterable protocol null, converts as iterator"); |
| |
| promise_test(async t => { |
| const results = []; |
| const async_iterable = { |
| [Symbol.asyncIterator]() { |
| results.push("[Symbol.asyncIterator]() invoked"); |
| return { |
| val: 0, |
| next() { |
| return new Promise(resolve => { |
| t.step_timeout(() => { |
| resolve({ |
| value: this.val, |
| done: this.val++ === 4 ? true : false, |
| }); |
| }, 400); |
| }); |
| }, |
| }; |
| }, |
| }; |
| |
| const source = Observable.from(async_iterable); |
| assert_array_equals(results, []); |
| |
| await new Promise(resolve => { |
| source.subscribe({ |
| next: v => { |
| results.push(`Observing ${v}`); |
| queueMicrotask(() => results.push(`next() microtask interleaving (v=${v})`)); |
| }, |
| complete: () => { |
| results.push('complete()'); |
| resolve(); |
| }, |
| }); |
| }); |
| |
| assert_array_equals(results, [ |
| "[Symbol.asyncIterator]() invoked", |
| "Observing 0", |
| "next() microtask interleaving (v=0)", |
| "Observing 1", |
| "next() microtask interleaving (v=1)", |
| "Observing 2", |
| "next() microtask interleaving (v=2)", |
| "Observing 3", |
| "next() microtask interleaving (v=3)", |
| "complete()", |
| ]); |
| }, "from(): Asynchronous iterable conversion"); |
| |
| // This test is a more chaotic version of the above. It ensures that a single |
| // Observable can handle multiple in-flight subscriptions to the same underlying |
| // async iterable without the two subscriptions competing. It asserts that the |
| // asynchronous values are pushed to the observers in the correct order. |
| promise_test(async t => { |
| const async_iterable = { |
| [Symbol.asyncIterator]() { |
| return { |
| val: 0, |
| next() { |
| // Returns a Promise that resolves in a random amount of time less |
| // than a second. |
| return new Promise(resolve => { |
| t.step_timeout(() => resolve({ |
| value: this.val, |
| done: this.val++ === 4 ? true : false, |
| }), 200); |
| }); |
| }, |
| }; |
| }, |
| }; |
| |
| const results = []; |
| const source = Observable.from(async_iterable); |
| |
| const promise = new Promise(resolve => { |
| source.subscribe({ |
| next: v => { |
| results.push(`${v}-first-sub`); |
| |
| // Half-way through the first subscription, start another subscription. |
| if (v === 0) { |
| source.subscribe({ |
| next: v => results.push(`${v}-second-sub`), |
| complete: () => { |
| results.push('complete-second-sub'); |
| resolve(); |
| } |
| }); |
| } |
| }, |
| complete: () => { |
| results.push('complete-first-sub'); |
| resolve(); |
| } |
| }); |
| }); |
| |
| await promise; |
| assert_array_equals(results, [ |
| '0-first-sub', |
| |
| '1-first-sub', |
| '1-second-sub', |
| |
| '2-first-sub', |
| '2-second-sub', |
| |
| '3-first-sub', |
| '3-second-sub', |
| |
| 'complete-first-sub', |
| 'complete-second-sub', |
| ]); |
| }, "from(): Asynchronous iterable multiple in-flight subscriptions"); |
| // This test is like the above, ensuring that multiple subscriptions to the same |
| // sync-iterable-converted-Observable can exist at a time. Since sync iterables |
| // push all of their values to the Observable synchronously, the way to do this |
| // is subscribe to the sync iterable Observable *inside* the next handler of the |
| // same Observable. |
| test(() => { |
| const results = []; |
| |
| const array = [1, 2, 3, 4, 5]; |
| const source = Observable.from(array); |
| source.subscribe({ |
| next: v => { |
| results.push(`${v}-first-sub`); |
| if (v === 3) { |
| // Pushes all 5 values to `results` right after the first instance of `3`. |
| source.subscribe({ |
| next: v => results.push(`${v}-second-sub`), |
| complete: () => results.push('complete-second-sub'), |
| }); |
| } |
| }, |
| complete: () => results.push('complete-first-sub'), |
| }); |
| |
| assert_array_equals(results, [ |
| // These values are pushed when there is only a single subscription. |
| '1-first-sub', '2-first-sub', '3-first-sub', |
| // These values are pushed in the correct order, for two subscriptions. |
| '4-first-sub', '4-second-sub', |
| '5-first-sub', '5-second-sub', |
| 'complete-first-sub', 'complete-second-sub', |
| ]); |
| }, "from(): Sync iterable multiple in-flight subscriptions"); |
| |
| promise_test(async () => { |
| const async_generator = async function*() { |
| yield 1; |
| yield 2; |
| yield 3; |
| }; |
| |
| const results = []; |
| const source = Observable.from(async_generator()); |
| |
| const subscribeFunction = function(resolve) { |
| source.subscribe({ |
| next: v => results.push(v), |
| complete: () => resolve(), |
| }); |
| } |
| await new Promise(subscribeFunction); |
| assert_array_equals(results, [1, 2, 3]); |
| await new Promise(subscribeFunction); |
| assert_array_equals(results, [1, 2, 3]); |
| }, "from(): Asynchronous generator conversion: can only be used once"); |
| |
| // The value returned by an async iterator object's `next()` method is supposed |
| // to be a Promise. But this requirement "isn't enforced": see [1]. Therefore, |
| // the Observable spec unconditionally wraps the return value in a resolved |
| // Promise, as is standard practice [2]. |
| // |
| // This test ensures that even if the object returned from an async iterator's |
| // `next()` method is a synchronously-available object with `done: true` |
| // (instead of a Promise), the `done` property is STILL not retrieved |
| // synchronously. In other words, we test that the Promise-wrapping is |
| // implemented. |
| // |
| // [1]: https://tc39.es/ecma262/#table-async-iterator-r |
| // [2]: https://matrixlogs.bakkot.com/WHATWG/2024-08-30#L30 |
| promise_test(async () => { |
| const results = []; |
| |
| const async_iterable = { |
| [Symbol.asyncIterator]() { |
| return { |
| next() { |
| return { |
| value: undefined, |
| get done() { |
| results.push('done() GETTER called'); |
| return true; |
| }, |
| }; |
| }, |
| }; |
| }, |
| }; |
| |
| const source = Observable.from(async_iterable); |
| assert_array_equals(results, []); |
| |
| queueMicrotask(() => results.push('Microtask queued before subscription')); |
| source.subscribe(); |
| assert_array_equals(results, []); |
| |
| await Promise.resolve(); |
| assert_array_equals(results, [ |
| "Microtask queued before subscription", |
| "done() GETTER called", |
| ]); |
| }, "from(): Promise-wrapping semantics of IteratorResult interface"); |
| |
| // Errors thrown from [Symbol.asyncIterator] are propagated to the observer |
| // synchronously. This is because in language constructs (i.e., for-await of |
| // loops) that invoke [Symbol.asyncIterator]() that throw errors, the errors are |
| // synchronously propagated to script outside of the loop, and are catchable. |
| // Observables follow this precedent. |
| test(() => { |
| const error = new Error("[Symbol.asyncIterator] error"); |
| const results = []; |
| const async_iterable = { |
| [Symbol.asyncIterator]() { |
| results.push("[Symbol.asyncIterator]() invoked"); |
| throw error; |
| } |
| }; |
| |
| Observable.from(async_iterable).subscribe({ |
| error: e => results.push(e), |
| }); |
| |
| assert_array_equals(results, [ |
| "[Symbol.asyncIterator]() invoked", |
| error, |
| ]); |
| }, "from(): Errors thrown in Symbol.asyncIterator() are propagated synchronously"); |
| |
| // AsyncIterable: next() throws exception instead of return Promise. Any errors |
| // that occur during the retrieval of `next()` always result in a rejected |
| // Promise. Therefore, the error makes it to the Observer with microtask timing. |
| promise_test(async () => { |
| const nextError = new Error('next error'); |
| const async_iterable = { |
| [Symbol.asyncIterator]() { |
| return { |
| get next() { |
| throw nextError; |
| } |
| }; |
| } |
| }; |
| |
| const results = []; |
| Observable.from(async_iterable).subscribe({ |
| error: e => results.push(e), |
| }); |
| |
| assert_array_equals(results, []); |
| // Wait one microtask since the error will be propagated through a rejected |
| // Promise managed by the async iterable conversion semantics. |
| await Promise.resolve(); |
| assert_array_equals(results, [nextError]); |
| }, "from(): Errors thrown in async iterator's next() GETTER are propagated " + |
| "in a microtask"); |
| promise_test(async () => { |
| const nextError = new Error('next error'); |
| const async_iterable = { |
| [Symbol.asyncIterator]() { |
| return { |
| next() { |
| throw nextError; |
| } |
| }; |
| } |
| }; |
| |
| const results = []; |
| Observable.from(async_iterable).subscribe({ |
| error: e => results.push(e), |
| }); |
| |
| assert_array_equals(results, []); |
| await Promise.resolve(); |
| assert_array_equals(results, [nextError]); |
| }, "from(): Errors thrown in async iterator's next() are propagated in a microtask"); |
| |
| test(() => { |
| const results = []; |
| const iterable = { |
| [Symbol.iterator]() { |
| return { |
| val: 0, |
| next() { |
| results.push(`IteratorRecord#next() pushing ${this.val}`); |
| return { |
| value: this.val, |
| done: this.val++ === 10 ? true : false, |
| }; |
| }, |
| return() { |
| results.push(`IteratorRecord#return() called with this.val=${this.val}`); |
| }, |
| }; |
| }, |
| }; |
| |
| const ac = new AbortController(); |
| Observable.from(iterable).subscribe(v => { |
| results.push(`Observing ${v}`); |
| if (v === 3) { |
| ac.abort(); |
| } |
| }, {signal: ac.signal}); |
| |
| assert_array_equals(results, [ |
| "IteratorRecord#next() pushing 0", |
| "Observing 0", |
| "IteratorRecord#next() pushing 1", |
| "Observing 1", |
| "IteratorRecord#next() pushing 2", |
| "Observing 2", |
| "IteratorRecord#next() pushing 3", |
| "Observing 3", |
| "IteratorRecord#return() called with this.val=4", |
| ]); |
| }, "from(): Aborting sync iterable midway through iteration both stops iteration " + |
| "and invokes `IteratorRecord#return()"); |
| // Like the above test, but for async iterables. |
| promise_test(async t => { |
| const results = []; |
| const iterable = { |
| [Symbol.asyncIterator]() { |
| return { |
| val: 0, |
| next() { |
| results.push(`IteratorRecord#next() pushing ${this.val}`); |
| return { |
| value: this.val, |
| done: this.val++ === 10 ? true : false, |
| }; |
| }, |
| return(reason) { |
| results.push(`IteratorRecord#return() called with reason=${reason}`); |
| return {done: true}; |
| }, |
| }; |
| }, |
| }; |
| |
| const ac = new AbortController(); |
| await new Promise(resolve => { |
| Observable.from(iterable).subscribe(v => { |
| results.push(`Observing ${v}`); |
| if (v === 3) { |
| ac.abort(`Aborting because v=${v}`); |
| resolve(); |
| } |
| }, {signal: ac.signal}); |
| }); |
| |
| assert_array_equals(results, [ |
| "IteratorRecord#next() pushing 0", |
| "Observing 0", |
| "IteratorRecord#next() pushing 1", |
| "Observing 1", |
| "IteratorRecord#next() pushing 2", |
| "Observing 2", |
| "IteratorRecord#next() pushing 3", |
| "Observing 3", |
| "IteratorRecord#return() called with reason=Aborting because v=3", |
| ]); |
| }, "from(): Aborting async iterable midway through iteration both stops iteration " + |
| "and invokes `IteratorRecord#return()"); |
| |
| test(() => { |
| const iterable = { |
| [Symbol.iterator]() { |
| return { |
| val: 0, |
| next() { |
| return {value: this.val, done: this.val++ === 10 ? true : false}; |
| }, |
| // Not returning an Object results in a TypeError being thrown. |
| return(reason) {}, |
| }; |
| }, |
| }; |
| |
| let thrownError = null; |
| const ac = new AbortController(); |
| Observable.from(iterable).subscribe(v => { |
| if (v === 3) { |
| try { |
| ac.abort(`Aborting because v=${v}`); |
| } catch (e) { |
| thrownError = e; |
| } |
| } |
| }, {signal: ac.signal}); |
| |
| assert_not_equals(thrownError, null, "abort() threw an Error"); |
| assert_true(thrownError instanceof TypeError); |
| assert_true(thrownError.message.includes('return()')); |
| assert_true(thrownError.message.includes('Object')); |
| }, "from(): Sync iterable: `Iterator#return()` must return an Object, or an " + |
| "error is thrown"); |
| // This test is just like the above but for async iterables. It asserts that a |
| // Promise is rejected when `return()` does not return an Object. |
| promise_test(async t => { |
| const iterable = { |
| [Symbol.asyncIterator]() { |
| return { |
| val: 0, |
| next() { |
| return {value: this.val, done: this.val++ === 10 ? true : false}; |
| }, |
| // Not returning an Object results in a rejected Promise. |
| return(reason) {}, |
| }; |
| }, |
| }; |
| |
| const unhandled_rejection_promise = new Promise((resolve, reject) => { |
| const unhandled_rejection_handler = e => resolve(e.reason); |
| self.addEventListener("unhandledrejection", unhandled_rejection_handler); |
| t.add_cleanup(() => |
| self.removeEventListener("unhandledrejection", unhandled_rejection_handler)); |
| |
| t.step_timeout(() => reject('Timeout'), 3000); |
| }); |
| |
| const ac = new AbortController(); |
| await new Promise(resolve => { |
| Observable.from(iterable).subscribe(v => { |
| if (v === 3) { |
| ac.abort(`Aborting because v=${v}`); |
| resolve(); |
| } |
| }, {signal: ac.signal}); |
| }); |
| |
| const reason = await unhandled_rejection_promise; |
| assert_true(reason instanceof TypeError); |
| assert_true(reason.message.includes('return()')); |
| assert_true(reason.message.includes('Object')); |
| }, "from(): Async iterable: `Iterator#return()` must return an Object, or a " + |
| "Promise rejects asynchronously"); |
| |
| // This test exercises the logic of `GetIterator()` async->sync fallback |
| // logic. Specifically, we have an object that is an async iterable — that is, |
| // it has a callback [Symbol.asyncIterator] implementation. Observable.from() |
| // detects this, and commits to converting the object from the async iterable |
| // protocol. Then, after conversion but before subscription, the object is |
| // modified such that it no longer implements the async iterable protocol. |
| // |
| // But since it still implements the *iterable* protocol, ECMAScript's |
| // `GetIterator()` abstract algorithm [1] is fully exercised, which is spec'd to |
| // fall-back to the synchronous iterable protocol if it exists, and create a |
| // fully async iterable out of the synchronous iterable. |
| // |
| // [1]: https://tc39.es/ecma262/#sec-getiterator |
| promise_test(async () => { |
| const results = []; |
| const async_iterable = { |
| asyncIteratorGotten: false, |
| get [Symbol.asyncIterator]() { |
| results.push("[Symbol.asyncIterator] GETTER"); |
| if (this.asyncIteratorGotten) { |
| return null; // Both null and undefined work here. |
| } |
| |
| this.asyncIteratorGotten = true; |
| // The only requirement for `this` to be converted as an async |
| // iterable -> Observable is that the return value be callable (i.e., a function). |
| return function() {}; |
| }, |
| |
| [Symbol.iterator]() { |
| results.push('[Symbol.iterator]() invoked as fallback'); |
| return { |
| val: 0, |
| next() { |
| return { |
| value: this.val, |
| done: this.val++ === 4 ? true : false, |
| }; |
| }, |
| }; |
| }, |
| }; |
| |
| const source = Observable.from(async_iterable); |
| assert_array_equals(results, [ |
| "[Symbol.asyncIterator] GETTER", |
| ]); |
| |
| await new Promise((resolve, reject) => { |
| source.subscribe({ |
| next: v => { |
| results.push(`Observing ${v}`); |
| queueMicrotask(() => results.push(`next() microtask interleaving (v=${v})`)); |
| }, |
| error: e => reject(e), |
| complete: () => { |
| results.push('complete()'); |
| resolve(); |
| }, |
| }); |
| }); |
| |
| assert_array_equals(results, [ |
| // Old: |
| "[Symbol.asyncIterator] GETTER", |
| // New: |
| "[Symbol.asyncIterator] GETTER", |
| "[Symbol.iterator]() invoked as fallback", |
| "Observing 0", |
| "next() microtask interleaving (v=0)", |
| "Observing 1", |
| "next() microtask interleaving (v=1)", |
| "Observing 2", |
| "next() microtask interleaving (v=2)", |
| "Observing 3", |
| "next() microtask interleaving (v=3)", |
| "complete()", |
| ]); |
| }, "from(): Asynchronous iterable conversion, with synchronous iterable fallback"); |
| |
| test(() => { |
| const results = []; |
| let generatorFinalized = false; |
| |
| const generator = function*() { |
| try { |
| for (let n = 0; n < 10; n++) { |
| yield n; |
| } |
| } finally { |
| generatorFinalized = true; |
| } |
| }; |
| |
| const observable = Observable.from(generator()); |
| const abortController = new AbortController(); |
| |
| observable.subscribe(n => { |
| results.push(n); |
| if (n === 3) { |
| abortController.abort(); |
| } |
| }, {signal: abortController.signal}); |
| |
| assert_array_equals(results, [0, 1, 2, 3]); |
| assert_true(generatorFinalized); |
| }, "from(): Generator finally block runs when subscription is aborted"); |
| |
| test(() => { |
| const results = []; |
| let generatorFinalized = false; |
| |
| const generator = function*() { |
| try { |
| for (let n = 0; n < 10; n++) { |
| yield n; |
| } |
| } catch { |
| assert_unreached("generator should not be aborted"); |
| } finally { |
| generatorFinalized = true; |
| } |
| }; |
| |
| const observable = Observable.from(generator()); |
| |
| observable.subscribe((n) => { |
| results.push(n); |
| }); |
| |
| assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); |
| assert_true(generatorFinalized); |
| }, "from(): Generator finally block run when Observable completes"); |
| |
| test(() => { |
| const results = []; |
| let generatorFinalized = false; |
| |
| const generator = function*() { |
| try { |
| for (let n = 0; n < 10; n++) { |
| yield n; |
| } |
| throw new Error('from the generator'); |
| } finally { |
| generatorFinalized = true; |
| } |
| }; |
| |
| const observable = Observable.from(generator()); |
| |
| observable.subscribe({ |
| next: n => results.push(n), |
| error: e => results.push(e.message) |
| }); |
| |
| assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "from the generator"]); |
| assert_true(generatorFinalized); |
| }, "from(): Generator finally block run when Observable errors"); |
| |
| promise_test(async t => { |
| const results = []; |
| let generatorFinalized = false; |
| |
| async function* asyncGenerator() { |
| try { |
| for (let n = 0; n < 10; n++) { |
| yield n; |
| } |
| } finally { |
| generatorFinalized = true; |
| } |
| } |
| |
| const observable = Observable.from(asyncGenerator()); |
| const abortController = new AbortController(); |
| |
| await new Promise((resolve) => { |
| observable.subscribe((n) => { |
| results.push(n); |
| if (n === 3) { |
| abortController.abort(); |
| resolve(); |
| } |
| }, {signal: abortController.signal}); |
| }); |
| |
| assert_array_equals(results, [0, 1, 2, 3]); |
| assert_true(generatorFinalized); |
| }, "from(): Async generator finally block run when subscription is aborted"); |
| |
| promise_test(async t => { |
| const results = []; |
| let generatorFinalized = false; |
| |
| async function* asyncGenerator() { |
| try { |
| for (let n = 0; n < 10; n++) { |
| yield n; |
| } |
| } finally { |
| generatorFinalized = true; |
| } |
| } |
| |
| const observable = Observable.from(asyncGenerator()); |
| |
| await new Promise(resolve => { |
| observable.subscribe({ |
| next: n => results.push(n), |
| complete: () => resolve(), |
| }); |
| }); |
| |
| assert_array_equals(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); |
| assert_true(generatorFinalized); |
| }, "from(): Async generator finally block runs when Observable completes"); |
| |
| promise_test(async t => { |
| const results = []; |
| let generatorFinalized = false; |
| |
| async function* asyncGenerator() { |
| try { |
| for (let n = 0; n < 10; n++) { |
| if (n === 4) { |
| throw new Error('from the async generator'); |
| } |
| yield n; |
| } |
| } finally { |
| generatorFinalized = true; |
| } |
| } |
| |
| const observable = Observable.from(asyncGenerator()); |
| |
| await new Promise((resolve) => { |
| observable.subscribe({ |
| next: (n) => results.push(n), |
| error: (e) => { |
| results.push(e.message); |
| resolve(); |
| } |
| }); |
| }); |
| |
| assert_array_equals(results, [0, 1, 2, 3, "from the async generator"]); |
| assert_true(generatorFinalized); |
| }, "from(): Async generator finally block run when Observable errors"); |
| |
| // Test what happens when `return()` throws an error upon abort. |
| test(() => { |
| const results = []; |
| const iterable = { |
| [Symbol.iterator]() { |
| return { |
| val: 0, |
| next() { |
| results.push('next() called'); |
| return {value: this.val, done: this.val++ === 10 ? true : false}; |
| }, |
| return() { |
| results.push('return() about to throw an error'); |
| throw new Error('return() error'); |
| }, |
| }; |
| } |
| }; |
| |
| const ac = new AbortController(); |
| const source = Observable.from(iterable); |
| source.subscribe(v => { |
| if (v === 3) { |
| try { |
| ac.abort(); |
| } catch (e) { |
| results.push(`AbortController#abort() threw an error: ${e.message}`); |
| } |
| } |
| }, {signal: ac.signal}); |
| |
| assert_array_equals(results, [ |
| 'next() called', |
| 'next() called', |
| 'next() called', |
| 'next() called', |
| 'return() about to throw an error', |
| 'AbortController#abort() threw an error: return() error', |
| ]); |
| }, "from(): Sync iterable: error thrown from IteratorRecord#return() can be " + |
| "synchronously caught"); |
| promise_test(async t => { |
| const results = []; |
| const iterable = { |
| [Symbol.asyncIterator]() { |
| return { |
| val: 0, |
| next() { |
| results.push('next() called'); |
| return {value: this.val, done: this.val++ === 10 ? true : false}; |
| }, |
| return() { |
| results.push('return() about to throw an error'); |
| // For async iterables, errors thrown in `return()` end up in a |
| // returned rejected Promise, so no error appears on the stack |
| // immediately. See [1]. |
| // |
| // [1]: https://whatpr.org/webidl/1397.html#async-iterator-close. |
| throw new Error('return() error'); |
| }, |
| }; |
| } |
| }; |
| |
| const unhandled_rejection_promise = new Promise((resolve, reject) => { |
| const unhandled_rejection_handler = e => resolve(e.reason); |
| self.addEventListener("unhandledrejection", unhandled_rejection_handler); |
| t.add_cleanup(() => |
| self.removeEventListener("unhandledrejection", unhandled_rejection_handler)); |
| |
| t.step_timeout(() => reject('Timeout'), 1500); |
| }); |
| |
| const ac = new AbortController(); |
| const source = Observable.from(iterable); |
| await new Promise((resolve, reject) => { |
| source.subscribe(v => { |
| if (v === 3) { |
| try { |
| ac.abort(); |
| results.push('No error thrown synchronously'); |
| resolve('No error thrown synchronously'); |
| } catch (e) { |
| results.push(`AbortController#abort() threw an error: ${e.message}`); |
| reject(e); |
| } |
| } |
| }, {signal: ac.signal}); |
| }); |
| |
| assert_array_equals(results, [ |
| 'next() called', |
| 'next() called', |
| 'next() called', |
| 'next() called', |
| 'return() about to throw an error', |
| 'No error thrown synchronously', |
| ]); |
| |
| const reason = await unhandled_rejection_promise; |
| assert_true(reason instanceof Error); |
| assert_equals(reason.message, "return() error", |
| "Custom error text passed through rejected Promise"); |
| }, "from(): Async iterable: error thrown from IteratorRecord#return() is " + |
| "wrapped in rejected Promise"); |
| |
| test(() => { |
| const results = []; |
| const iterable = { |
| getter() { |
| results.push('GETTER called'); |
| return () => { |
| results.push('Obtaining iterator'); |
| return { |
| next() { |
| results.push('next() running'); |
| return {done: true}; |
| } |
| }; |
| }; |
| } |
| }; |
| |
| Object.defineProperty(iterable, Symbol.iterator, { |
| get: iterable.getter |
| }); |
| { |
| const source = Observable.from(iterable); |
| assert_array_equals(results, ["GETTER called"]); |
| source.subscribe({}, {signal: AbortSignal.abort()}); |
| assert_array_equals(results, ["GETTER called"]); |
| } |
| iterable[Symbol.iterator] = undefined; |
| Object.defineProperty(iterable, Symbol.asyncIterator, { |
| get: iterable.getter |
| }); |
| { |
| const source = Observable.from(iterable); |
| assert_array_equals(results, ["GETTER called", "GETTER called"]); |
| source.subscribe({}, {signal: AbortSignal.abort()}); |
| assert_array_equals(results, ["GETTER called", "GETTER called"]); |
| } |
| }, "from(): Subscribing to an iterable Observable with an aborted signal " + |
| "does not call next()"); |
| |
| test(() => { |
| let results = []; |
| |
| const iterable = { |
| controller: null, |
| calledOnce: false, |
| getter() { |
| results.push('GETTER called'); |
| if (!this.calledOnce) { |
| this.calledOnce = true; |
| return () => { |
| results.push('NOT CALLED'); |
| // We don't need to return anything here. The only time this path is |
| // hit is during `Observable.from()` which doesn't actually obtain an |
| // iterator. It just samples the iterable protocol property to ensure |
| // that it's valid. |
| }; |
| } |
| |
| // This path is only called the second time the iterator protocol getter |
| // is run. |
| this.controller.abort(); |
| return () => { |
| results.push('iterator obtained'); |
| return { |
| val: 0, |
| next() { |
| results.push('next() called'); |
| return {done: true}; |
| }, |
| return() { |
| results.push('return() called'); |
| } |
| }; |
| }; |
| } |
| }; |
| |
| // Test for sync iterators. |
| { |
| const ac = new AbortController(); |
| iterable.controller = ac; |
| Object.defineProperty(iterable, Symbol.iterator, { |
| get: iterable.getter, |
| }); |
| |
| const source = Observable.from(iterable); |
| assert_false(ac.signal.aborted, "[Sync iterator]: signal is not yet aborted after from() conversion"); |
| assert_array_equals(results, ["GETTER called"]); |
| |
| source.subscribe({ |
| next: n => results.push(n), |
| complete: () => results.push('complete'), |
| }, {signal: ac.signal}); |
| assert_true(ac.signal.aborted, "[Sync iterator]: signal is aborted during subscription"); |
| assert_array_equals(results, ["GETTER called", "GETTER called", "iterator obtained"]); |
| } |
| |
| results = []; |
| |
| // Test for async iterators. |
| { |
| // Reset `iterable` so it can be reused. |
| const ac = new AbortController(); |
| iterable.controller = ac; |
| iterable.calledOnce = false; |
| iterable[Symbol.iterator] = undefined; |
| Object.defineProperty(iterable, Symbol.asyncIterator, { |
| get: iterable.getter |
| }); |
| |
| const source = Observable.from(iterable); |
| assert_false(ac.signal.aborted, "[Async iterator]: signal is not yet aborted after from() conversion"); |
| assert_array_equals(results, ["GETTER called"]); |
| |
| source.subscribe({ |
| next: n => results.push(n), |
| complete: () => results.push('complete'), |
| }, {signal: ac.signal}); |
| assert_true(ac.signal.aborted, "[Async iterator]: signal is aborted during subscription"); |
| assert_array_equals(results, ["GETTER called", "GETTER called", "iterator obtained"]); |
| } |
| }, "from(): When iterable conversion aborts the subscription, next() is " + |
| "never called"); |
| |
| // This test asserts some very subtle behavior with regard to async iterables |
| // and a mid-subscription signal abort. Specifically it detects that a signal |
| // abort ensures that the `next()` method is not called again on the iterator |
| // again, BUT detects that pending Promise from the *previous* `next()` call |
| // still has its IteratorResult object examined. I.e., the implementation |
| // inspecting the `done` attribute on the resolved IteratorResult is observable |
| // event after abort() takes place. |
| promise_test(async () => { |
| const results = []; |
| let resolveNext = null; |
| |
| const iterable = { |
| [Symbol.asyncIterator]() { |
| return { |
| next() { |
| results.push('next() called'); |
| return new Promise(resolve => { |
| resolveNext = resolve; |
| }); |
| }, |
| return() { |
| results.push('return() called'); |
| } |
| }; |
| } |
| }; |
| |
| const ac = new AbortController(); |
| const source = Observable.from(iterable); |
| source.subscribe({ |
| next: v => results.push(v), |
| complete: () => results.push('complete'), |
| }, {signal: ac.signal}); |
| |
| assert_array_equals(results, [ |
| "next() called", |
| ]); |
| |
| // First abort, ensuring `return()` is called. |
| ac.abort(); |
| |
| assert_array_equals(results, [ |
| "next() called", |
| "return() called", |
| ]); |
| |
| // Then resolve the pending `next()` Promise to an object whose `done` getter |
| // reports to the test whether it was accessed. We have to wait one microtask |
| // for the internal Observable implementation to finish "reacting" to said |
| // `next()` promise resolution, for it to grab the `done` attribute. |
| await new Promise(resolveOuter => { |
| resolveNext({ |
| get done() { |
| results.push('IteratorResult.done GETTER'); |
| resolveOuter(); |
| return true; |
| } |
| }); |
| }); |
| |
| assert_array_equals(results, [ |
| "next() called", |
| "return() called", |
| "IteratorResult.done GETTER", |
| // Note that "next() called" does not make another appearance. |
| ]); |
| }, "from(): Aborting an async iterable subscription stops subsequent next() " + |
| "calls, but old next() Promise reactions are web-observable"); |
| |
| test(() => { |
| const results = []; |
| const iterable = { |
| [Symbol.iterator]() { |
| return { |
| val: 0, |
| next() { |
| return {value: this.val, done: this.val++ === 4 ? true : false}; |
| }, |
| return() { |
| results.push('return() called'); |
| }, |
| }; |
| } |
| }; |
| |
| const source = Observable.from(iterable); |
| const ac = new AbortController(); |
| source.subscribe({ |
| next: v => results.push(v), |
| complete: () => results.push('complete'), |
| }, {signal: ac.signal}); |
| |
| ac.abort(); // Must do nothing! |
| assert_array_equals(results, [0, 1, 2, 3, 'complete']); |
| }, "from(): Abort after complete does NOT call IteratorRecord#return()"); |
| |
| test(() => { |
| const controller = new AbortController(); |
| // Invalid @@asyncIterator protocol that also aborts the subscription. By the |
| // time the invalid-ness of the protocol is detected, the controller has been |
| // aborted, meaning that invalid-ness cannot manifest itself in the form of an |
| // error that goes to the Observable's subscriber. Instead, it gets reported |
| // to the global. |
| const asyncIterable = { |
| calledOnce: false, |
| get[Symbol.asyncIterator]() { |
| // This `calledOnce` path is to ensure the Observable first converts |
| // correctly via `Observable.from()`, but *later* fails in the path where |
| // `@@asyncIterator` is null. |
| if (this.calledOnce) { |
| controller.abort(); |
| return null; |
| } else { |
| this.calledOnce = true; |
| return this.validImplementation; |
| } |
| }, |
| validImplementation() { |
| controller.abort(); |
| return null; |
| } |
| }; |
| |
| let reportedError = null; |
| self.addEventListener("error", e => reportedError = e.error, {once: true}); |
| |
| let errorThrown = null; |
| const observable = Observable.from(asyncIterable); |
| observable.subscribe({ |
| error: e => errorThrown = e, |
| }, {signal: controller.signal}); |
| |
| assert_equals(errorThrown, null, "Protocol error is not surfaced to the Subscriber"); |
| |
| assert_not_equals(reportedError, null, "Protocol error is reported to the global"); |
| assert_true(reportedError instanceof TypeError); |
| }, "Invalid async iterator protocol error is surfaced before Subscriber#signal is consulted"); |
| |
| test(() => { |
| const controller = new AbortController(); |
| const iterable = { |
| calledOnce: false, |
| get[Symbol.iterator]() { |
| if (this.calledOnce) { |
| controller.abort(); |
| return null; |
| } else { |
| this.calledOnce = true; |
| return this.validImplementation; |
| } |
| }, |
| validImplementation() { |
| controller.abort(); |
| return null; |
| } |
| }; |
| |
| let reportedError = null; |
| self.addEventListener("error", e => reportedError = e.error, {once: true}); |
| |
| let errorThrown = null; |
| const observable = Observable.from(iterable); |
| observable.subscribe({ |
| error: e => errorThrown = e, |
| }, {signal: controller.signal}); |
| |
| assert_equals(errorThrown, null, "Protocol error is not surfaced to the Subscriber"); |
| |
| assert_not_equals(reportedError, null, "Protocol error is reported to the global"); |
| assert_true(reportedError instanceof TypeError); |
| }, "Invalid iterator protocol error is surfaced before Subscriber#signal is consulted"); |
| |
| // Regression test for https://github.com/WICG/observable/issues/208. |
| promise_test(async () => { |
| let errorReported = false; |
| self.onerror = e => errorReported = true; |
| |
| // `first()` aborts the subscription after the first item is encountered. |
| const value = await Observable.from([1, 2, 3]).first(); |
| assert_false(errorReported); |
| }, "No error is reported when aborting a subscription to a sync iterator " + |
| "that has no `return()` implementation"); |