chore: use injections for wait tasks (#8943)

This PR refactors wait tasks to use injected scripts.
This commit is contained in:
jrandolf 2022-09-15 08:22:20 +02:00 committed by GitHub
parent 8d5097d7f6
commit 7c4f41fadc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 550 additions and 654 deletions

View File

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

View File

@ -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;
} }
``` ```

View File

@ -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]>>>;
}; };
``` ```

View File

@ -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&lt;[HandleFor](./puppeteer.handlefor.md)&lt;Awaited&lt;ReturnType&lt;Func&gt;&gt;&gt;&gt; Promise&lt;[HandleFor](./puppeteer.handlefor.md)&lt;Awaited&lt;ReturnType&lt;Func&gt;&gt;&gt;&gt;
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:

View File

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

View File

@ -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`};

View File

@ -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,

View File

@ -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
View 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();
}
}

View File

@ -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
View 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();
})
);
}
}

View File

@ -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]>>>;
}; };
/** /**

View File

@ -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
*/ */

View File

@ -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;
} }
} }

View File

@ -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,
}); });

View File

@ -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);
}

View File

@ -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';

View File

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

View File

@ -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'
);
}); });
}); });

View File

@ -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'));

View File

@ -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();