From 9109b76276c9d86a2c521c72fc5b7189979279ca Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Tue, 18 Apr 2023 18:45:10 +0200 Subject: [PATCH] feat: support AbortController in waitForSelector (#10018) --- docs/api/index.md | 1 + docs/api/puppeteer.aborterror.md | 19 +++++++++++++++++++ docs/api/puppeteer.waitforselectoroptions.md | 11 ++++++----- packages/puppeteer-core/src/common/Errors.ts | 11 +++++++++++ .../src/common/IsolatedWorld.ts | 7 +++++++ .../puppeteer-core/src/common/QueryHandler.ts | 13 ++++++++++++- .../puppeteer-core/src/common/WaitTask.ts | 14 +++++++++++++- test/src/waittask.spec.ts | 12 ++++++++++++ 8 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 docs/api/puppeteer.aborterror.md diff --git a/docs/api/index.md b/docs/api/index.md index e90971ab..08fda59a 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -8,6 +8,7 @@ sidebar_label: API | Class | Description | | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [AbortError](./puppeteer.aborterror.md) | AbortError is emitted whenever certain operations are terminated due to an abort request. | | [Accessibility](./puppeteer.accessibility.md) | The Accessibility class provides methods for inspecting Chromium's accessibility tree. The accessibility tree is used by assistive technology such as [screen readers](https://en.wikipedia.org/wiki/Screen_reader) or [switches](https://en.wikipedia.org/wiki/Switch_access). | | [Browser](./puppeteer.browser.md) | A Browser is created when Puppeteer connects to a Chromium instance, either through [PuppeteerNode.launch()](./puppeteer.puppeteernode.launch.md) or [Puppeteer.connect()](./puppeteer.puppeteer.connect.md). | | [BrowserContext](./puppeteer.browsercontext.md) | BrowserContexts provide a way to operate multiple independent browser sessions. When a browser is launched, it has a single BrowserContext used by default. The method [Browser.newPage](./puppeteer.browser.newpage.md) creates a page in the default browser context. | diff --git a/docs/api/puppeteer.aborterror.md b/docs/api/puppeteer.aborterror.md new file mode 100644 index 00000000..8510e82d --- /dev/null +++ b/docs/api/puppeteer.aborterror.md @@ -0,0 +1,19 @@ +--- +sidebar_label: AbortError +--- + +# AbortError class + +AbortError is emitted whenever certain operations are terminated due to an abort request. + +#### Signature: + +```typescript +export declare class AbortError extends CustomError +``` + +**Extends:** [CustomError](./puppeteer.customerror.md) + +## Remarks + +Example operations are [page.waitForSelector](./puppeteer.page.waitforselector.md). diff --git a/docs/api/puppeteer.waitforselectoroptions.md b/docs/api/puppeteer.waitforselectoroptions.md index 59757785..b8f3f3b3 100644 --- a/docs/api/puppeteer.waitforselectoroptions.md +++ b/docs/api/puppeteer.waitforselectoroptions.md @@ -12,8 +12,9 @@ export interface WaitForSelectorOptions ## Properties -| Property | Modifiers | Type | Description | Default | -| -------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | -| hidden | optional | boolean | Wait for the selected element to not be found in the DOM or to be hidden, i.e. have display: none or visibility: hidden CSS properties. | false | -| timeout | optional | number |

Maximum time to wait in milliseconds. Pass 0 to disable timeout.

The default value can be changed by using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md)

| 30_000 (30 seconds) | -| visible | optional | boolean | Wait for the selected element to be present in DOM and to be visible, i.e. to not have display: none or visibility: hidden CSS properties. | false | +| Property | Modifiers | Type | Description | Default | +| --------------- | --------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | +| abortController | optional | AbortController | Provide an abort controller to cancel a waitForSelector call. | | +| hidden | optional | boolean | Wait for the selected element to not be found in the DOM or to be hidden, i.e. have display: none or visibility: hidden CSS properties. | false | +| timeout | optional | number |

Maximum time to wait in milliseconds. Pass 0 to disable timeout.

The default value can be changed by using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md)

| 30_000 (30 seconds) | +| visible | optional | boolean | Wait for the selected element to be present in DOM and to be visible, i.e. to not have display: none or visibility: hidden CSS properties. | false | diff --git a/packages/puppeteer-core/src/common/Errors.ts b/packages/puppeteer-core/src/common/Errors.ts index 4d067c89..a4b7c930 100644 --- a/packages/puppeteer-core/src/common/Errors.ts +++ b/packages/puppeteer-core/src/common/Errors.ts @@ -42,6 +42,17 @@ export class CustomError extends Error { */ export class TimeoutError extends CustomError {} +/** + * AbortError is emitted whenever certain operations are terminated due to + * an abort request. + * + * @remarks + * Example operations are {@link Page.waitForSelector | page.waitForSelector}. + * + * @public + */ +export class AbortError extends CustomError {} + /** * ProtocolError is emitted whenever there is an error from the protocol. * diff --git a/packages/puppeteer-core/src/common/IsolatedWorld.ts b/packages/puppeteer-core/src/common/IsolatedWorld.ts index 9b1bda7e..1e1e6450 100644 --- a/packages/puppeteer-core/src/common/IsolatedWorld.ts +++ b/packages/puppeteer-core/src/common/IsolatedWorld.ts @@ -72,6 +72,10 @@ export interface WaitForSelectorOptions { * @defaultValue `30_000` (30 seconds) */ timeout?: number; + /** + * Provide an abort controller to cancel a waitForSelector call. + */ + abortController?: AbortController; } /** @@ -431,6 +435,7 @@ export class IsolatedWorld { polling?: 'raf' | 'mutation' | number; timeout?: number; root?: ElementHandle; + abortController?: AbortController; } = {}, ...args: Params ): Promise>>> { @@ -438,6 +443,7 @@ export class IsolatedWorld { polling = 'raf', timeout = this.#timeoutSettings.timeout(), root, + abortController, } = options; if (typeof polling === 'number' && polling < 0) { throw new Error('Cannot poll with non-positive interval'); @@ -448,6 +454,7 @@ export class IsolatedWorld { polling, root, timeout, + abortController, }, pageFunction as unknown as | ((...args: unknown[]) => Promise>>) diff --git a/packages/puppeteer-core/src/common/QueryHandler.ts b/packages/puppeteer-core/src/common/QueryHandler.ts index 21848369..1c174a2b 100644 --- a/packages/puppeteer-core/src/common/QueryHandler.ts +++ b/packages/puppeteer-core/src/common/QueryHandler.ts @@ -20,6 +20,7 @@ import {assert} from '../util/assert.js'; import {isErrorLike} from '../util/ErrorLike.js'; import {interpolateFunction, stringifyFunction} from '../util/Function.js'; +import {AbortError} from './Errors.js'; import type {Frame} from './Frame.js'; import {transposeIterableHandle} from './HandleIterator.js'; import type {WaitForSelectorOptions} from './IsolatedWorld.js'; @@ -166,9 +167,13 @@ export class QueryHandler { element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame); } - const {visible = false, hidden = false, timeout} = options; + const {visible = false, hidden = false, timeout, abortController} = options; try { + if (options.abortController?.signal.aborted) { + throw new AbortError('QueryHander.waitFor has been aborted.'); + } + const handle = await frame.worlds[PUPPETEER_WORLD].waitForFunction( async (PuppeteerUtil, query, selector, root, visible) => { const querySelector = PuppeteerUtil.createFunction( @@ -185,6 +190,7 @@ export class QueryHandler { polling: visible || hidden ? 'raf' : 'mutation', root: element, timeout, + abortController, }, LazyArg.create(context => { return context.puppeteerUtil; @@ -195,6 +201,11 @@ export class QueryHandler { visible ? true : hidden ? false : undefined ); + if (options.abortController?.signal.aborted) { + await handle.dispose(); + throw new AbortError('QueryHander.waitFor has been aborted.'); + } + if (!(handle instanceof ElementHandle)) { await handle.dispose(); return null; diff --git a/packages/puppeteer-core/src/common/WaitTask.ts b/packages/puppeteer-core/src/common/WaitTask.ts index 2a44a740..d88ab59f 100644 --- a/packages/puppeteer-core/src/common/WaitTask.ts +++ b/packages/puppeteer-core/src/common/WaitTask.ts @@ -20,7 +20,7 @@ import type {Poller} from '../injected/Poller.js'; import {createDeferredPromise} from '../util/DeferredPromise.js'; import {stringifyFunction} from '../util/Function.js'; -import {TimeoutError} from './Errors.js'; +import {TimeoutError, AbortError} from './Errors.js'; import {IsolatedWorld} from './IsolatedWorld.js'; import {LazyArg} from './LazyArg.js'; import {HandleFor} from './types.js'; @@ -32,6 +32,7 @@ export interface WaitTaskOptions { polling: 'raf' | 'mutation' | number; root?: ElementHandle; timeout: number; + abortController?: AbortController; } /** @@ -50,6 +51,7 @@ export class WaitTask { #result = createDeferredPromise>(); #poller?: JSHandle>; + #abortController?: AbortController; constructor( world: IsolatedWorld, @@ -60,6 +62,16 @@ export class WaitTask { this.#world = world; this.#polling = options.polling; this.#root = options.root; + this.#abortController = options.abortController; + this.#abortController?.signal?.addEventListener( + 'abort', + () => { + this.terminate(new AbortError('WaitTask has been aborted.')); + }, + { + once: true, + } + ); switch (typeof fn) { case 'string': diff --git a/test/src/waittask.spec.ts b/test/src/waittask.spec.ts index c6cc0fc8..f5abb7c2 100644 --- a/test/src/waittask.spec.ts +++ b/test/src/waittask.spec.ts @@ -380,6 +380,18 @@ describe('waittask specs', function () { await frame.waitForSelector('div'); }); + it('should be cancellable', async () => { + const {page, server} = getTestState(); + + await page.goto(server.EMPTY_PAGE); + const abortController = new AbortController(); + const task = page.waitForSelector('wrong', { + abortController, + }); + abortController.abort(); + expect(task).rejects.toThrow(/aborted/); + }); + it('should work with removed MutationObserver', async () => { const {page} = getTestState();