feat: support AbortController in waitForSelector (#10018)

This commit is contained in:
Alex Rudenko 2023-04-18 18:45:10 +02:00 committed by GitHub
parent fd08e6ad22
commit 9109b76276
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 81 additions and 7 deletions

View File

@ -8,6 +8,7 @@ sidebar_label: API
| Class | Description | | 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). | | [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). | | [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. | | [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. |

View File

@ -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).

View File

@ -12,8 +12,9 @@ export interface WaitForSelectorOptions
## Properties ## Properties
| Property | Modifiers | Type | Description | Default | | Property | Modifiers | Type | Description | Default |
| -------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | | --------------- | --------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| hidden | <code>optional</code> | boolean | Wait for the selected element to not be found in the DOM or to be hidden, i.e. have <code>display: none</code> or <code>visibility: hidden</code> CSS properties. | <code>false</code> | | abortController | <code>optional</code> | AbortController | Provide an abort controller to cancel a waitForSelector call. | |
| timeout | <code>optional</code> | number | <p>Maximum time to wait in milliseconds. Pass <code>0</code> to disable timeout.</p><p>The default value can be changed by using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md)</p> | <code>30_000</code> (30 seconds) | | hidden | <code>optional</code> | boolean | Wait for the selected element to not be found in the DOM or to be hidden, i.e. have <code>display: none</code> or <code>visibility: hidden</code> CSS properties. | <code>false</code> |
| visible | <code>optional</code> | boolean | Wait for the selected element to be present in DOM and to be visible, i.e. to not have <code>display: none</code> or <code>visibility: hidden</code> CSS properties. | <code>false</code> | | timeout | <code>optional</code> | number | <p>Maximum time to wait in milliseconds. Pass <code>0</code> to disable timeout.</p><p>The default value can be changed by using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md)</p> | <code>30_000</code> (30 seconds) |
| visible | <code>optional</code> | boolean | Wait for the selected element to be present in DOM and to be visible, i.e. to not have <code>display: none</code> or <code>visibility: hidden</code> CSS properties. | <code>false</code> |

View File

@ -42,6 +42,17 @@ export class CustomError extends Error {
*/ */
export class TimeoutError extends CustomError {} 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. * ProtocolError is emitted whenever there is an error from the protocol.
* *

View File

@ -72,6 +72,10 @@ export interface WaitForSelectorOptions {
* @defaultValue `30_000` (30 seconds) * @defaultValue `30_000` (30 seconds)
*/ */
timeout?: number; timeout?: number;
/**
* Provide an abort controller to cancel a waitForSelector call.
*/
abortController?: AbortController;
} }
/** /**
@ -431,6 +435,7 @@ export class IsolatedWorld {
polling?: 'raf' | 'mutation' | number; polling?: 'raf' | 'mutation' | number;
timeout?: number; timeout?: number;
root?: ElementHandle<Node>; root?: ElementHandle<Node>;
abortController?: AbortController;
} = {}, } = {},
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
@ -438,6 +443,7 @@ export class IsolatedWorld {
polling = 'raf', polling = 'raf',
timeout = this.#timeoutSettings.timeout(), timeout = this.#timeoutSettings.timeout(),
root, root,
abortController,
} = options; } = options;
if (typeof polling === 'number' && polling < 0) { if (typeof polling === 'number' && polling < 0) {
throw new Error('Cannot poll with non-positive interval'); throw new Error('Cannot poll with non-positive interval');
@ -448,6 +454,7 @@ export class IsolatedWorld {
polling, polling,
root, root,
timeout, timeout,
abortController,
}, },
pageFunction as unknown as pageFunction as unknown as
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>) | ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)

View File

@ -20,6 +20,7 @@ import {assert} from '../util/assert.js';
import {isErrorLike} from '../util/ErrorLike.js'; import {isErrorLike} from '../util/ErrorLike.js';
import {interpolateFunction, stringifyFunction} from '../util/Function.js'; import {interpolateFunction, stringifyFunction} from '../util/Function.js';
import {AbortError} from './Errors.js';
import type {Frame} from './Frame.js'; import type {Frame} from './Frame.js';
import {transposeIterableHandle} from './HandleIterator.js'; import {transposeIterableHandle} from './HandleIterator.js';
import type {WaitForSelectorOptions} from './IsolatedWorld.js'; import type {WaitForSelectorOptions} from './IsolatedWorld.js';
@ -166,9 +167,13 @@ export class QueryHandler {
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame); element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
} }
const {visible = false, hidden = false, timeout} = options; const {visible = false, hidden = false, timeout, abortController} = options;
try { try {
if (options.abortController?.signal.aborted) {
throw new AbortError('QueryHander.waitFor has been aborted.');
}
const handle = await frame.worlds[PUPPETEER_WORLD].waitForFunction( const handle = await frame.worlds[PUPPETEER_WORLD].waitForFunction(
async (PuppeteerUtil, query, selector, root, visible) => { async (PuppeteerUtil, query, selector, root, visible) => {
const querySelector = PuppeteerUtil.createFunction( const querySelector = PuppeteerUtil.createFunction(
@ -185,6 +190,7 @@ export class QueryHandler {
polling: visible || hidden ? 'raf' : 'mutation', polling: visible || hidden ? 'raf' : 'mutation',
root: element, root: element,
timeout, timeout,
abortController,
}, },
LazyArg.create(context => { LazyArg.create(context => {
return context.puppeteerUtil; return context.puppeteerUtil;
@ -195,6 +201,11 @@ export class QueryHandler {
visible ? true : hidden ? false : undefined 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)) { if (!(handle instanceof ElementHandle)) {
await handle.dispose(); await handle.dispose();
return null; return null;

View File

@ -20,7 +20,7 @@ import type {Poller} from '../injected/Poller.js';
import {createDeferredPromise} from '../util/DeferredPromise.js'; import {createDeferredPromise} from '../util/DeferredPromise.js';
import {stringifyFunction} from '../util/Function.js'; import {stringifyFunction} from '../util/Function.js';
import {TimeoutError} from './Errors.js'; import {TimeoutError, AbortError} from './Errors.js';
import {IsolatedWorld} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
import {LazyArg} from './LazyArg.js'; import {LazyArg} from './LazyArg.js';
import {HandleFor} from './types.js'; import {HandleFor} from './types.js';
@ -32,6 +32,7 @@ export interface WaitTaskOptions {
polling: 'raf' | 'mutation' | number; polling: 'raf' | 'mutation' | number;
root?: ElementHandle<Node>; root?: ElementHandle<Node>;
timeout: number; timeout: number;
abortController?: AbortController;
} }
/** /**
@ -50,6 +51,7 @@ export class WaitTask<T = unknown> {
#result = createDeferredPromise<HandleFor<T>>(); #result = createDeferredPromise<HandleFor<T>>();
#poller?: JSHandle<Poller<T>>; #poller?: JSHandle<Poller<T>>;
#abortController?: AbortController;
constructor( constructor(
world: IsolatedWorld, world: IsolatedWorld,
@ -60,6 +62,16 @@ export class WaitTask<T = unknown> {
this.#world = world; this.#world = world;
this.#polling = options.polling; this.#polling = options.polling;
this.#root = options.root; 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) { switch (typeof fn) {
case 'string': case 'string':

View File

@ -380,6 +380,18 @@ describe('waittask specs', function () {
await frame.waitForSelector('div'); 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 () => { it('should work with removed MutationObserver', async () => {
const {page} = getTestState(); const {page} = getTestState();