diff --git a/docs/api/puppeteer.framewaitforfunctionoptions.md b/docs/api/puppeteer.framewaitforfunctionoptions.md
index 5c718fc3..d377cbeb 100644
--- a/docs/api/puppeteer.framewaitforfunctionoptions.md
+++ b/docs/api/puppeteer.framewaitforfunctionoptions.md
@@ -12,7 +12,7 @@ export interface FrameWaitForFunctionOptions
## Properties
-| Property | Modifiers | Type | Description |
-| -------------------------------------------------------------- | --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| [polling?](./puppeteer.framewaitforfunctionoptions.polling.md) | | string \| number |
(Optional) 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?](./puppeteer.framewaitforfunctionoptions.timeout.md) | | number | (Optional) Maximum time to wait in milliseconds. Defaults to 30000
(30 seconds). Pass 0
to disable the timeout. Puppeteer's default timeout can be changed using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md). |
+| Property | Modifiers | Type | Description |
+| -------------------------------------------------------------- | --------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [polling?](./puppeteer.framewaitforfunctionoptions.polling.md) | | 'raf' \| 'mutation' \| number | (Optional) 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?](./puppeteer.framewaitforfunctionoptions.timeout.md) | | number | (Optional) Maximum time to wait in milliseconds. Defaults to 30000
(30 seconds). Pass 0
to disable the timeout. Puppeteer's default timeout can be changed using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md). |
diff --git a/docs/api/puppeteer.framewaitforfunctionoptions.polling.md b/docs/api/puppeteer.framewaitforfunctionoptions.polling.md
index 21f99272..c7d1ef93 100644
--- a/docs/api/puppeteer.framewaitforfunctionoptions.polling.md
+++ b/docs/api/puppeteer.framewaitforfunctionoptions.polling.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;
}
```
diff --git a/docs/api/puppeteer.innerparams.md b/docs/api/puppeteer.innerparams.md
index 8518b00e..1cb93ea6 100644
--- a/docs/api/puppeteer.innerparams.md
+++ b/docs/api/puppeteer.innerparams.md
@@ -8,7 +8,7 @@ sidebar_label: InnerParams
```typescript
export declare type InnerParams = {
- [K in keyof T]: FlattenHandle;
+ [K in keyof T]: FlattenHandle>>;
};
```
diff --git a/docs/api/puppeteer.page.waitforfunction.md b/docs/api/puppeteer.page.waitforfunction.md
index 3e49efc0..9f08f0d7 100644
--- a/docs/api/puppeteer.page.waitforfunction.md
+++ b/docs/api/puppeteer.page.waitforfunction.md
@@ -15,10 +15,7 @@ class Page {
Func extends EvaluateFunc = EvaluateFunc
>(
pageFunction: Func | string,
- options?: {
- timeout?: number;
- polling?: string | number;
- },
+ options?: FrameWaitForFunctionOptions,
...args: Params
): Promise>>>;
}
@@ -26,18 +23,16 @@ class Page {
## Parameters
-| Parameter | Type | Description |
-| ------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| pageFunction | Func \| string | Function to be evaluated in browser context |
-| options | { timeout?: number; polling?: string \| number; } | (Optional) 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 [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md) method.
|
-| args | Params | Arguments to pass to pageFunction
|
+| Parameter | Type | Description |
+| ------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
+| pageFunction | Func \| string | Function to be evaluated in browser context |
+| options | [FrameWaitForFunctionOptions](./puppeteer.framewaitforfunctionoptions.md) | (Optional) 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:
diff --git a/src/common/AriaQueryHandler.ts b/src/common/AriaQueryHandler.ts
index 2ec86f5b..4460c909 100644
--- a/src/common/AriaQueryHandler.ts
+++ b/src/common/AriaQueryHandler.ts
@@ -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,21 +121,20 @@ 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 id = await queryOneId(
- element || (await frame.worlds[PUPPETEER_WORLD].document()),
- selector
- );
- if (!id) {
- return null;
- }
- return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
- id
- )) as ElementHandle;
- },
+
+ const ariaQuerySelector = async (selector: string) => {
+ const id = await queryOneId(
+ element || (await frame.worlds[PUPPETEER_WORLD].document()),
+ selector
+ );
+ if (!id) {
+ return null;
+ }
+ return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
+ id
+ )) as ElementHandle;
};
+
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);
diff --git a/src/common/ExecutionContext.ts b/src/common/ExecutionContext.ts
index 13dfbc51..c5058fa1 100644
--- a/src/common/ExecutionContext.ts
+++ b/src/common/ExecutionContext.ts
@@ -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 {
+ if (arg instanceof LazyArg) {
+ arg = await arg.get();
+ }
if (typeof arg === 'bigint') {
// eslint-disable-line valid-typeof
return {unserializableValue: `${arg.toString()}n`};
diff --git a/src/common/Frame.ts b/src/common/Frame.ts
index 54dfaca8..619ff17a 100644
--- a/src/common/Frame.ts
+++ b/src/common/Frame.ts
@@ -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>>> {
- // TODO: Fix when NodeHandle has been added.
return this.worlds[MAIN_WORLD].waitForFunction(
pageFunction,
options,
diff --git a/src/common/IsolatedWorld.ts b/src/common/IsolatedWorld.ts
index 6423ce27..9a7de4bb 100644
--- a/src/common/IsolatedWorld.ts
+++ b/src/common/IsolatedWorld.ts
@@ -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>;
-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();
- #waitTasks = new Set();
+ #taskManager = new TaskManager();
#puppeteerUtil = createDeferredPromise>();
get puppeteerUtil(): Promise> {
return this.#puppeteerUtil;
}
- get _waitTasks(): Set {
- return this.#waitTasks;
+ get taskManager(): TaskManager {
+ return this.#taskManager;
}
get _boundFunctions(): Map {
@@ -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 {
@@ -186,10 +167,10 @@ export class IsolatedWorld {
this.#puppeteerUtil.resolve(
(await context.evaluateHandle(
`(() => {
- const module = {};
- ${injectedSource}
- return module.exports.default;
- })()`
+ const module = {};
+ ${injectedSource}
+ return module.exports.default;
+ })()`
)) as JSHandle
);
} catch (error: unknown) {
@@ -204,11 +185,9 @@ export class IsolatedWorld {
_detach(): void {
this.#detached = true;
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
- for (const waitTask of this._waitTasks) {
- waitTask.terminate(
- new Error('waitForFunction failed: frame got detached.')
- );
- }
+ this.#taskManager.terminateAll(
+ new Error('waitForFunction failed: frame got detached.')
+ );
}
executionContext(): Promise {
@@ -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);
- return;
+ 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 | undefined,
selector: string,
options: WaitForSelectorOptions,
- binding?: PageBinding
+ bindings = new Set<(...args: never[]) => unknown>()
): Promise | 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 {
- 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 node = (await PuppeteerUtil.createFunction(query)(
+ root || document,
+ selector
+ )) as Node | null;
+ 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(
- pageFunction: Function | string,
- options: {polling?: string | number; timeout?: number} = {},
- ...args: unknown[]
- ): Promise {
- const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} =
- options;
- const waitTaskOptions: WaitTaskOptions = {
- isolatedWorld: this,
- predicateBody: pageFunction,
- predicateAcceptsContextElement: false,
- title: 'function',
- polling,
- timeout,
- args,
- };
- const waitTask = new WaitTask(waitTaskOptions);
- return waitTask.promise;
+ waitForFunction<
+ Params extends unknown[],
+ Func extends EvaluateFunc = EvaluateFunc
+ >(
+ pageFunction: Func | string,
+ options: {
+ polling?: 'raf' | 'mutation' | number;
+ timeout?: number;
+ root?: ElementHandle;
+ bindings?: Set<(...args: never[]) => unknown>;
+ } = {},
+ ...args: Params
+ ): Promise>>> {
+ 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,
+ },
+ pageFunction as unknown as
+ | ((...args: unknown[]) => Promise>>)
+ | string,
+ ...args
+ );
+ return waitTask.result;
}
async title(): Promise {
@@ -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;
-}
-
-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 | null = null;
-
- promise: Promise;
-
- 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((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 {
- 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 {
- 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 {
- 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 {
- let fulfill = (_?: unknown): void => {};
- const result = new Promise(x => {
- return (fulfill = x);
- });
- await onRaf();
- return result;
-
- async function onRaf(): Promise {
- 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 {
- let fulfill = (_?: unknown): void => {};
- const result = new Promise(x => {
- return (fulfill = x);
- });
- await onTimeout();
- return result;
-
- async function onTimeout(): Promise {
- if (timedOut) {
- fulfill();
- return;
- }
- const success = predicateAcceptsContextElement
- ? await predicate(root, ...args)
- : await predicate(...args);
- if (success) {
- fulfill(success);
- } else {
- setTimeout(onTimeout, pollInterval);
- }
- }
- }
-}
diff --git a/src/common/LazyArg.ts b/src/common/LazyArg.ts
new file mode 100644
index 00000000..e4db8e68
--- /dev/null
+++ b/src/common/LazyArg.ts
@@ -0,0 +1,13 @@
+/**
+ * @internal
+ */
+export class LazyArg {
+ #get: () => Promise;
+ constructor(get: () => Promise) {
+ this.#get = get;
+ }
+
+ get(): Promise {
+ return this.#get();
+ }
+}
diff --git a/src/common/Page.ts b/src/common/Page.ts
index 7a7d5605..ce90e758 100644
--- a/src/common/Page.ts
+++ b/src/common/Page.ts
@@ -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 = EvaluateFunc
>(
pageFunction: Func | string,
- options: {
- timeout?: number;
- polling?: string | number;
- } = {},
+ options: FrameWaitForFunctionOptions = {},
...args: Params
): Promise>>> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
diff --git a/src/common/WaitTask.ts b/src/common/WaitTask.ts
new file mode 100644
index 00000000..660f2ac3
--- /dev/null
+++ b/src/common/WaitTask.ts
@@ -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;
+ timeout: number;
+}
+
+/**
+ * @internal
+ */
+export class WaitTask {
+ #world: IsolatedWorld;
+ #bindings: Set<(...args: never[]) => unknown>;
+ #polling: 'raf' | 'mutation' | number;
+ #root?: ElementHandle;
+
+ #fn: string;
+ #args: unknown[];
+
+ #timeout?: NodeJS.Timeout;
+
+ #result = createDeferredPromise>();
+
+ #poller?: JSHandle>;
+
+ constructor(
+ world: IsolatedWorld,
+ options: WaitTaskOptions,
+ fn: ((...args: unknown[]) => Promise) | 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> {
+ return this.#result;
+ }
+
+ async rerun(): Promise {
+ 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;
+ });
+ },
+ 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;
+ }, 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;
+ }, 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 {
+ 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 = new Set();
+
+ add(task: WaitTask): void {
+ this.#tasks.add(task);
+ }
+
+ delete(task: WaitTask): void {
+ this.#tasks.delete(task);
+ }
+
+ terminateAll(error?: Error): void {
+ for (const task of this.#tasks) {
+ task.terminate(error);
+ }
+ this.#tasks.clear();
+ }
+
+ async rerunAll(): Promise {
+ await Promise.all(
+ [...this.#tasks].map(task => {
+ return task.rerun();
+ })
+ );
+ }
+}
diff --git a/src/common/types.ts b/src/common/types.ts
index 8af6f23f..42f984e1 100644
--- a/src/common/types.ts
+++ b/src/common/types.ts
@@ -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 = HandleFor | JSHandle | T;
* @public
*/
export type FlattenHandle = T extends HandleOr ? U : never;
+
+/**
+ * @internal
+ */
+export type FlattenLazyArg = T extends LazyArg ? U : T;
+
/**
* @public
*/
export type InnerParams = {
- [K in keyof T]: FlattenHandle;
+ [K in keyof T]: FlattenHandle>>;
};
/**
diff --git a/src/common/util.ts b/src/common/util.ts
index 0b280292..a39e914f 100644
--- a/src/common/util.ts
+++ b/src/common/util.ts
@@ -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 => {
- 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});
- });
- binding(JSON.stringify({type, name: bindingName, seq, args}));
- return promise;
- };
+ // We replace the CDP binding with a Puppeteer binding.
+ Object.assign(self, {
+ [name](...args: unknown[]): Promise {
+ // 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});
+ });
+ },
+ });
}
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
*/
diff --git a/src/injected/Poller.ts b/src/injected/Poller.ts
index c6748ebb..4966f816 100644
--- a/src/injected/Poller.ts
+++ b/src/injected/Poller.ts
@@ -4,12 +4,18 @@ import {
} from '../util/DeferredPromise.js';
import {assert} from '../util/assert.js';
-interface Poller {
- start(): Promise;
+/**
+ * @internal
+ */
+export interface Poller {
+ start(): Promise;
stop(): Promise;
result(): Promise;
}
+/**
+ * @internal
+ */
export class MutationPoller implements Poller {
#fn: () => Promise;
@@ -22,15 +28,16 @@ export class MutationPoller implements Poller {
this.#root = root;
}
- async start(): Promise {
+ async start(): Promise {
const promise = (this.#promise = createDeferredPromise());
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 implements Poller {
subtree: true,
attributes: true,
});
-
- return this.#promise;
}
async stop(): Promise {
@@ -54,6 +59,7 @@ export class MutationPoller implements Poller {
}
if (this.#observer) {
this.#observer.disconnect();
+ this.#observer = undefined;
}
}
@@ -70,12 +76,12 @@ export class RAFPoller implements Poller {
this.#fn = fn;
}
- async start(): Promise {
+ async start(): Promise {
const promise = (this.#promise = createDeferredPromise());
const result = await this.#fn();
if (result) {
promise.resolve(result);
- return result;
+ return;
}
const poll = async () => {
@@ -91,8 +97,6 @@ export class RAFPoller implements Poller {
await this.stop();
};
window.requestAnimationFrame(poll);
-
- return this.#promise;
}
async stop(): Promise {
@@ -119,12 +123,12 @@ export class IntervalPoller implements Poller {
this.#ms = ms;
}
- async start(): Promise {
+ async start(): Promise {
const promise = (this.#promise = createDeferredPromise());
const result = await this.#fn();
if (result) {
promise.resolve(result);
- return result;
+ return;
}
this.#interval = setInterval(async () => {
@@ -135,8 +139,6 @@ export class IntervalPoller implements Poller {
promise.resolve(result);
await this.stop();
}, this.#ms);
-
- return this.#promise;
}
async stop(): Promise {
@@ -146,6 +148,7 @@ export class IntervalPoller implements Poller {
}
if (this.#interval) {
clearInterval(this.#interval);
+ this.#interval = undefined;
}
}
diff --git a/src/injected/injected.ts b/src/injected/injected.ts
index 6a6bdca3..0b11fff7 100644
--- a/src/injected/injected.ts
+++ b/src/injected/injected.ts
@@ -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,
});
diff --git a/src/injected/util.ts b/src/injected/util.ts
index 79e68e5e..5279050c 100644
--- a/src/injected/util.ts
+++ b/src/injected/util.ts
@@ -2,6 +2,8 @@ const createdFunctions = new Map 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);
+}
diff --git a/src/types.ts b/src/types.ts
index 29176c03..4f4db83d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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';
diff --git a/src/util/DeferredPromise.ts b/src/util/DeferredPromise.ts
index 2f21087b..8c7945e3 100644
--- a/src/util/DeferredPromise.ts
+++ b/src/util/DeferredPromise.ts
@@ -6,8 +6,8 @@ import {TimeoutError} from '../common/Errors.js';
export interface DeferredPromise extends Promise {
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(
): DeferredPromise {
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((resolve, reject) => {
resolver = resolve;
rejector = reject;
@@ -59,7 +59,7 @@ export function createDeferredPromise(
isResolved = true;
resolver(value);
},
- reject: (err: Error) => {
+ reject: (err?: unknown) => {
clearTimeout(timeoutId);
isRejected = true;
rejector(err);
diff --git a/test/src/ariaqueryhandler.spec.ts b/test/src/ariaqueryhandler.spec.ts
index f6c11e83..5071432e 100644
--- a/test/src/ariaqueryhandler.spec.ts
+++ b/test/src/ariaqueryhandler.spec.ts
@@ -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'
+ );
});
});
diff --git a/test/src/page.spec.ts b/test/src/page.spec.ts
index c1f3c83c..0da27ebd 100644
--- a/test/src/page.spec.ts
+++ b/test/src/page.spec.ts
@@ -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'));
diff --git a/test/src/waittask.spec.ts b/test/src/waittask.spec.ts
index 6c63fabf..69dbad48 100644
--- a/test/src/waittask.spec.ts
+++ b/test/src/waittask.spec.ts
@@ -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(
- () => {
- return (globalThis as any).__FOO === 'hit';
- },
- {
- polling,
- }
- )
- .then(() => {
- return (success = true);
- });
+ const watchdog = page.waitForFunction(
+ () => {
+ return (globalThis as any).__FOO === 'hit';
+ },
+ {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_ => {
- return (error = 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_ => {
- return (error = 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();