From 3a6ceb84ac89b07ac84b08bd9173c0395faf6472 Mon Sep 17 00:00:00 2001 From: Dominic Farolino Date: Wed, 29 Jan 2025 17:39:42 +0000 Subject: [PATCH] Bug 1944349 [wpt PR 50338] - DOM: Add more Observable iterable tests, a=testonly Automatic update from web-platform-tests DOM: Add more Observable iterable tests This CL adds and supplements a few tests: 1. First we modify the existing "subscribe with aborted signal" tests. Specifically, we expand their assertions to not only assert that `next()` isn't ever called, but make more assertions about the iterator protocol getter and function invocations in general. 2. Second, we modify the test that asserts `next()` is not called when you subscribe with an unaborted signal, but that signal gets aborted while the iterator protocol methods are called during subscription of the Observable. We expand the assertions in the same way as (1), and combine the two separate tests into one that covers both sync and async iterators, also to match (1). 3. Finally, this CL adds a sync iterable version of the test added in https://crrev.com/c/6199630. The test scenario is: you subscribe to a sync iterable with an unaborted signal that gets aborted while obtaining the iterator (just like (2)), BUT while getting the iterator, an error is thrown. The tests asserts that the error is reported to the global before we consult the aborted signal and stop the subscription process. This ensures that the exception is not swallowed, but is appropriately surfaced, even though the subscription is aborted. This corresponds with the spec PR: https://github.com/WICG/observable/pull/192. R=masonf Bug: 363015168 Change-Id: Ida605c49a2d73cd407a9dc3c392d6b2f338855be Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6202182 Commit-Queue: Dominic Farolino Reviewed-by: Mason Freed Cr-Commit-Position: refs/heads/main@{#1412315} -- wpt-commits: ea15691f277a5e965d90f9c0167638559ff62f0d wpt-pr: 50338 --- .../tentative/observable-from.any.js | 186 ++++++++++++------ 1 file changed, 128 insertions(+), 58 deletions(-) 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 index 7819bd43586aa..e204ece52da34 100644 --- a/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js +++ b/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js @@ -1437,90 +1437,126 @@ promise_test(async t => { test(() => { const results = []; const iterable = { - impl() { - return { - next() { - results.push('next() running'); - return {done: true}; - } + getter() { + results.push('GETTER called'); + return () => { + results.push('Obtaining iterator'); + return { + next() { + results.push('next() running'); + return {done: true}; + } + }; }; } }; - iterable[Symbol.iterator] = iterable.impl; + 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, []); + assert_array_equals(results, ["GETTER called"]); } iterable[Symbol.iterator] = undefined; - iterable[Symbol.asyncIterator] = iterable.impl; + 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, []); + assert_array_equals(results, ["GETTER called", "GETTER called"]); } }, "from(): Subscribing to an iterable Observable with an aborted signal " + "does not call next()"); test(() => { - const results = []; - const ac = new AbortController(); + let results = []; const iterable = { - [Symbol.iterator]() { - ac.abort(); - return { - val: 0, - next() { - results.push('next() called'); - return {done: true}; - }, - return() { - results.push('return() called'); - } + 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'); + } + }; }; } - }; + }; - const source = Observable.from(iterable); - source.subscribe({ - next: v => results.push(v), - complete: () => results.push('complete'), - }, {signal: ac.signal}); + // Test for sync iterators. + { + const ac = new AbortController(); + iterable.controller = ac; + Object.defineProperty(iterable, Symbol.iterator, { + get: iterable.getter, + }); - assert_array_equals(results, []); -}, "from(): When iterable conversion aborts the subscription, next() is " + - "never called"); -test(() => { - const results = []; - const ac = new AbortController(); + 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"]); - const iterable = { - [Symbol.asyncIterator]() { - ac.abort(); - return { - val: 0, - next() { - results.push('next() called'); - return {done: true}; - }, - return() { - results.push('return() 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"]); + } - const source = Observable.from(iterable); - source.subscribe({ - next: v => results.push(v), - complete: () => results.push('complete'), - }, {signal: ac.signal}); + results = []; - assert_array_equals(results, []); -}, "from(): When async iterable conversion aborts the subscription, next() " + - "is never called"); + // 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 @@ -1659,3 +1695,37 @@ test(() => { 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");