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
@ -13,6 +13,6 @@ export interface FrameWaitForFunctionOptions
|
||||
## Properties
|
||||
|
||||
| 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). |
|
||||
|
@ -14,6 +14,6 @@ An interval at which the `pageFunction` is executed, defaults to `raf`. If `poll
|
||||
|
||||
```typescript
|
||||
interface FrameWaitForFunctionOptions {
|
||||
polling?: string | number;
|
||||
polling?: 'raf' | 'mutation' | number;
|
||||
}
|
||||
```
|
||||
|
@ -8,7 +8,7 @@ sidebar_label: InnerParams
|
||||
|
||||
```typescript
|
||||
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>
|
||||
>(
|
||||
pageFunction: Func | string,
|
||||
options?: {
|
||||
timeout?: number;
|
||||
polling?: string | number;
|
||||
},
|
||||
options?: FrameWaitForFunctionOptions,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
|
||||
}
|
||||
@ -27,17 +24,15 @@ class Page {
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| 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> |
|
||||
| args | Params | Arguments to pass to <code>pageFunction</code> |
|
||||
| options | [FrameWaitForFunctionOptions](./puppeteer.framewaitforfunctionoptions.md) | <i>(Optional)</i> Options for configuring waiting behavior. |
|
||||
| args | Params | |
|
||||
|
||||
**Returns:**
|
||||
|
||||
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
|
||||
|
||||
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 {ElementHandle} from './ElementHandle.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';
|
||||
|
||||
async function queryAXTree(
|
||||
@ -121,9 +121,8 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
||||
frame = elementOrFrame.frame;
|
||||
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
|
||||
}
|
||||
const binding: PageBinding = {
|
||||
name: 'ariaQuerySelector',
|
||||
pptrFunction: async (selector: string) => {
|
||||
|
||||
const ariaQuerySelector = async (selector: string) => {
|
||||
const id = await queryOneId(
|
||||
element || (await frame.worlds[PUPPETEER_WORLD].document()),
|
||||
selector
|
||||
@ -134,8 +133,8 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
||||
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
|
||||
id
|
||||
)) as ElementHandle<Node>;
|
||||
},
|
||||
};
|
||||
|
||||
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
|
||||
(_: Element, selector: string) => {
|
||||
return (
|
||||
@ -147,16 +146,13 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
|
||||
element,
|
||||
selector,
|
||||
options,
|
||||
binding
|
||||
new Set([ariaQuerySelector])
|
||||
);
|
||||
if (element) {
|
||||
await element.dispose();
|
||||
}
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
if (!(result instanceof ElementHandle)) {
|
||||
await result.dispose();
|
||||
await result?.dispose();
|
||||
return null;
|
||||
}
|
||||
return result.frame.worlds[MAIN_WORLD].transferHandle(result);
|
||||
|
@ -18,6 +18,7 @@ import {Protocol} from 'devtools-protocol';
|
||||
import {CDPSession} from './Connection.js';
|
||||
import {IsolatedWorld} from './IsolatedWorld.js';
|
||||
import {JSHandle} from './JSHandle.js';
|
||||
import {LazyArg} from './LazyArg.js';
|
||||
import {EvaluateFunc, HandleFor} from './types.js';
|
||||
import {
|
||||
createJSHandle,
|
||||
@ -273,7 +274,7 @@ export class ExecutionContext {
|
||||
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: functionText + '\n' + suffix + '\n',
|
||||
executionContextId: this._contextId,
|
||||
arguments: args.map(convertArgument.bind(this)),
|
||||
arguments: await Promise.all(args.map(convertArgument.bind(this))),
|
||||
returnByValue,
|
||||
awaitPromise: true,
|
||||
userGesture: true,
|
||||
@ -298,10 +299,13 @@ export class ExecutionContext {
|
||||
? valueFromRemoteObject(remoteObject)
|
||||
: createJSHandle(this, remoteObject);
|
||||
|
||||
function convertArgument(
|
||||
async function convertArgument(
|
||||
this: ExecutionContext,
|
||||
arg: unknown
|
||||
): Protocol.Runtime.CallArgument {
|
||||
): Promise<Protocol.Runtime.CallArgument> {
|
||||
if (arg instanceof LazyArg) {
|
||||
arg = await arg.get();
|
||||
}
|
||||
if (typeof arg === 'bigint') {
|
||||
// eslint-disable-line valid-typeof
|
||||
return {unserializableValue: `${arg.toString()}n`};
|
||||
|
@ -36,7 +36,7 @@ export interface FrameWaitForFunctionOptions {
|
||||
*
|
||||
* - `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).
|
||||
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
|
||||
@ -664,7 +664,6 @@ export class Frame {
|
||||
options: FrameWaitForFunctionOptions = {},
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
// TODO: Fix when NodeHandle has been added.
|
||||
return this.worlds[MAIN_WORLD].waitForFunction(
|
||||
pageFunction,
|
||||
options,
|
||||
|
@ -19,37 +19,20 @@ import {source as injectedSource} from '../generated/injected.js';
|
||||
import type PuppeteerUtil from '../injected/injected.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||
import {isErrorLike} from '../util/ErrorLike.js';
|
||||
import {CDPSession} from './Connection.js';
|
||||
import {ElementHandle} from './ElementHandle.js';
|
||||
import {TimeoutError} from './Errors.js';
|
||||
import {ExecutionContext} from './ExecutionContext.js';
|
||||
import {Frame} from './Frame.js';
|
||||
import {FrameManager} from './FrameManager.js';
|
||||
import {MouseButton} from './Input.js';
|
||||
import {JSHandle} from './JSHandle.js';
|
||||
import {LazyArg} from './LazyArg.js';
|
||||
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
|
||||
import {TimeoutSettings} from './TimeoutSettings.js';
|
||||
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
|
||||
import {
|
||||
createJSHandle,
|
||||
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;
|
||||
import {createJSHandle, debugError, pageBindingInitString} from './util.js';
|
||||
import {TaskManager, WaitTask} from './WaitTask.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -124,15 +107,15 @@ export class IsolatedWorld {
|
||||
|
||||
// Contains mapping from functions that should be bound to Puppeteer functions.
|
||||
#boundFunctions = new Map<string, Function>();
|
||||
#waitTasks = new Set<WaitTask>();
|
||||
#taskManager = new TaskManager();
|
||||
#puppeteerUtil = createDeferredPromise<JSHandle<PuppeteerUtil>>();
|
||||
|
||||
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
|
||||
return this.#puppeteerUtil;
|
||||
}
|
||||
|
||||
get _waitTasks(): Set<WaitTask> {
|
||||
return this.#waitTasks;
|
||||
get taskManager(): TaskManager {
|
||||
return this.#taskManager;
|
||||
}
|
||||
|
||||
get _boundFunctions(): Map<string, Function> {
|
||||
@ -176,9 +159,7 @@ export class IsolatedWorld {
|
||||
this.#injectPuppeteerUtil(context);
|
||||
this.#ctxBindings.clear();
|
||||
this.#context.resolve(context);
|
||||
for (const waitTask of this._waitTasks) {
|
||||
waitTask.rerun();
|
||||
}
|
||||
this.#taskManager.rerunAll();
|
||||
}
|
||||
|
||||
async #injectPuppeteerUtil(context: ExecutionContext): Promise<void> {
|
||||
@ -204,12 +185,10 @@ export class IsolatedWorld {
|
||||
_detach(): void {
|
||||
this.#detached = true;
|
||||
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
|
||||
for (const waitTask of this._waitTasks) {
|
||||
waitTask.terminate(
|
||||
this.#taskManager.terminateAll(
|
||||
new Error('waitForFunction failed: frame got detached.')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
executionContext(): Promise<ExecutionContext> {
|
||||
if (this.#detached) {
|
||||
@ -430,8 +409,6 @@ export class IsolatedWorld {
|
||||
// TODO: In theory, it would be enough to call this just once
|
||||
await context._client.send('Runtime.addBinding', {
|
||||
name,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore The protocol definition is not up to date.
|
||||
executionContextName: context._contextName,
|
||||
});
|
||||
await context.evaluate(expression);
|
||||
@ -439,18 +416,19 @@ export class IsolatedWorld {
|
||||
// We could have tried to evaluate in a context which was already
|
||||
// destroyed. This happens, for example, if the page is navigated while
|
||||
// we are trying to add the binding
|
||||
const ctxDestroyed = (error as Error).message.includes(
|
||||
'Execution context was destroyed'
|
||||
);
|
||||
const ctxNotFound = (error as Error).message.includes(
|
||||
'Cannot find context with specified id'
|
||||
);
|
||||
if (ctxDestroyed || ctxNotFound) {
|
||||
return;
|
||||
} else {
|
||||
debugError(error);
|
||||
if (error instanceof Error) {
|
||||
// Destroyed context.
|
||||
if (error.message.includes('Execution context was destroyed')) {
|
||||
return;
|
||||
}
|
||||
// Missing context.
|
||||
if (error.message.includes('Cannot find context with specified id')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
debugError(error);
|
||||
return;
|
||||
}
|
||||
this.#ctxBindings.add(
|
||||
IsolatedWorld.#bindingIdentifier(name, context._contextId)
|
||||
@ -495,7 +473,17 @@ export class IsolatedWorld {
|
||||
throw new Error(`Bound function $name is not found`);
|
||||
}
|
||||
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) {
|
||||
// The WaitTask may already have been resolved by timing out, or the
|
||||
// exection context may have been destroyed.
|
||||
@ -507,14 +495,6 @@ export class IsolatedWorld {
|
||||
}
|
||||
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(
|
||||
@ -522,59 +502,96 @@ export class IsolatedWorld {
|
||||
root: ElementHandle<Node> | undefined,
|
||||
selector: string,
|
||||
options: WaitForSelectorOptions,
|
||||
binding?: PageBinding
|
||||
bindings = new Set<(...args: never[]) => unknown>()
|
||||
): Promise<JSHandle<unknown> | null> {
|
||||
const {
|
||||
visible: waitForVisible = false,
|
||||
hidden: waitForHidden = false,
|
||||
timeout = this.#timeoutSettings.timeout(),
|
||||
} = options;
|
||||
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
|
||||
const title = `selector \`${selector}\`${
|
||||
waitForHidden ? ' to be hidden' : ''
|
||||
}`;
|
||||
async function predicate(
|
||||
root: Element | Document,
|
||||
selector: string,
|
||||
waitForVisible: boolean,
|
||||
waitForHidden: boolean
|
||||
): Promise<Node | null | boolean> {
|
||||
const node = (await predicateQueryHandler(root, selector)) as Element;
|
||||
return checkWaitForOptions(node, waitForVisible, waitForHidden);
|
||||
|
||||
try {
|
||||
const handle = await this.waitForFunction(
|
||||
async (PuppeteerUtil, query, selector, root, visible) => {
|
||||
if (!PuppeteerUtil) {
|
||||
return;
|
||||
}
|
||||
const waitTaskOptions: WaitTaskOptions = {
|
||||
isolatedWorld: this,
|
||||
predicateBody: makePredicateString(predicate, queryOne),
|
||||
predicateAcceptsContextElement: true,
|
||||
title,
|
||||
polling,
|
||||
timeout,
|
||||
args: [selector, waitForVisible, waitForHidden],
|
||||
binding,
|
||||
const node = (await PuppeteerUtil.createFunction(query)(
|
||||
root || document,
|
||||
selector
|
||||
)) as Node | null;
|
||||
return PuppeteerUtil.checkVisibility(node, visible);
|
||||
},
|
||||
{
|
||||
bindings,
|
||||
polling: waitForVisible || waitForHidden ? 'raf' : 'mutation',
|
||||
root,
|
||||
};
|
||||
const waitTask = new WaitTask(waitTaskOptions);
|
||||
return waitTask.promise;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
waitForFunction(
|
||||
pageFunction: Function | string,
|
||||
options: {polling?: string | number; timeout?: number} = {},
|
||||
...args: unknown[]
|
||||
): Promise<JSHandle> {
|
||||
const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} =
|
||||
options;
|
||||
const waitTaskOptions: WaitTaskOptions = {
|
||||
isolatedWorld: this,
|
||||
predicateBody: pageFunction,
|
||||
predicateAcceptsContextElement: false,
|
||||
title: 'function',
|
||||
waitForFunction<
|
||||
Params extends unknown[],
|
||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||
>(
|
||||
pageFunction: Func | string,
|
||||
options: {
|
||||
polling?: 'raf' | 'mutation' | number;
|
||||
timeout?: number;
|
||||
root?: ElementHandle<Node>;
|
||||
bindings?: Set<(...args: never[]) => unknown>;
|
||||
} = {},
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
const {
|
||||
polling = 'raf',
|
||||
timeout = this.#timeoutSettings.timeout(),
|
||||
bindings,
|
||||
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,
|
||||
args,
|
||||
};
|
||||
const waitTask = new WaitTask(waitTaskOptions);
|
||||
return waitTask.promise;
|
||||
},
|
||||
pageFunction as unknown as
|
||||
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
|
||||
| string,
|
||||
...args
|
||||
);
|
||||
return waitTask.result;
|
||||
}
|
||||
|
||||
async title(): Promise<string> {
|
||||
@ -612,315 +629,3 @@ export class IsolatedWorld {
|
||||
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,
|
||||
FrameAddScriptTagOptions,
|
||||
FrameAddStyleTagOptions,
|
||||
FrameWaitForFunctionOptions,
|
||||
} from './Frame.js';
|
||||
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.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 options - Optional waiting parameters
|
||||
*
|
||||
* - `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.
|
||||
* @param options - Options for configuring waiting behavior.
|
||||
*/
|
||||
waitForFunction<
|
||||
Params extends unknown[],
|
||||
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
|
||||
>(
|
||||
pageFunction: Func | string,
|
||||
options: {
|
||||
timeout?: number;
|
||||
polling?: string | number;
|
||||
} = {},
|
||||
options: FrameWaitForFunctionOptions = {},
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
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 {ElementHandle} from './ElementHandle.js';
|
||||
import {LazyArg} from './LazyArg.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -36,11 +37,17 @@ export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;
|
||||
* @public
|
||||
*/
|
||||
export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type FlattenLazyArg<T> = T extends LazyArg<infer U> ? U : T;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
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
|
||||
*/
|
||||
export function pageBindingInitString(type: string, name: string): string {
|
||||
function addPageBinding(type: string, bindingName: string): void {
|
||||
/* Cast window to any here as we're about to add properties to it
|
||||
* via win[bindingName] which TypeScript doesn't like.
|
||||
*/
|
||||
const win = window as any;
|
||||
const binding = win[bindingName];
|
||||
function addPageBinding(type: string, name: string): void {
|
||||
// This is the CDP binding.
|
||||
// @ts-expect-error: In a different context.
|
||||
const callCDP = self[name];
|
||||
|
||||
win[bindingName] = (...args: unknown[]): Promise<unknown> => {
|
||||
const me = (window as any)[bindingName];
|
||||
let callbacks = me.callbacks;
|
||||
if (!callbacks) {
|
||||
callbacks = new Map();
|
||||
me.callbacks = callbacks;
|
||||
}
|
||||
const seq = (me.lastSeq || 0) + 1;
|
||||
me.lastSeq = seq;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
return callbacks.set(seq, {resolve, reject});
|
||||
// We replace the CDP binding with a Puppeteer binding.
|
||||
Object.assign(self, {
|
||||
[name](...args: unknown[]): Promise<unknown> {
|
||||
// This is the Puppeteer binding.
|
||||
// @ts-expect-error: In a different context.
|
||||
const callPuppeteer = self[name];
|
||||
callPuppeteer.callbacks ??= new Map();
|
||||
const seq = (callPuppeteer.lastSeq ?? 0) + 1;
|
||||
callPuppeteer.lastSeq = seq;
|
||||
callCDP(JSON.stringify({type, name, seq, args}));
|
||||
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);
|
||||
}
|
||||
@ -328,50 +326,6 @@ export function pageBindingDeliverErrorValueString(
|
||||
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
|
||||
*/
|
||||
|
@ -4,12 +4,18 @@ import {
|
||||
} from '../util/DeferredPromise.js';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
interface Poller<T> {
|
||||
start(): Promise<T>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface Poller<T> {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
result(): Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class MutationPoller<T> implements Poller<T> {
|
||||
#fn: () => Promise<T>;
|
||||
|
||||
@ -22,15 +28,16 @@ export class MutationPoller<T> implements Poller<T> {
|
||||
this.#root = root;
|
||||
}
|
||||
|
||||
async start(): Promise<T> {
|
||||
async start(): Promise<void> {
|
||||
const promise = (this.#promise = createDeferredPromise<T>());
|
||||
const result = await this.#fn();
|
||||
if (result) {
|
||||
promise.resolve(result);
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#observer = new MutationObserver(async () => {
|
||||
console.log(1);
|
||||
const result = await this.#fn();
|
||||
if (!result) {
|
||||
return;
|
||||
@ -43,8 +50,6 @@ export class MutationPoller<T> implements Poller<T> {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
});
|
||||
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
@ -54,6 +59,7 @@ export class MutationPoller<T> implements Poller<T> {
|
||||
}
|
||||
if (this.#observer) {
|
||||
this.#observer.disconnect();
|
||||
this.#observer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,12 +76,12 @@ export class RAFPoller<T> implements Poller<T> {
|
||||
this.#fn = fn;
|
||||
}
|
||||
|
||||
async start(): Promise<T> {
|
||||
async start(): Promise<void> {
|
||||
const promise = (this.#promise = createDeferredPromise<T>());
|
||||
const result = await this.#fn();
|
||||
if (result) {
|
||||
promise.resolve(result);
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
const poll = async () => {
|
||||
@ -91,8 +97,6 @@ export class RAFPoller<T> implements Poller<T> {
|
||||
await this.stop();
|
||||
};
|
||||
window.requestAnimationFrame(poll);
|
||||
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
@ -119,12 +123,12 @@ export class IntervalPoller<T> implements Poller<T> {
|
||||
this.#ms = ms;
|
||||
}
|
||||
|
||||
async start(): Promise<T> {
|
||||
async start(): Promise<void> {
|
||||
const promise = (this.#promise = createDeferredPromise<T>());
|
||||
const result = await this.#fn();
|
||||
if (result) {
|
||||
promise.resolve(result);
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#interval = setInterval(async () => {
|
||||
@ -135,8 +139,6 @@ export class IntervalPoller<T> implements Poller<T> {
|
||||
promise.resolve(result);
|
||||
await this.stop();
|
||||
}, this.#ms);
|
||||
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
@ -146,6 +148,7 @@ export class IntervalPoller<T> implements Poller<T> {
|
||||
}
|
||||
if (this.#interval) {
|
||||
clearInterval(this.#interval);
|
||||
this.#interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {createDeferredPromise} from '../util/DeferredPromise.js';
|
||||
import * as util from './util.js';
|
||||
import * as Poller from './Poller.js';
|
||||
|
||||
const PuppeteerUtil = Object.freeze({
|
||||
...util,
|
||||
...Poller,
|
||||
createDeferredPromise,
|
||||
});
|
||||
|
||||
|
@ -2,6 +2,8 @@ const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
|
||||
|
||||
/**
|
||||
* Creates a function from a string.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const createFunction = (
|
||||
functionValue: string
|
||||
@ -16,3 +18,31 @@ export const createFunction = (
|
||||
createdFunctions.set(functionValue, 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/IsolatedWorld.js';
|
||||
export * from './common/JSHandle.js';
|
||||
export * from './common/LazyArg.js';
|
||||
export * from './common/LifecycleWatcher.js';
|
||||
export * from './common/NetworkConditions.js';
|
||||
export * from './common/NetworkEventManager.js';
|
||||
@ -48,6 +49,7 @@ export * from './common/Tracing.js';
|
||||
export * from './common/types.js';
|
||||
export * from './common/USKeyboardLayout.js';
|
||||
export * from './common/util.js';
|
||||
export * from './common/WaitTask.js';
|
||||
export * from './common/WebWorker.js';
|
||||
export * from './compat.d.js';
|
||||
export * from './constants.js';
|
||||
|
@ -6,8 +6,8 @@ import {TimeoutError} from '../common/Errors.js';
|
||||
export interface DeferredPromise<T> extends Promise<T> {
|
||||
finished: () => boolean;
|
||||
resolved: () => boolean;
|
||||
resolve: (_: T) => void;
|
||||
reject: (_: Error) => void;
|
||||
resolve: (value: T) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,8 +32,8 @@ export function createDeferredPromise<T>(
|
||||
): DeferredPromise<T> {
|
||||
let isResolved = false;
|
||||
let isRejected = false;
|
||||
let resolver = (_: T): void => {};
|
||||
let rejector = (_: Error) => {};
|
||||
let resolver: (value: T) => void;
|
||||
let rejector: (reason?: unknown) => void;
|
||||
const taskPromise = new Promise<T>((resolve, reject) => {
|
||||
resolver = resolve;
|
||||
rejector = reject;
|
||||
@ -59,7 +59,7 @@ export function createDeferredPromise<T>(
|
||||
isResolved = true;
|
||||
resolver(value);
|
||||
},
|
||||
reject: (err: Error) => {
|
||||
reject: (err?: unknown) => {
|
||||
clearTimeout(timeoutId);
|
||||
isRejected = true;
|
||||
rejector(err);
|
||||
|
@ -524,7 +524,7 @@ describe('AriaQueryHandler', () => {
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
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);
|
||||
});
|
||||
@ -541,7 +541,7 @@ describe('AriaQueryHandler', () => {
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
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_ => {
|
||||
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'});
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return (globalThis as any).__es6injected;
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
})
|
||||
).toBe(42);
|
||||
});
|
||||
@ -1663,10 +1663,12 @@ describe('Page', function () {
|
||||
path: path.join(__dirname, '../assets/es6/es6pathimport.js'),
|
||||
type: 'module',
|
||||
});
|
||||
await page.waitForFunction('window.__es6injected');
|
||||
await page.waitForFunction(() => {
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
});
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return (globalThis as any).__es6injected;
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
})
|
||||
).toBe(42);
|
||||
});
|
||||
@ -1679,10 +1681,12 @@ describe('Page', function () {
|
||||
content: `import num from '/es6/es6module.js';window.__es6injected = num;`,
|
||||
type: 'module',
|
||||
});
|
||||
await page.waitForFunction('window.__es6injected');
|
||||
await page.waitForFunction(() => {
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
});
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
return (globalThis as any).__es6injected;
|
||||
return (window as unknown as {__es6injected: number}).__es6injected;
|
||||
})
|
||||
).toBe(42);
|
||||
});
|
||||
@ -1853,7 +1857,7 @@ describe('Page', function () {
|
||||
path: path.join(__dirname, '../assets/injectedstyle.css'),
|
||||
});
|
||||
const styleHandle = (await page.$('style'))!;
|
||||
const styleContent = await page.evaluate(style => {
|
||||
const styleContent = await page.evaluate((style: HTMLStyleElement) => {
|
||||
return style.innerHTML;
|
||||
}, styleHandle);
|
||||
expect(styleContent).toContain(path.join('assets', 'injectedstyle.css'));
|
||||
|
@ -31,9 +31,9 @@ describe('waittask specs', function () {
|
||||
it('should accept a string', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
const watchdog = page.waitForFunction('window.__FOO === 1');
|
||||
const watchdog = page.waitForFunction('self.__FOO === 1');
|
||||
await page.evaluate(() => {
|
||||
return ((globalThis as any).__FOO = 1);
|
||||
return ((self as unknown as {__FOO: number}).__FOO = 1);
|
||||
});
|
||||
await watchdog;
|
||||
});
|
||||
@ -52,55 +52,18 @@ describe('waittask specs', function () {
|
||||
});
|
||||
it('should poll on interval', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
let success = false;
|
||||
const startTime = Date.now();
|
||||
const polling = 100;
|
||||
const watchdog = page
|
||||
.waitForFunction(
|
||||
const watchdog = page.waitForFunction(
|
||||
() => {
|
||||
return (globalThis as any).__FOO === 'hit';
|
||||
},
|
||||
{
|
||||
polling,
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
return (success = true);
|
||||
});
|
||||
{polling}
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
return ((globalThis as any).__FOO = 'hit');
|
||||
});
|
||||
expect(success).toBe(false);
|
||||
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'));
|
||||
setTimeout(() => {
|
||||
(globalThis as any).__FOO = 'hit';
|
||||
}, 50);
|
||||
});
|
||||
await watchdog;
|
||||
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
|
||||
@ -212,26 +175,6 @@ describe('waittask specs', function () {
|
||||
]);
|
||||
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 () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
@ -299,23 +242,34 @@ describe('waittask specs', function () {
|
||||
const {page, puppeteer} = getTestState();
|
||||
|
||||
let error!: Error;
|
||||
await page.waitForFunction('false', {timeout: 10}).catch(error_ => {
|
||||
await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
return false;
|
||||
},
|
||||
{timeout: 10}
|
||||
)
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const {page, puppeteer} = getTestState();
|
||||
|
||||
page.setDefaultTimeout(1);
|
||||
let error!: Error;
|
||||
await page.waitForFunction('false').catch(error_ => {
|
||||
await page
|
||||
.waitForFunction(() => {
|
||||
return false;
|
||||
})
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
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 () => {
|
||||
const {page} = getTestState();
|
||||
@ -341,7 +295,9 @@ describe('waittask specs', function () {
|
||||
|
||||
let fooFound = false;
|
||||
const waitForFunction = page
|
||||
.waitForFunction('globalThis.__FOO === 1')
|
||||
.waitForFunction(() => {
|
||||
return (globalThis as unknown as {__FOO: number}).__FOO === 1;
|
||||
})
|
||||
.then(() => {
|
||||
return (fooFound = true);
|
||||
});
|
||||
@ -647,7 +603,7 @@ describe('waittask specs', function () {
|
||||
});
|
||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||
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 () => {
|
||||
@ -662,7 +618,7 @@ describe('waittask specs', function () {
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
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_ => {
|
||||
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.
|
||||
expect(error?.stack).toContain('waittask.spec.ts');
|
||||
expect(error?.stack).toContain('WaitTask.ts');
|
||||
});
|
||||
});
|
||||
|
||||
@ -730,9 +688,7 @@ describe('waittask specs', function () {
|
||||
return (error = error_);
|
||||
});
|
||||
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
|
||||
expect(error?.message).toContain(
|
||||
'waiting for selector `.//div` failed: timeout 10ms exceeded'
|
||||
);
|
||||
expect(error?.message).toContain('Waiting failed: 10ms exceeded');
|
||||
});
|
||||
it('should run in specified frame', async () => {
|
||||
const {page, server} = getTestState();
|
||||
|
Loading…
Reference in New Issue
Block a user