mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
chore: use injections for wait tasks (#8943)
This PR refactors wait tasks to use injected scripts.
This commit is contained in:
parent
8d5097d7f6
commit
7c4f41fadc
@ -12,7 +12,7 @@ export interface FrameWaitForFunctionOptions
|
|||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
| Property | Modifiers | Type | Description |
|
| Property | Modifiers | Type | Description |
|
||||||
| -------------------------------------------------------------- | --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------------------------------------------- | --------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| [polling?](./puppeteer.framewaitforfunctionoptions.polling.md) | | string \| number | <p><i>(Optional)</i> An interval at which the <code>pageFunction</code> is executed, defaults to <code>raf</code>. If <code>polling</code> is a number, then it is treated as an interval in milliseconds at which the function would be executed. If <code>polling</code> is a string, then it can be one of the following values:</p><p>- <code>raf</code> - to constantly execute <code>pageFunction</code> in <code>requestAnimationFrame</code> callback. This is the tightest polling mode which is suitable to observe styling changes.</p><p>- <code>mutation</code> - to execute <code>pageFunction</code> on every DOM mutation.</p> |
|
| [polling?](./puppeteer.framewaitforfunctionoptions.polling.md) | | 'raf' \| 'mutation' \| number | <p><i>(Optional)</i> An interval at which the <code>pageFunction</code> is executed, defaults to <code>raf</code>. If <code>polling</code> is a number, then it is treated as an interval in milliseconds at which the function would be executed. If <code>polling</code> is a string, then it can be one of the following values:</p><p>- <code>raf</code> - to constantly execute <code>pageFunction</code> in <code>requestAnimationFrame</code> callback. This is the tightest polling mode which is suitable to observe styling changes.</p><p>- <code>mutation</code> - to execute <code>pageFunction</code> on every DOM mutation.</p> |
|
||||||
| [timeout?](./puppeteer.framewaitforfunctionoptions.timeout.md) | | number | <i>(Optional)</i> Maximum time to wait in milliseconds. Defaults to <code>30000</code> (30 seconds). Pass <code>0</code> to disable the timeout. Puppeteer's default timeout can be changed using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md). |
|
| [timeout?](./puppeteer.framewaitforfunctionoptions.timeout.md) | | number | <i>(Optional)</i> Maximum time to wait in milliseconds. Defaults to <code>30000</code> (30 seconds). Pass <code>0</code> to disable the timeout. Puppeteer's default timeout can be changed using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md). |
|
||||||
|
@ -14,6 +14,6 @@ An interval at which the `pageFunction` is executed, defaults to `raf`. If `poll
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface FrameWaitForFunctionOptions {
|
interface FrameWaitForFunctionOptions {
|
||||||
polling?: string | number;
|
polling?: 'raf' | 'mutation' | number;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -8,7 +8,7 @@ sidebar_label: InnerParams
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export declare type InnerParams<T extends unknown[]> = {
|
export declare type InnerParams<T extends unknown[]> = {
|
||||||
[K in keyof T]: FlattenHandle<T[K]>;
|
[K in keyof T]: FlattenHandle<FlattenLazyArg<FlattenHandle<T[K]>>>;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -15,10 +15,7 @@ class Page {
|
|||||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||||
>(
|
>(
|
||||||
pageFunction: Func | string,
|
pageFunction: Func | string,
|
||||||
options?: {
|
options?: FrameWaitForFunctionOptions,
|
||||||
timeout?: number;
|
|
||||||
polling?: string | number;
|
|
||||||
},
|
|
||||||
...args: Params
|
...args: Params
|
||||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
|
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
|
||||||
}
|
}
|
||||||
@ -26,18 +23,16 @@ class Page {
|
|||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| ------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||||
| pageFunction | Func \| string | Function to be evaluated in browser context |
|
| pageFunction | Func \| string | Function to be evaluated in browser context |
|
||||||
| options | { timeout?: number; polling?: string \| number; } | <p><i>(Optional)</i> Optional waiting parameters</p><p>- <code>polling</code> - An interval at which the <code>pageFunction</code> is executed, defaults to <code>raf</code>. If <code>polling</code> is a number, then it is treated as an interval in milliseconds at which the function would be executed. If polling is a string, then it can be one of the following values: - <code>raf</code> - to constantly execute <code>pageFunction</code> in <code>requestAnimationFrame</code> callback. This is the tightest polling mode which is suitable to observe styling changes. - <code>mutation</code>- to execute pageFunction on every DOM mutation. - <code>timeout</code> - maximum time to wait for in milliseconds. Defaults to <code>30000</code> (30 seconds). Pass <code>0</code> to disable timeout. The default value can be changed by using the [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md) method.</p> |
|
| options | [FrameWaitForFunctionOptions](./puppeteer.framewaitforfunctionoptions.md) | <i>(Optional)</i> Options for configuring waiting behavior. |
|
||||||
| args | Params | Arguments to pass to <code>pageFunction</code> |
|
| args | Params | |
|
||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
|
|
||||||
Promise<[HandleFor](./puppeteer.handlefor.md)<Awaited<ReturnType<Func>>>>
|
Promise<[HandleFor](./puppeteer.handlefor.md)<Awaited<ReturnType<Func>>>>
|
||||||
|
|
||||||
A `Promise` which resolves to a JSHandle/ElementHandle of the the `pageFunction`'s return value.
|
|
||||||
|
|
||||||
## Example 1
|
## Example 1
|
||||||
|
|
||||||
The [Page.waitForFunction()](./puppeteer.page.waitforfunction.md) can be used to observe viewport size change:
|
The [Page.waitForFunction()](./puppeteer.page.waitforfunction.md) can be used to observe viewport size change:
|
||||||
|
@ -19,7 +19,7 @@ import {assert} from '../util/assert.js';
|
|||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {ElementHandle} from './ElementHandle.js';
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
import {Frame} from './Frame.js';
|
import {Frame} from './Frame.js';
|
||||||
import {MAIN_WORLD, PageBinding, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
|
||||||
import {InternalQueryHandler} from './QueryHandler.js';
|
import {InternalQueryHandler} from './QueryHandler.js';
|
||||||
|
|
||||||
async function queryAXTree(
|
async function queryAXTree(
|
||||||
@ -121,21 +121,20 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
|||||||
frame = elementOrFrame.frame;
|
frame = elementOrFrame.frame;
|
||||||
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
|
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
|
||||||
}
|
}
|
||||||
const binding: PageBinding = {
|
|
||||||
name: 'ariaQuerySelector',
|
const ariaQuerySelector = async (selector: string) => {
|
||||||
pptrFunction: async (selector: string) => {
|
const id = await queryOneId(
|
||||||
const id = await queryOneId(
|
element || (await frame.worlds[PUPPETEER_WORLD].document()),
|
||||||
element || (await frame.worlds[PUPPETEER_WORLD].document()),
|
selector
|
||||||
selector
|
);
|
||||||
);
|
if (!id) {
|
||||||
if (!id) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
|
||||||
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
|
id
|
||||||
id
|
)) as ElementHandle<Node>;
|
||||||
)) as ElementHandle<Node>;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
|
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
|
||||||
(_: Element, selector: string) => {
|
(_: Element, selector: string) => {
|
||||||
return (
|
return (
|
||||||
@ -147,16 +146,13 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
|||||||
element,
|
element,
|
||||||
selector,
|
selector,
|
||||||
options,
|
options,
|
||||||
binding
|
new Set([ariaQuerySelector])
|
||||||
);
|
);
|
||||||
if (element) {
|
if (element) {
|
||||||
await element.dispose();
|
await element.dispose();
|
||||||
}
|
}
|
||||||
if (!result) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!(result instanceof ElementHandle)) {
|
if (!(result instanceof ElementHandle)) {
|
||||||
await result.dispose();
|
await result?.dispose();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return result.frame.worlds[MAIN_WORLD].transferHandle(result);
|
return result.frame.worlds[MAIN_WORLD].transferHandle(result);
|
||||||
|
@ -18,6 +18,7 @@ import {Protocol} from 'devtools-protocol';
|
|||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {IsolatedWorld} from './IsolatedWorld.js';
|
import {IsolatedWorld} from './IsolatedWorld.js';
|
||||||
import {JSHandle} from './JSHandle.js';
|
import {JSHandle} from './JSHandle.js';
|
||||||
|
import {LazyArg} from './LazyArg.js';
|
||||||
import {EvaluateFunc, HandleFor} from './types.js';
|
import {EvaluateFunc, HandleFor} from './types.js';
|
||||||
import {
|
import {
|
||||||
createJSHandle,
|
createJSHandle,
|
||||||
@ -273,7 +274,7 @@ export class ExecutionContext {
|
|||||||
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
|
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: functionText + '\n' + suffix + '\n',
|
functionDeclaration: functionText + '\n' + suffix + '\n',
|
||||||
executionContextId: this._contextId,
|
executionContextId: this._contextId,
|
||||||
arguments: args.map(convertArgument.bind(this)),
|
arguments: await Promise.all(args.map(convertArgument.bind(this))),
|
||||||
returnByValue,
|
returnByValue,
|
||||||
awaitPromise: true,
|
awaitPromise: true,
|
||||||
userGesture: true,
|
userGesture: true,
|
||||||
@ -298,10 +299,13 @@ export class ExecutionContext {
|
|||||||
? valueFromRemoteObject(remoteObject)
|
? valueFromRemoteObject(remoteObject)
|
||||||
: createJSHandle(this, remoteObject);
|
: createJSHandle(this, remoteObject);
|
||||||
|
|
||||||
function convertArgument(
|
async function convertArgument(
|
||||||
this: ExecutionContext,
|
this: ExecutionContext,
|
||||||
arg: unknown
|
arg: unknown
|
||||||
): Protocol.Runtime.CallArgument {
|
): Promise<Protocol.Runtime.CallArgument> {
|
||||||
|
if (arg instanceof LazyArg) {
|
||||||
|
arg = await arg.get();
|
||||||
|
}
|
||||||
if (typeof arg === 'bigint') {
|
if (typeof arg === 'bigint') {
|
||||||
// eslint-disable-line valid-typeof
|
// eslint-disable-line valid-typeof
|
||||||
return {unserializableValue: `${arg.toString()}n`};
|
return {unserializableValue: `${arg.toString()}n`};
|
||||||
|
@ -36,7 +36,7 @@ export interface FrameWaitForFunctionOptions {
|
|||||||
*
|
*
|
||||||
* - `mutation` - to execute `pageFunction` on every DOM mutation.
|
* - `mutation` - to execute `pageFunction` on every DOM mutation.
|
||||||
*/
|
*/
|
||||||
polling?: string | number;
|
polling?: 'raf' | 'mutation' | number;
|
||||||
/**
|
/**
|
||||||
* Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
|
* Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
|
||||||
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
|
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
|
||||||
@ -664,7 +664,6 @@ export class Frame {
|
|||||||
options: FrameWaitForFunctionOptions = {},
|
options: FrameWaitForFunctionOptions = {},
|
||||||
...args: Params
|
...args: Params
|
||||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||||
// TODO: Fix when NodeHandle has been added.
|
|
||||||
return this.worlds[MAIN_WORLD].waitForFunction(
|
return this.worlds[MAIN_WORLD].waitForFunction(
|
||||||
pageFunction,
|
pageFunction,
|
||||||
options,
|
options,
|
||||||
|
@ -19,37 +19,20 @@ import {source as injectedSource} from '../generated/injected.js';
|
|||||||
import type PuppeteerUtil from '../injected/injected.js';
|
import type PuppeteerUtil from '../injected/injected.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||||
|
import {isErrorLike} from '../util/ErrorLike.js';
|
||||||
import {CDPSession} from './Connection.js';
|
import {CDPSession} from './Connection.js';
|
||||||
import {ElementHandle} from './ElementHandle.js';
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
import {TimeoutError} from './Errors.js';
|
|
||||||
import {ExecutionContext} from './ExecutionContext.js';
|
import {ExecutionContext} from './ExecutionContext.js';
|
||||||
import {Frame} from './Frame.js';
|
import {Frame} from './Frame.js';
|
||||||
import {FrameManager} from './FrameManager.js';
|
import {FrameManager} from './FrameManager.js';
|
||||||
import {MouseButton} from './Input.js';
|
import {MouseButton} from './Input.js';
|
||||||
import {JSHandle} from './JSHandle.js';
|
import {JSHandle} from './JSHandle.js';
|
||||||
|
import {LazyArg} from './LazyArg.js';
|
||||||
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
|
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
|
||||||
import {TimeoutSettings} from './TimeoutSettings.js';
|
import {TimeoutSettings} from './TimeoutSettings.js';
|
||||||
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
|
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
|
||||||
import {
|
import {createJSHandle, debugError, pageBindingInitString} from './util.js';
|
||||||
createJSHandle,
|
import {TaskManager, WaitTask} from './WaitTask.js';
|
||||||
debugError,
|
|
||||||
isNumber,
|
|
||||||
isString,
|
|
||||||
makePredicateString,
|
|
||||||
pageBindingInitString,
|
|
||||||
} from './util.js';
|
|
||||||
|
|
||||||
// predicateQueryHandler and checkWaitForOptions are declared here so that
|
|
||||||
// TypeScript knows about them when used in the predicate function below.
|
|
||||||
declare const predicateQueryHandler: (
|
|
||||||
element: Element | Document,
|
|
||||||
selector: string
|
|
||||||
) => Promise<Element | Element[] | NodeListOf<Element>>;
|
|
||||||
declare const checkWaitForOptions: (
|
|
||||||
node: Node | null,
|
|
||||||
waitForVisible: boolean,
|
|
||||||
waitForHidden: boolean
|
|
||||||
) => Element | null | boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -124,15 +107,15 @@ export class IsolatedWorld {
|
|||||||
|
|
||||||
// Contains mapping from functions that should be bound to Puppeteer functions.
|
// Contains mapping from functions that should be bound to Puppeteer functions.
|
||||||
#boundFunctions = new Map<string, Function>();
|
#boundFunctions = new Map<string, Function>();
|
||||||
#waitTasks = new Set<WaitTask>();
|
#taskManager = new TaskManager();
|
||||||
#puppeteerUtil = createDeferredPromise<JSHandle<PuppeteerUtil>>();
|
#puppeteerUtil = createDeferredPromise<JSHandle<PuppeteerUtil>>();
|
||||||
|
|
||||||
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
|
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
|
||||||
return this.#puppeteerUtil;
|
return this.#puppeteerUtil;
|
||||||
}
|
}
|
||||||
|
|
||||||
get _waitTasks(): Set<WaitTask> {
|
get taskManager(): TaskManager {
|
||||||
return this.#waitTasks;
|
return this.#taskManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
get _boundFunctions(): Map<string, Function> {
|
get _boundFunctions(): Map<string, Function> {
|
||||||
@ -176,9 +159,7 @@ export class IsolatedWorld {
|
|||||||
this.#injectPuppeteerUtil(context);
|
this.#injectPuppeteerUtil(context);
|
||||||
this.#ctxBindings.clear();
|
this.#ctxBindings.clear();
|
||||||
this.#context.resolve(context);
|
this.#context.resolve(context);
|
||||||
for (const waitTask of this._waitTasks) {
|
this.#taskManager.rerunAll();
|
||||||
waitTask.rerun();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #injectPuppeteerUtil(context: ExecutionContext): Promise<void> {
|
async #injectPuppeteerUtil(context: ExecutionContext): Promise<void> {
|
||||||
@ -186,10 +167,10 @@ export class IsolatedWorld {
|
|||||||
this.#puppeteerUtil.resolve(
|
this.#puppeteerUtil.resolve(
|
||||||
(await context.evaluateHandle(
|
(await context.evaluateHandle(
|
||||||
`(() => {
|
`(() => {
|
||||||
const module = {};
|
const module = {};
|
||||||
${injectedSource}
|
${injectedSource}
|
||||||
return module.exports.default;
|
return module.exports.default;
|
||||||
})()`
|
})()`
|
||||||
)) as JSHandle<PuppeteerUtil>
|
)) as JSHandle<PuppeteerUtil>
|
||||||
);
|
);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@ -204,11 +185,9 @@ export class IsolatedWorld {
|
|||||||
_detach(): void {
|
_detach(): void {
|
||||||
this.#detached = true;
|
this.#detached = true;
|
||||||
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
|
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
|
||||||
for (const waitTask of this._waitTasks) {
|
this.#taskManager.terminateAll(
|
||||||
waitTask.terminate(
|
new Error('waitForFunction failed: frame got detached.')
|
||||||
new Error('waitForFunction failed: frame got detached.')
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
executionContext(): Promise<ExecutionContext> {
|
executionContext(): Promise<ExecutionContext> {
|
||||||
@ -430,8 +409,6 @@ export class IsolatedWorld {
|
|||||||
// TODO: In theory, it would be enough to call this just once
|
// TODO: In theory, it would be enough to call this just once
|
||||||
await context._client.send('Runtime.addBinding', {
|
await context._client.send('Runtime.addBinding', {
|
||||||
name,
|
name,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore The protocol definition is not up to date.
|
|
||||||
executionContextName: context._contextName,
|
executionContextName: context._contextName,
|
||||||
});
|
});
|
||||||
await context.evaluate(expression);
|
await context.evaluate(expression);
|
||||||
@ -439,18 +416,19 @@ export class IsolatedWorld {
|
|||||||
// We could have tried to evaluate in a context which was already
|
// We could have tried to evaluate in a context which was already
|
||||||
// destroyed. This happens, for example, if the page is navigated while
|
// destroyed. This happens, for example, if the page is navigated while
|
||||||
// we are trying to add the binding
|
// we are trying to add the binding
|
||||||
const ctxDestroyed = (error as Error).message.includes(
|
if (error instanceof Error) {
|
||||||
'Execution context was destroyed'
|
// Destroyed context.
|
||||||
);
|
if (error.message.includes('Execution context was destroyed')) {
|
||||||
const ctxNotFound = (error as Error).message.includes(
|
return;
|
||||||
'Cannot find context with specified id'
|
}
|
||||||
);
|
// Missing context.
|
||||||
if (ctxDestroyed || ctxNotFound) {
|
if (error.message.includes('Cannot find context with specified id')) {
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
debugError(error);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugError(error);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.#ctxBindings.add(
|
this.#ctxBindings.add(
|
||||||
IsolatedWorld.#bindingIdentifier(name, context._contextId)
|
IsolatedWorld.#bindingIdentifier(name, context._contextId)
|
||||||
@ -495,7 +473,17 @@ export class IsolatedWorld {
|
|||||||
throw new Error(`Bound function $name is not found`);
|
throw new Error(`Bound function $name is not found`);
|
||||||
}
|
}
|
||||||
const result = await fn(...args);
|
const result = await fn(...args);
|
||||||
await context.evaluate(deliverResult, name, seq, result);
|
await context.evaluate(
|
||||||
|
(name: string, seq: number, result: unknown) => {
|
||||||
|
// @ts-expect-error Code is evaluated in a different context.
|
||||||
|
const callbacks = self[name].callbacks;
|
||||||
|
callbacks.get(seq).resolve(result);
|
||||||
|
callbacks.delete(seq);
|
||||||
|
},
|
||||||
|
name,
|
||||||
|
seq,
|
||||||
|
result
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The WaitTask may already have been resolved by timing out, or the
|
// The WaitTask may already have been resolved by timing out, or the
|
||||||
// exection context may have been destroyed.
|
// exection context may have been destroyed.
|
||||||
@ -507,14 +495,6 @@ export class IsolatedWorld {
|
|||||||
}
|
}
|
||||||
debugError(error);
|
debugError(error);
|
||||||
}
|
}
|
||||||
function deliverResult(name: string, seq: number, result: unknown): void {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore Code is evaluated in a different context.
|
|
||||||
(globalThis as any)[name].callbacks.get(seq).resolve(result);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore Code is evaluated in a different context.
|
|
||||||
(globalThis as any)[name].callbacks.delete(seq);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async _waitForSelectorInPage(
|
async _waitForSelectorInPage(
|
||||||
@ -522,59 +502,96 @@ export class IsolatedWorld {
|
|||||||
root: ElementHandle<Node> | undefined,
|
root: ElementHandle<Node> | undefined,
|
||||||
selector: string,
|
selector: string,
|
||||||
options: WaitForSelectorOptions,
|
options: WaitForSelectorOptions,
|
||||||
binding?: PageBinding
|
bindings = new Set<(...args: never[]) => unknown>()
|
||||||
): Promise<JSHandle<unknown> | null> {
|
): Promise<JSHandle<unknown> | null> {
|
||||||
const {
|
const {
|
||||||
visible: waitForVisible = false,
|
visible: waitForVisible = false,
|
||||||
hidden: waitForHidden = false,
|
hidden: waitForHidden = false,
|
||||||
timeout = this.#timeoutSettings.timeout(),
|
timeout = this.#timeoutSettings.timeout(),
|
||||||
} = options;
|
} = options;
|
||||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
|
||||||
const title = `selector \`${selector}\`${
|
try {
|
||||||
waitForHidden ? ' to be hidden' : ''
|
const handle = await this.waitForFunction(
|
||||||
}`;
|
async (PuppeteerUtil, query, selector, root, visible) => {
|
||||||
async function predicate(
|
if (!PuppeteerUtil) {
|
||||||
root: Element | Document,
|
return;
|
||||||
selector: string,
|
}
|
||||||
waitForVisible: boolean,
|
const node = (await PuppeteerUtil.createFunction(query)(
|
||||||
waitForHidden: boolean
|
root || document,
|
||||||
): Promise<Node | null | boolean> {
|
selector
|
||||||
const node = (await predicateQueryHandler(root, selector)) as Element;
|
)) as Node | null;
|
||||||
return checkWaitForOptions(node, waitForVisible, waitForHidden);
|
return PuppeteerUtil.checkVisibility(node, visible);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bindings,
|
||||||
|
polling: waitForVisible || waitForHidden ? 'raf' : 'mutation',
|
||||||
|
root,
|
||||||
|
timeout,
|
||||||
|
},
|
||||||
|
new LazyArg(async () => {
|
||||||
|
try {
|
||||||
|
// In case CDP fails.
|
||||||
|
return await this.puppeteerUtil;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
queryOne.toString(),
|
||||||
|
selector,
|
||||||
|
root,
|
||||||
|
waitForVisible ? true : waitForHidden ? false : undefined
|
||||||
|
);
|
||||||
|
const elementHandle = handle.asElement();
|
||||||
|
if (!elementHandle) {
|
||||||
|
await handle.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return elementHandle;
|
||||||
|
} catch (error) {
|
||||||
|
if (!isErrorLike(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`;
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
const waitTaskOptions: WaitTaskOptions = {
|
|
||||||
isolatedWorld: this,
|
|
||||||
predicateBody: makePredicateString(predicate, queryOne),
|
|
||||||
predicateAcceptsContextElement: true,
|
|
||||||
title,
|
|
||||||
polling,
|
|
||||||
timeout,
|
|
||||||
args: [selector, waitForVisible, waitForHidden],
|
|
||||||
binding,
|
|
||||||
root,
|
|
||||||
};
|
|
||||||
const waitTask = new WaitTask(waitTaskOptions);
|
|
||||||
return waitTask.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForFunction(
|
waitForFunction<
|
||||||
pageFunction: Function | string,
|
Params extends unknown[],
|
||||||
options: {polling?: string | number; timeout?: number} = {},
|
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||||
...args: unknown[]
|
>(
|
||||||
): Promise<JSHandle> {
|
pageFunction: Func | string,
|
||||||
const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} =
|
options: {
|
||||||
options;
|
polling?: 'raf' | 'mutation' | number;
|
||||||
const waitTaskOptions: WaitTaskOptions = {
|
timeout?: number;
|
||||||
isolatedWorld: this,
|
root?: ElementHandle<Node>;
|
||||||
predicateBody: pageFunction,
|
bindings?: Set<(...args: never[]) => unknown>;
|
||||||
predicateAcceptsContextElement: false,
|
} = {},
|
||||||
title: 'function',
|
...args: Params
|
||||||
polling,
|
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||||
timeout,
|
const {
|
||||||
args,
|
polling = 'raf',
|
||||||
};
|
timeout = this.#timeoutSettings.timeout(),
|
||||||
const waitTask = new WaitTask(waitTaskOptions);
|
bindings,
|
||||||
return waitTask.promise;
|
root,
|
||||||
|
} = options;
|
||||||
|
if (typeof polling === 'number' && polling < 0) {
|
||||||
|
throw new Error('Cannot poll with non-positive interval');
|
||||||
|
}
|
||||||
|
const waitTask = new WaitTask(
|
||||||
|
this,
|
||||||
|
{
|
||||||
|
bindings,
|
||||||
|
polling,
|
||||||
|
root,
|
||||||
|
timeout,
|
||||||
|
},
|
||||||
|
pageFunction as unknown as
|
||||||
|
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
|
||||||
|
| string,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
return waitTask.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async title(): Promise<string> {
|
async title(): Promise<string> {
|
||||||
@ -612,315 +629,3 @@ export class IsolatedWorld {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export interface WaitTaskOptions {
|
|
||||||
isolatedWorld: IsolatedWorld;
|
|
||||||
predicateBody: Function | string;
|
|
||||||
predicateAcceptsContextElement: boolean;
|
|
||||||
title: string;
|
|
||||||
polling: string | number;
|
|
||||||
timeout: number;
|
|
||||||
binding?: PageBinding;
|
|
||||||
args: unknown[];
|
|
||||||
root?: ElementHandle<Node>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const noop = (): void => {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export class WaitTask {
|
|
||||||
#isolatedWorld: IsolatedWorld;
|
|
||||||
#polling: 'raf' | 'mutation' | number;
|
|
||||||
#timeout: number;
|
|
||||||
#predicateBody: string;
|
|
||||||
#predicateAcceptsContextElement: boolean;
|
|
||||||
#args: unknown[];
|
|
||||||
#binding?: PageBinding;
|
|
||||||
#runCount = 0;
|
|
||||||
#resolve: (x: JSHandle) => void = noop;
|
|
||||||
#reject: (x: Error) => void = noop;
|
|
||||||
#timeoutTimer?: NodeJS.Timeout;
|
|
||||||
#terminated = false;
|
|
||||||
#root: ElementHandle<Node> | null = null;
|
|
||||||
|
|
||||||
promise: Promise<JSHandle>;
|
|
||||||
|
|
||||||
constructor(options: WaitTaskOptions) {
|
|
||||||
if (isString(options.polling)) {
|
|
||||||
assert(
|
|
||||||
options.polling === 'raf' || options.polling === 'mutation',
|
|
||||||
'Unknown polling option: ' + options.polling
|
|
||||||
);
|
|
||||||
} else if (isNumber(options.polling)) {
|
|
||||||
assert(
|
|
||||||
options.polling > 0,
|
|
||||||
'Cannot poll with non-positive interval: ' + options.polling
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error('Unknown polling options: ' + options.polling);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPredicateBody(predicateBody: Function | string) {
|
|
||||||
if (isString(predicateBody)) {
|
|
||||||
return `return (${predicateBody});`;
|
|
||||||
}
|
|
||||||
return `return (${predicateBody})(...args);`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#isolatedWorld = options.isolatedWorld;
|
|
||||||
this.#polling = options.polling;
|
|
||||||
this.#timeout = options.timeout;
|
|
||||||
this.#root = options.root || null;
|
|
||||||
this.#predicateBody = getPredicateBody(options.predicateBody);
|
|
||||||
this.#predicateAcceptsContextElement =
|
|
||||||
options.predicateAcceptsContextElement;
|
|
||||||
this.#args = options.args;
|
|
||||||
this.#binding = options.binding;
|
|
||||||
this.#runCount = 0;
|
|
||||||
this.#isolatedWorld._waitTasks.add(this);
|
|
||||||
if (this.#binding) {
|
|
||||||
this.#isolatedWorld._boundFunctions.set(
|
|
||||||
this.#binding.name,
|
|
||||||
this.#binding.pptrFunction
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.promise = new Promise<JSHandle>((resolve, reject) => {
|
|
||||||
this.#resolve = resolve;
|
|
||||||
this.#reject = reject;
|
|
||||||
});
|
|
||||||
// Since page navigation requires us to re-install the pageScript, we should track
|
|
||||||
// timeout on our end.
|
|
||||||
if (options.timeout) {
|
|
||||||
const timeoutError = new TimeoutError(
|
|
||||||
`waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`
|
|
||||||
);
|
|
||||||
this.#timeoutTimer = setTimeout(() => {
|
|
||||||
return this.terminate(timeoutError);
|
|
||||||
}, options.timeout);
|
|
||||||
}
|
|
||||||
this.rerun();
|
|
||||||
}
|
|
||||||
|
|
||||||
terminate(error: Error): void {
|
|
||||||
this.#terminated = true;
|
|
||||||
this.#reject(error);
|
|
||||||
this.#cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
async rerun(): Promise<void> {
|
|
||||||
const runCount = ++this.#runCount;
|
|
||||||
let success: JSHandle | null = null;
|
|
||||||
let error: Error | null = null;
|
|
||||||
const context = await this.#isolatedWorld.executionContext();
|
|
||||||
if (this.#terminated || runCount !== this.#runCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.#binding) {
|
|
||||||
await this.#isolatedWorld._addBindingToContext(
|
|
||||||
context,
|
|
||||||
this.#binding.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.#terminated || runCount !== this.#runCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
success = await context.evaluateHandle(
|
|
||||||
waitForPredicatePageFunction,
|
|
||||||
this.#root || null,
|
|
||||||
this.#predicateBody,
|
|
||||||
this.#predicateAcceptsContextElement,
|
|
||||||
this.#polling,
|
|
||||||
this.#timeout,
|
|
||||||
...this.#args
|
|
||||||
);
|
|
||||||
} catch (error_) {
|
|
||||||
error = error_ as Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.#terminated || runCount !== this.#runCount) {
|
|
||||||
if (success) {
|
|
||||||
await success.dispose();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore timeouts in pageScript - we track timeouts ourselves.
|
|
||||||
// If the frame's execution context has already changed, `frame.evaluate` will
|
|
||||||
// throw an error - ignore this predicate run altogether.
|
|
||||||
if (
|
|
||||||
!error &&
|
|
||||||
(await this.#isolatedWorld
|
|
||||||
.evaluate(s => {
|
|
||||||
return !s;
|
|
||||||
}, success)
|
|
||||||
.catch(() => {
|
|
||||||
return true;
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
if (!success) {
|
|
||||||
throw new Error('Assertion: result handle is not available');
|
|
||||||
}
|
|
||||||
await success.dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
if (error.message.includes('TypeError: binding is not a function')) {
|
|
||||||
return this.rerun();
|
|
||||||
}
|
|
||||||
// When frame is detached the task should have been terminated by the IsolatedWorld.
|
|
||||||
// This can fail if we were adding this task while the frame was detached,
|
|
||||||
// so we terminate here instead.
|
|
||||||
if (
|
|
||||||
error.message.includes(
|
|
||||||
'Execution context is not available in detached frame'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.terminate(
|
|
||||||
new Error('waitForFunction failed: frame got detached.')
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the page is navigated, the promise is rejected.
|
|
||||||
// We will try again in the new execution context.
|
|
||||||
if (error.message.includes('Execution context was destroyed')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could have tried to evaluate in a context which was already
|
|
||||||
// destroyed.
|
|
||||||
if (error.message.includes('Cannot find context with specified id')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#reject(error);
|
|
||||||
} else {
|
|
||||||
if (!success) {
|
|
||||||
throw new Error('Assertion: result handle is not available');
|
|
||||||
}
|
|
||||||
this.#resolve(success);
|
|
||||||
}
|
|
||||||
this.#cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
#cleanup(): void {
|
|
||||||
this.#timeoutTimer !== undefined && clearTimeout(this.#timeoutTimer);
|
|
||||||
this.#isolatedWorld._waitTasks.delete(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForPredicatePageFunction(
|
|
||||||
root: Node | null,
|
|
||||||
predicateBody: string,
|
|
||||||
predicateAcceptsContextElement: boolean,
|
|
||||||
polling: 'raf' | 'mutation' | number,
|
|
||||||
timeout: number,
|
|
||||||
...args: unknown[]
|
|
||||||
): Promise<unknown> {
|
|
||||||
root = root || document;
|
|
||||||
const predicate = new Function('...args', predicateBody);
|
|
||||||
let timedOut = false;
|
|
||||||
if (timeout) {
|
|
||||||
setTimeout(() => {
|
|
||||||
return (timedOut = true);
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
switch (polling) {
|
|
||||||
case 'raf':
|
|
||||||
return await pollRaf();
|
|
||||||
case 'mutation':
|
|
||||||
return await pollMutation();
|
|
||||||
default:
|
|
||||||
return await pollInterval(polling);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollMutation(): Promise<unknown> {
|
|
||||||
const success = predicateAcceptsContextElement
|
|
||||||
? await predicate(root, ...args)
|
|
||||||
: await predicate(...args);
|
|
||||||
if (success) {
|
|
||||||
return Promise.resolve(success);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fulfill = (_?: unknown) => {};
|
|
||||||
const result = new Promise(x => {
|
|
||||||
return (fulfill = x);
|
|
||||||
});
|
|
||||||
const observer = new MutationObserver(async () => {
|
|
||||||
if (timedOut) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill();
|
|
||||||
}
|
|
||||||
const success = predicateAcceptsContextElement
|
|
||||||
? await predicate(root, ...args)
|
|
||||||
: await predicate(...args);
|
|
||||||
if (success) {
|
|
||||||
observer.disconnect();
|
|
||||||
fulfill(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!root) {
|
|
||||||
throw new Error('Root element is not found.');
|
|
||||||
}
|
|
||||||
observer.observe(root, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollRaf(): Promise<unknown> {
|
|
||||||
let fulfill = (_?: unknown): void => {};
|
|
||||||
const result = new Promise(x => {
|
|
||||||
return (fulfill = x);
|
|
||||||
});
|
|
||||||
await onRaf();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
async function onRaf(): Promise<void> {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicateAcceptsContextElement
|
|
||||||
? await predicate(root, ...args)
|
|
||||||
: await predicate(...args);
|
|
||||||
if (success) {
|
|
||||||
fulfill(success);
|
|
||||||
} else {
|
|
||||||
requestAnimationFrame(onRaf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollInterval(pollInterval: number): Promise<unknown> {
|
|
||||||
let fulfill = (_?: unknown): void => {};
|
|
||||||
const result = new Promise(x => {
|
|
||||||
return (fulfill = x);
|
|
||||||
});
|
|
||||||
await onTimeout();
|
|
||||||
return result;
|
|
||||||
|
|
||||||
async function onTimeout(): Promise<void> {
|
|
||||||
if (timedOut) {
|
|
||||||
fulfill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = predicateAcceptsContextElement
|
|
||||||
? await predicate(root, ...args)
|
|
||||||
: await predicate(...args);
|
|
||||||
if (success) {
|
|
||||||
fulfill(success);
|
|
||||||
} else {
|
|
||||||
setTimeout(onTimeout, pollInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
13
src/common/LazyArg.ts
Normal file
13
src/common/LazyArg.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class LazyArg<T> {
|
||||||
|
#get: () => Promise<T>;
|
||||||
|
constructor(get: () => Promise<T>) {
|
||||||
|
this.#get = get;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): Promise<T> {
|
||||||
|
return this.#get();
|
||||||
|
}
|
||||||
|
}
|
@ -40,6 +40,7 @@ import {
|
|||||||
Frame,
|
Frame,
|
||||||
FrameAddScriptTagOptions,
|
FrameAddScriptTagOptions,
|
||||||
FrameAddStyleTagOptions,
|
FrameAddStyleTagOptions,
|
||||||
|
FrameWaitForFunctionOptions,
|
||||||
} from './Frame.js';
|
} from './Frame.js';
|
||||||
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
|
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
|
||||||
import {HTTPRequest} from './HTTPRequest.js';
|
import {HTTPRequest} from './HTTPRequest.js';
|
||||||
@ -3564,32 +3565,14 @@ export class Page extends EventEmitter {
|
|||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param pageFunction - Function to be evaluated in browser context
|
* @param pageFunction - Function to be evaluated in browser context
|
||||||
* @param options - Optional waiting parameters
|
* @param options - Options for configuring waiting behavior.
|
||||||
*
|
|
||||||
* - `polling` - An interval at which the `pageFunction` is executed, defaults
|
|
||||||
* to `raf`. If `polling` is a number, then it is treated as an interval in
|
|
||||||
* milliseconds at which the function would be executed. If polling is a
|
|
||||||
* string, then it can be one of the following values:
|
|
||||||
* - `raf` - to constantly execute `pageFunction` in
|
|
||||||
* `requestAnimationFrame` callback. This is the tightest polling mode
|
|
||||||
* which is suitable to observe styling changes.
|
|
||||||
* - `mutation`- to execute pageFunction on every DOM mutation.
|
|
||||||
* - `timeout` - maximum time to wait for in milliseconds. Defaults to `30000`
|
|
||||||
* (30 seconds). Pass `0` to disable timeout. The default value can be
|
|
||||||
* changed by using the {@link Page.setDefaultTimeout} method.
|
|
||||||
* @param args - Arguments to pass to `pageFunction`
|
|
||||||
* @returns A `Promise` which resolves to a JSHandle/ElementHandle of the the
|
|
||||||
* `pageFunction`'s return value.
|
|
||||||
*/
|
*/
|
||||||
waitForFunction<
|
waitForFunction<
|
||||||
Params extends unknown[],
|
Params extends unknown[],
|
||||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||||
>(
|
>(
|
||||||
pageFunction: Func | string,
|
pageFunction: Func | string,
|
||||||
options: {
|
options: FrameWaitForFunctionOptions = {},
|
||||||
timeout?: number;
|
|
||||||
polling?: string | number;
|
|
||||||
} = {},
|
|
||||||
...args: Params
|
...args: Params
|
||||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||||
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
|
||||||
|
241
src/common/WaitTask.ts
Normal file
241
src/common/WaitTask.ts
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import type {Poller} from '../injected/Poller.js';
|
||||||
|
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||||
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
|
import {TimeoutError} from './Errors.js';
|
||||||
|
import {IsolatedWorld} from './IsolatedWorld.js';
|
||||||
|
import {JSHandle} from './JSHandle.js';
|
||||||
|
import {HandleFor} from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface WaitTaskOptions {
|
||||||
|
bindings?: Set<(...args: never[]) => unknown>;
|
||||||
|
polling: 'raf' | 'mutation' | number;
|
||||||
|
root?: ElementHandle<Node>;
|
||||||
|
timeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class WaitTask<T = unknown> {
|
||||||
|
#world: IsolatedWorld;
|
||||||
|
#bindings: Set<(...args: never[]) => unknown>;
|
||||||
|
#polling: 'raf' | 'mutation' | number;
|
||||||
|
#root?: ElementHandle<Node>;
|
||||||
|
|
||||||
|
#fn: string;
|
||||||
|
#args: unknown[];
|
||||||
|
|
||||||
|
#timeout?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
#result = createDeferredPromise<HandleFor<T>>();
|
||||||
|
|
||||||
|
#poller?: JSHandle<Poller<T>>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
world: IsolatedWorld,
|
||||||
|
options: WaitTaskOptions,
|
||||||
|
fn: ((...args: unknown[]) => Promise<T>) | string,
|
||||||
|
...args: unknown[]
|
||||||
|
) {
|
||||||
|
this.#world = world;
|
||||||
|
this.#bindings = options.bindings ?? new Set();
|
||||||
|
this.#polling = options.polling;
|
||||||
|
this.#root = options.root;
|
||||||
|
|
||||||
|
switch (typeof fn) {
|
||||||
|
case 'string':
|
||||||
|
this.#fn = `() => {return (${fn});}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.#fn = fn.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.#args = args;
|
||||||
|
|
||||||
|
this.#world.taskManager.add(this);
|
||||||
|
|
||||||
|
if (options.timeout) {
|
||||||
|
this.#timeout = setTimeout(() => {
|
||||||
|
this.terminate(
|
||||||
|
new TimeoutError(`Waiting failed: ${options.timeout}ms exceeded`)
|
||||||
|
);
|
||||||
|
}, options.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#bindings.size !== 0) {
|
||||||
|
for (const fn of this.#bindings) {
|
||||||
|
this.#world._boundFunctions.set(fn.name, fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rerun();
|
||||||
|
}
|
||||||
|
|
||||||
|
get result(): Promise<HandleFor<T>> {
|
||||||
|
return this.#result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async rerun(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.#bindings.size !== 0) {
|
||||||
|
const context = await this.#world.executionContext();
|
||||||
|
await Promise.all(
|
||||||
|
[...this.#bindings].map(async ({name}) => {
|
||||||
|
return await this.#world._addBindingToContext(context, name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.#polling) {
|
||||||
|
case 'raf':
|
||||||
|
this.#poller = await this.#world.evaluateHandle(
|
||||||
|
({RAFPoller, createFunction}, fn, ...args) => {
|
||||||
|
const fun = createFunction(fn);
|
||||||
|
return new RAFPoller(() => {
|
||||||
|
return fun(...args) as Promise<T>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
await this.#world.puppeteerUtil,
|
||||||
|
this.#fn,
|
||||||
|
...this.#args
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'mutation':
|
||||||
|
this.#poller = await this.#world.evaluateHandle(
|
||||||
|
({MutationPoller, createFunction}, root, fn, ...args) => {
|
||||||
|
const fun = createFunction(fn);
|
||||||
|
return new MutationPoller(() => {
|
||||||
|
return fun(...args) as Promise<T>;
|
||||||
|
}, root || document);
|
||||||
|
},
|
||||||
|
await this.#world.puppeteerUtil,
|
||||||
|
this.#root,
|
||||||
|
this.#fn,
|
||||||
|
...this.#args
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.#poller = await this.#world.evaluateHandle(
|
||||||
|
({IntervalPoller, createFunction}, ms, fn, ...args) => {
|
||||||
|
const fun = createFunction(fn);
|
||||||
|
return new IntervalPoller(() => {
|
||||||
|
return fun(...args) as Promise<T>;
|
||||||
|
}, ms);
|
||||||
|
},
|
||||||
|
await this.#world.puppeteerUtil,
|
||||||
|
this.#polling,
|
||||||
|
this.#fn,
|
||||||
|
...this.#args
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#poller.evaluate(poller => {
|
||||||
|
poller.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await this.#poller.evaluateHandle(poller => {
|
||||||
|
return poller.result();
|
||||||
|
});
|
||||||
|
this.#result.resolve(result);
|
||||||
|
|
||||||
|
await this.terminate();
|
||||||
|
} catch (error) {
|
||||||
|
const badError = this.getBadError(error);
|
||||||
|
if (badError) {
|
||||||
|
await this.terminate(badError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async terminate(error?: unknown): Promise<void> {
|
||||||
|
this.#world.taskManager.delete(this);
|
||||||
|
|
||||||
|
if (this.#timeout) {
|
||||||
|
clearTimeout(this.#timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error && !this.#result.finished()) {
|
||||||
|
this.#result.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#poller) {
|
||||||
|
try {
|
||||||
|
await this.#poller.evaluateHandle(async poller => {
|
||||||
|
await poller.stop();
|
||||||
|
});
|
||||||
|
if (this.#poller) {
|
||||||
|
await this.#poller.dispose();
|
||||||
|
this.#poller = undefined;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors since they most likely come from low-level cleanup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not all errors lead to termination. They usually imply we need to rerun the task.
|
||||||
|
*/
|
||||||
|
getBadError(error: unknown): unknown {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
// When frame is detached the task should have been terminated by the IsolatedWorld.
|
||||||
|
// This can fail if we were adding this task while the frame was detached,
|
||||||
|
// so we terminate here instead.
|
||||||
|
if (
|
||||||
|
error.message.includes(
|
||||||
|
'Execution context is not available in detached frame'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return new Error('Waiting failed: Frame detached');
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the page is navigated, the promise is rejected.
|
||||||
|
// We will try again in the new execution context.
|
||||||
|
if (error.message.includes('Execution context was destroyed')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could have tried to evaluate in a context which was already
|
||||||
|
// destroyed.
|
||||||
|
if (error.message.includes('Cannot find context with specified id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class TaskManager {
|
||||||
|
#tasks: Set<WaitTask> = new Set<WaitTask>();
|
||||||
|
|
||||||
|
add(task: WaitTask<any>): void {
|
||||||
|
this.#tasks.add(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(task: WaitTask<any>): void {
|
||||||
|
this.#tasks.delete(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminateAll(error?: Error): void {
|
||||||
|
for (const task of this.#tasks) {
|
||||||
|
task.terminate(error);
|
||||||
|
}
|
||||||
|
this.#tasks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async rerunAll(): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
[...this.#tasks].map(task => {
|
||||||
|
return task.rerun();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import {JSHandle} from './JSHandle.js';
|
import {JSHandle} from './JSHandle.js';
|
||||||
import {ElementHandle} from './ElementHandle.js';
|
import {ElementHandle} from './ElementHandle.js';
|
||||||
|
import {LazyArg} from './LazyArg.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -36,11 +37,17 @@ export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
|
export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type FlattenLazyArg<T> = T extends LazyArg<infer U> ? U : T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type InnerParams<T extends unknown[]> = {
|
export type InnerParams<T extends unknown[]> = {
|
||||||
[K in keyof T]: FlattenHandle<T[K]>;
|
[K in keyof T]: FlattenHandle<FlattenLazyArg<FlattenHandle<T[K]>>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -249,28 +249,26 @@ export function evaluationString(
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function pageBindingInitString(type: string, name: string): string {
|
export function pageBindingInitString(type: string, name: string): string {
|
||||||
function addPageBinding(type: string, bindingName: string): void {
|
function addPageBinding(type: string, name: string): void {
|
||||||
/* Cast window to any here as we're about to add properties to it
|
// This is the CDP binding.
|
||||||
* via win[bindingName] which TypeScript doesn't like.
|
// @ts-expect-error: In a different context.
|
||||||
*/
|
const callCDP = self[name];
|
||||||
const win = window as any;
|
|
||||||
const binding = win[bindingName];
|
|
||||||
|
|
||||||
win[bindingName] = (...args: unknown[]): Promise<unknown> => {
|
// We replace the CDP binding with a Puppeteer binding.
|
||||||
const me = (window as any)[bindingName];
|
Object.assign(self, {
|
||||||
let callbacks = me.callbacks;
|
[name](...args: unknown[]): Promise<unknown> {
|
||||||
if (!callbacks) {
|
// This is the Puppeteer binding.
|
||||||
callbacks = new Map();
|
// @ts-expect-error: In a different context.
|
||||||
me.callbacks = callbacks;
|
const callPuppeteer = self[name];
|
||||||
}
|
callPuppeteer.callbacks ??= new Map();
|
||||||
const seq = (me.lastSeq || 0) + 1;
|
const seq = (callPuppeteer.lastSeq ?? 0) + 1;
|
||||||
me.lastSeq = seq;
|
callPuppeteer.lastSeq = seq;
|
||||||
const promise = new Promise((resolve, reject) => {
|
callCDP(JSON.stringify({type, name, seq, args}));
|
||||||
return callbacks.set(seq, {resolve, reject});
|
return new Promise((resolve, reject) => {
|
||||||
});
|
callPuppeteer.callbacks.set(seq, {resolve, reject});
|
||||||
binding(JSON.stringify({type, name: bindingName, seq, args}));
|
});
|
||||||
return promise;
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
return evaluationString(addPageBinding, type, name);
|
return evaluationString(addPageBinding, type, name);
|
||||||
}
|
}
|
||||||
@ -328,50 +326,6 @@ export function pageBindingDeliverErrorValueString(
|
|||||||
return evaluationString(deliverErrorValue, name, seq, value);
|
return evaluationString(deliverErrorValue, name, seq, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
export function makePredicateString(
|
|
||||||
predicate: Function,
|
|
||||||
predicateQueryHandler: Function
|
|
||||||
): string {
|
|
||||||
function checkWaitForOptions(
|
|
||||||
node: Node | null,
|
|
||||||
waitForVisible: boolean,
|
|
||||||
waitForHidden: boolean
|
|
||||||
): Node | null | boolean {
|
|
||||||
if (!node) {
|
|
||||||
return waitForHidden;
|
|
||||||
}
|
|
||||||
if (!waitForVisible && !waitForHidden) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
const element =
|
|
||||||
node.nodeType === Node.TEXT_NODE
|
|
||||||
? (node.parentElement as Element)
|
|
||||||
: (node as Element);
|
|
||||||
|
|
||||||
const style = window.getComputedStyle(element);
|
|
||||||
const isVisible =
|
|
||||||
style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
|
|
||||||
const success =
|
|
||||||
waitForVisible === isVisible || waitForHidden === !isVisible;
|
|
||||||
return success ? node : null;
|
|
||||||
|
|
||||||
function hasVisibleBoundingBox(): boolean {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
(() => {
|
|
||||||
const predicateQueryHandler = ${predicateQueryHandler};
|
|
||||||
const checkWaitForOptions = ${checkWaitForOptions};
|
|
||||||
return (${predicate})(...args)
|
|
||||||
})() `;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
@ -4,12 +4,18 @@ import {
|
|||||||
} from '../util/DeferredPromise.js';
|
} from '../util/DeferredPromise.js';
|
||||||
import {assert} from '../util/assert.js';
|
import {assert} from '../util/assert.js';
|
||||||
|
|
||||||
interface Poller<T> {
|
/**
|
||||||
start(): Promise<T>;
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface Poller<T> {
|
||||||
|
start(): Promise<void>;
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
result(): Promise<T>;
|
result(): Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
export class MutationPoller<T> implements Poller<T> {
|
export class MutationPoller<T> implements Poller<T> {
|
||||||
#fn: () => Promise<T>;
|
#fn: () => Promise<T>;
|
||||||
|
|
||||||
@ -22,15 +28,16 @@ export class MutationPoller<T> implements Poller<T> {
|
|||||||
this.#root = root;
|
this.#root = root;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<T> {
|
async start(): Promise<void> {
|
||||||
const promise = (this.#promise = createDeferredPromise<T>());
|
const promise = (this.#promise = createDeferredPromise<T>());
|
||||||
const result = await this.#fn();
|
const result = await this.#fn();
|
||||||
if (result) {
|
if (result) {
|
||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
return result;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#observer = new MutationObserver(async () => {
|
this.#observer = new MutationObserver(async () => {
|
||||||
|
console.log(1);
|
||||||
const result = await this.#fn();
|
const result = await this.#fn();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
@ -43,8 +50,6 @@ export class MutationPoller<T> implements Poller<T> {
|
|||||||
subtree: true,
|
subtree: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.#promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
@ -54,6 +59,7 @@ export class MutationPoller<T> implements Poller<T> {
|
|||||||
}
|
}
|
||||||
if (this.#observer) {
|
if (this.#observer) {
|
||||||
this.#observer.disconnect();
|
this.#observer.disconnect();
|
||||||
|
this.#observer = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,12 +76,12 @@ export class RAFPoller<T> implements Poller<T> {
|
|||||||
this.#fn = fn;
|
this.#fn = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<T> {
|
async start(): Promise<void> {
|
||||||
const promise = (this.#promise = createDeferredPromise<T>());
|
const promise = (this.#promise = createDeferredPromise<T>());
|
||||||
const result = await this.#fn();
|
const result = await this.#fn();
|
||||||
if (result) {
|
if (result) {
|
||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
return result;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
@ -91,8 +97,6 @@ export class RAFPoller<T> implements Poller<T> {
|
|||||||
await this.stop();
|
await this.stop();
|
||||||
};
|
};
|
||||||
window.requestAnimationFrame(poll);
|
window.requestAnimationFrame(poll);
|
||||||
|
|
||||||
return this.#promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
@ -119,12 +123,12 @@ export class IntervalPoller<T> implements Poller<T> {
|
|||||||
this.#ms = ms;
|
this.#ms = ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<T> {
|
async start(): Promise<void> {
|
||||||
const promise = (this.#promise = createDeferredPromise<T>());
|
const promise = (this.#promise = createDeferredPromise<T>());
|
||||||
const result = await this.#fn();
|
const result = await this.#fn();
|
||||||
if (result) {
|
if (result) {
|
||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
return result;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#interval = setInterval(async () => {
|
this.#interval = setInterval(async () => {
|
||||||
@ -135,8 +139,6 @@ export class IntervalPoller<T> implements Poller<T> {
|
|||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
await this.stop();
|
await this.stop();
|
||||||
}, this.#ms);
|
}, this.#ms);
|
||||||
|
|
||||||
return this.#promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
@ -146,6 +148,7 @@ export class IntervalPoller<T> implements Poller<T> {
|
|||||||
}
|
}
|
||||||
if (this.#interval) {
|
if (this.#interval) {
|
||||||
clearInterval(this.#interval);
|
clearInterval(this.#interval);
|
||||||
|
this.#interval = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||||
import * as util from './util.js';
|
import * as util from './util.js';
|
||||||
|
import * as Poller from './Poller.js';
|
||||||
|
|
||||||
const PuppeteerUtil = Object.freeze({
|
const PuppeteerUtil = Object.freeze({
|
||||||
...util,
|
...util,
|
||||||
|
...Poller,
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a function from a string.
|
* Creates a function from a string.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const createFunction = (
|
export const createFunction = (
|
||||||
functionValue: string
|
functionValue: string
|
||||||
@ -16,3 +18,31 @@ export const createFunction = (
|
|||||||
createdFunctions.set(functionValue, fn);
|
createdFunctions.set(functionValue, fn);
|
||||||
return fn;
|
return fn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export const checkVisibility = (
|
||||||
|
node: Node | null,
|
||||||
|
visible?: boolean
|
||||||
|
): Node | boolean => {
|
||||||
|
if (!node) {
|
||||||
|
return visible === false;
|
||||||
|
}
|
||||||
|
if (visible === undefined) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const element = (
|
||||||
|
node.nodeType === Node.TEXT_NODE ? node.parentElement : node
|
||||||
|
) as Element;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
const isVisible =
|
||||||
|
style && style.visibility !== 'hidden' && isBoundingBoxVisible(element);
|
||||||
|
return visible === isVisible ? node : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isBoundingBoxVisible(element: Element): boolean {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
||||||
|
}
|
||||||
|
@ -29,6 +29,7 @@ export * from './common/HTTPResponse.js';
|
|||||||
export * from './common/Input.js';
|
export * from './common/Input.js';
|
||||||
export * from './common/IsolatedWorld.js';
|
export * from './common/IsolatedWorld.js';
|
||||||
export * from './common/JSHandle.js';
|
export * from './common/JSHandle.js';
|
||||||
|
export * from './common/LazyArg.js';
|
||||||
export * from './common/LifecycleWatcher.js';
|
export * from './common/LifecycleWatcher.js';
|
||||||
export * from './common/NetworkConditions.js';
|
export * from './common/NetworkConditions.js';
|
||||||
export * from './common/NetworkEventManager.js';
|
export * from './common/NetworkEventManager.js';
|
||||||
@ -48,6 +49,7 @@ export * from './common/Tracing.js';
|
|||||||
export * from './common/types.js';
|
export * from './common/types.js';
|
||||||
export * from './common/USKeyboardLayout.js';
|
export * from './common/USKeyboardLayout.js';
|
||||||
export * from './common/util.js';
|
export * from './common/util.js';
|
||||||
|
export * from './common/WaitTask.js';
|
||||||
export * from './common/WebWorker.js';
|
export * from './common/WebWorker.js';
|
||||||
export * from './compat.d.js';
|
export * from './compat.d.js';
|
||||||
export * from './constants.js';
|
export * from './constants.js';
|
||||||
|
@ -6,8 +6,8 @@ import {TimeoutError} from '../common/Errors.js';
|
|||||||
export interface DeferredPromise<T> extends Promise<T> {
|
export interface DeferredPromise<T> extends Promise<T> {
|
||||||
finished: () => boolean;
|
finished: () => boolean;
|
||||||
resolved: () => boolean;
|
resolved: () => boolean;
|
||||||
resolve: (_: T) => void;
|
resolve: (value: T) => void;
|
||||||
reject: (_: Error) => void;
|
reject: (reason?: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,8 +32,8 @@ export function createDeferredPromise<T>(
|
|||||||
): DeferredPromise<T> {
|
): DeferredPromise<T> {
|
||||||
let isResolved = false;
|
let isResolved = false;
|
||||||
let isRejected = false;
|
let isRejected = false;
|
||||||
let resolver = (_: T): void => {};
|
let resolver: (value: T) => void;
|
||||||
let rejector = (_: Error) => {};
|
let rejector: (reason?: unknown) => void;
|
||||||
const taskPromise = new Promise<T>((resolve, reject) => {
|
const taskPromise = new Promise<T>((resolve, reject) => {
|
||||||
resolver = resolve;
|
resolver = resolve;
|
||||||
rejector = reject;
|
rejector = reject;
|
||||||
@ -59,7 +59,7 @@ export function createDeferredPromise<T>(
|
|||||||
isResolved = true;
|
isResolved = true;
|
||||||
resolver(value);
|
resolver(value);
|
||||||
},
|
},
|
||||||
reject: (err: Error) => {
|
reject: (err?: unknown) => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
isRejected = true;
|
isRejected = true;
|
||||||
rejector(err);
|
rejector(err);
|
||||||
|
@ -524,7 +524,7 @@ describe('AriaQueryHandler', () => {
|
|||||||
});
|
});
|
||||||
expect(error).toBeTruthy();
|
expect(error).toBeTruthy();
|
||||||
expect(error.message).toContain(
|
expect(error.message).toContain(
|
||||||
'waiting for selector `[role="button"]` failed: timeout'
|
'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded'
|
||||||
);
|
);
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
@ -541,7 +541,7 @@ describe('AriaQueryHandler', () => {
|
|||||||
});
|
});
|
||||||
expect(error).toBeTruthy();
|
expect(error).toBeTruthy();
|
||||||
expect(error.message).toContain(
|
expect(error.message).toContain(
|
||||||
'waiting for selector `[role="main"]` to be hidden failed: timeout'
|
'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -581,7 +581,9 @@ describe('AriaQueryHandler', () => {
|
|||||||
await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => {
|
await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => {
|
||||||
return (error = error_);
|
return (error = error_);
|
||||||
});
|
});
|
||||||
expect(error!.stack).toContain('waiting for selector `zombo` failed');
|
expect(error!.stack).toContain(
|
||||||
|
'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1650,7 +1650,7 @@ describe('Page', function () {
|
|||||||
await page.addScriptTag({url: '/es6/es6import.js', type: 'module'});
|
await page.addScriptTag({url: '/es6/es6import.js', type: 'module'});
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return (globalThis as any).__es6injected;
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
})
|
})
|
||||||
).toBe(42);
|
).toBe(42);
|
||||||
});
|
});
|
||||||
@ -1663,10 +1663,12 @@ describe('Page', function () {
|
|||||||
path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
|
path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
|
||||||
type: 'module',
|
type: 'module',
|
||||||
});
|
});
|
||||||
await page.waitForFunction('window.__es6injected');
|
await page.waitForFunction(() => {
|
||||||
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return (globalThis as any).__es6injected;
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
})
|
})
|
||||||
).toBe(42);
|
).toBe(42);
|
||||||
});
|
});
|
||||||
@ -1679,10 +1681,12 @@ describe('Page', function () {
|
|||||||
content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
|
content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
|
||||||
type: 'module',
|
type: 'module',
|
||||||
});
|
});
|
||||||
await page.waitForFunction('window.__es6injected');
|
await page.waitForFunction(() => {
|
||||||
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return (globalThis as any).__es6injected;
|
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||||
})
|
})
|
||||||
).toBe(42);
|
).toBe(42);
|
||||||
});
|
});
|
||||||
@ -1853,7 +1857,7 @@ describe('Page', function () {
|
|||||||
path: path.join(__dirname, '../assets/injectedstyle.css'),
|
path: path.join(__dirname, '../assets/injectedstyle.css'),
|
||||||
});
|
});
|
||||||
const styleHandle = (await page.$('style'))!;
|
const styleHandle = (await page.$('style'))!;
|
||||||
const styleContent = await page.evaluate(style => {
|
const styleContent = await page.evaluate((style: HTMLStyleElement) => {
|
||||||
return style.innerHTML;
|
return style.innerHTML;
|
||||||
}, styleHandle);
|
}, styleHandle);
|
||||||
expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
|
expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
|
||||||
|
@ -31,9 +31,9 @@ describe('waittask specs', function () {
|
|||||||
it('should accept a string', async () => {
|
it('should accept a string', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
const watchdog = page.waitForFunction('window.__FOO === 1');
|
const watchdog = page.waitForFunction('self.__FOO === 1');
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return ((globalThis as any).__FOO = 1);
|
return ((self as unknown as {__FOO: number}).__FOO = 1);
|
||||||
});
|
});
|
||||||
await watchdog;
|
await watchdog;
|
||||||
});
|
});
|
||||||
@ -52,55 +52,18 @@ describe('waittask specs', function () {
|
|||||||
});
|
});
|
||||||
it('should poll on interval', async () => {
|
it('should poll on interval', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
let success = false;
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const polling = 100;
|
const polling = 100;
|
||||||
const watchdog = page
|
const watchdog = page.waitForFunction(
|
||||||
.waitForFunction(
|
() => {
|
||||||
() => {
|
return (globalThis as any).__FOO === 'hit';
|
||||||
return (globalThis as any).__FOO === 'hit';
|
},
|
||||||
},
|
{polling}
|
||||||
{
|
);
|
||||||
polling,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
return (success = true);
|
|
||||||
});
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
return ((globalThis as any).__FOO = 'hit');
|
setTimeout(() => {
|
||||||
});
|
(globalThis as any).__FOO = 'hit';
|
||||||
expect(success).toBe(false);
|
}, 50);
|
||||||
await page.evaluate(() => {
|
|
||||||
return document.body.appendChild(document.createElement('div'));
|
|
||||||
});
|
|
||||||
await watchdog;
|
|
||||||
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
|
||||||
});
|
|
||||||
it('should poll on interval async', async () => {
|
|
||||||
const {page} = getTestState();
|
|
||||||
let success = false;
|
|
||||||
const startTime = Date.now();
|
|
||||||
const polling = 100;
|
|
||||||
const watchdog = page
|
|
||||||
.waitForFunction(
|
|
||||||
async () => {
|
|
||||||
return (globalThis as any).__FOO === 'hit';
|
|
||||||
},
|
|
||||||
{
|
|
||||||
polling,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
return (success = true);
|
|
||||||
});
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
return ((globalThis as any).__FOO = 'hit');
|
|
||||||
});
|
|
||||||
expect(success).toBe(false);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
return document.body.appendChild(document.createElement('div'));
|
|
||||||
});
|
});
|
||||||
await watchdog;
|
await watchdog;
|
||||||
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
||||||
@ -212,26 +175,6 @@ describe('waittask specs', function () {
|
|||||||
]);
|
]);
|
||||||
expect(error).toBeUndefined();
|
expect(error).toBeUndefined();
|
||||||
});
|
});
|
||||||
it('should throw on bad polling value', async () => {
|
|
||||||
const {page} = getTestState();
|
|
||||||
|
|
||||||
let error!: Error;
|
|
||||||
try {
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => {
|
|
||||||
return !!document.body;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
polling: 'unknown',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error_) {
|
|
||||||
if (isErrorLike(error_)) {
|
|
||||||
error = error_ as Error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(error?.message).toContain('polling');
|
|
||||||
});
|
|
||||||
it('should throw negative polling interval', async () => {
|
it('should throw negative polling interval', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
|
|
||||||
@ -299,23 +242,34 @@ describe('waittask specs', function () {
|
|||||||
const {page, puppeteer} = getTestState();
|
const {page, puppeteer} = getTestState();
|
||||||
|
|
||||||
let error!: Error;
|
let error!: Error;
|
||||||
await page.waitForFunction('false', {timeout: 10}).catch(error_ => {
|
await page
|
||||||
return (error = error_);
|
.waitForFunction(
|
||||||
});
|
() => {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
{timeout: 10}
|
||||||
|
)
|
||||||
|
.catch(error_ => {
|
||||||
|
return (error = error_);
|
||||||
|
});
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
expect(error?.message).toContain('waiting for function failed: timeout');
|
expect(error?.message).toContain('Waiting failed: 10ms exceeded');
|
||||||
});
|
});
|
||||||
it('should respect default timeout', async () => {
|
it('should respect default timeout', async () => {
|
||||||
const {page, puppeteer} = getTestState();
|
const {page, puppeteer} = getTestState();
|
||||||
|
|
||||||
page.setDefaultTimeout(1);
|
page.setDefaultTimeout(1);
|
||||||
let error!: Error;
|
let error!: Error;
|
||||||
await page.waitForFunction('false').catch(error_ => {
|
await page
|
||||||
return (error = error_);
|
.waitForFunction(() => {
|
||||||
});
|
return false;
|
||||||
|
})
|
||||||
|
.catch(error_ => {
|
||||||
|
return (error = error_);
|
||||||
|
});
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
expect(error?.message).toContain('waiting for function failed: timeout');
|
expect(error?.message).toContain('Waiting failed: 1ms exceeded');
|
||||||
});
|
});
|
||||||
it('should disable timeout when its set to 0', async () => {
|
it('should disable timeout when its set to 0', async () => {
|
||||||
const {page} = getTestState();
|
const {page} = getTestState();
|
||||||
@ -341,7 +295,9 @@ describe('waittask specs', function () {
|
|||||||
|
|
||||||
let fooFound = false;
|
let fooFound = false;
|
||||||
const waitForFunction = page
|
const waitForFunction = page
|
||||||
.waitForFunction('globalThis.__FOO === 1')
|
.waitForFunction(() => {
|
||||||
|
return (globalThis as unknown as {__FOO: number}).__FOO === 1;
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return (fooFound = true);
|
return (fooFound = true);
|
||||||
});
|
});
|
||||||
@ -647,7 +603,7 @@ describe('waittask specs', function () {
|
|||||||
});
|
});
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
expect(error?.message).toContain(
|
expect(error?.message).toContain(
|
||||||
'waiting for selector `div` failed: timeout'
|
'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should have an error message specifically for awaiting an element to be hidden', async () => {
|
it('should have an error message specifically for awaiting an element to be hidden', async () => {
|
||||||
@ -662,7 +618,7 @@ describe('waittask specs', function () {
|
|||||||
});
|
});
|
||||||
expect(error).toBeTruthy();
|
expect(error).toBeTruthy();
|
||||||
expect(error?.message).toContain(
|
expect(error?.message).toContain(
|
||||||
'waiting for selector `div` to be hidden failed: timeout'
|
'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -698,9 +654,11 @@ describe('waittask specs', function () {
|
|||||||
await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
|
await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
|
||||||
return (error = error_);
|
return (error = error_);
|
||||||
});
|
});
|
||||||
expect(error?.stack).toContain('waiting for selector `.zombo` failed');
|
expect(error?.stack).toContain(
|
||||||
|
'Waiting for selector `.zombo` failed: Waiting failed: 10ms exceeded'
|
||||||
|
);
|
||||||
// The extension is ts here as Mocha maps back via sourcemaps.
|
// The extension is ts here as Mocha maps back via sourcemaps.
|
||||||
expect(error?.stack).toContain('waittask.spec.ts');
|
expect(error?.stack).toContain('WaitTask.ts');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -730,9 +688,7 @@ describe('waittask specs', function () {
|
|||||||
return (error = error_);
|
return (error = error_);
|
||||||
});
|
});
|
||||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||||
expect(error?.message).toContain(
|
expect(error?.message).toContain('Waiting failed: 10ms exceeded');
|
||||||
'waiting for selector `.//div` failed: timeout 10ms exceeded'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('should run in specified frame', async () => {
|
it('should run in specified frame', async () => {
|
||||||
const {page, server} = getTestState();
|
const {page, server} = getTestState();
|
||||||
|
Loading…
Reference in New Issue
Block a user