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

@ -13,6 +13,6 @@ export interface FrameWaitForFunctionOptions
## Properties
| Property | Modifiers | Type | Description |
| -------------------------------------------------------------- | --------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [polling?](./puppeteer.framewaitforfunctionoptions.polling.md) | | string \| number | <p><i>(Optional)</i> An interval at which the <code>pageFunction</code> is executed, defaults to <code>raf</code>. If <code>polling</code> is a number, then it is treated as an interval in milliseconds at which the function would be executed. If <code>polling</code> is a string, then it can be one of the following values:</p><p>- <code>raf</code> - to constantly execute <code>pageFunction</code> in <code>requestAnimationFrame</code> callback. This is the tightest polling mode which is suitable to observe styling changes.</p><p>- <code>mutation</code> - to execute <code>pageFunction</code> on every DOM mutation.</p> |
| -------------------------------------------------------------- | --------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [polling?](./puppeteer.framewaitforfunctionoptions.polling.md) | | 'raf' \| 'mutation' \| number | <p><i>(Optional)</i> An interval at which the <code>pageFunction</code> is executed, defaults to <code>raf</code>. If <code>polling</code> is a number, then it is treated as an interval in milliseconds at which the function would be executed. If <code>polling</code> is a string, then it can be one of the following values:</p><p>- <code>raf</code> - to constantly execute <code>pageFunction</code> in <code>requestAnimationFrame</code> callback. This is the tightest polling mode which is suitable to observe styling changes.</p><p>- <code>mutation</code> - to execute <code>pageFunction</code> on every DOM mutation.</p> |
| [timeout?](./puppeteer.framewaitforfunctionoptions.timeout.md) | | number | <i>(Optional)</i> Maximum time to wait in milliseconds. Defaults to <code>30000</code> (30 seconds). Pass <code>0</code> to disable the timeout. Puppeteer's default timeout can be changed using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md). |

View File

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

View File

@ -8,7 +8,7 @@ sidebar_label: InnerParams
```typescript
export declare type InnerParams<T extends unknown[]> = {
[K in keyof T]: FlattenHandle<T[K]>;
[K in keyof T]: FlattenHandle<FlattenLazyArg<FlattenHandle<T[K]>>>;
};
```

View File

@ -15,10 +15,7 @@ class Page {
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
options?: {
timeout?: number;
polling?: string | number;
},
options?: FrameWaitForFunctionOptions,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
}
@ -27,17 +24,15 @@ class Page {
## Parameters
| Parameter | Type | Description |
| ------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
| pageFunction | Func \| string | Function to be evaluated in browser context |
| options | { timeout?: number; polling?: string \| number; } | <p><i>(Optional)</i> Optional waiting parameters</p><p>- <code>polling</code> - An interval at which the <code>pageFunction</code> is executed, defaults to <code>raf</code>. If <code>polling</code> is a number, then it is treated as an interval in milliseconds at which the function would be executed. If polling is a string, then it can be one of the following values: - <code>raf</code> - to constantly execute <code>pageFunction</code> in <code>requestAnimationFrame</code> callback. This is the tightest polling mode which is suitable to observe styling changes. - <code>mutation</code>- to execute pageFunction on every DOM mutation. - <code>timeout</code> - maximum time to wait for in milliseconds. Defaults to <code>30000</code> (30 seconds). Pass <code>0</code> to disable timeout. The default value can be changed by using the [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md) method.</p> |
| args | Params | Arguments to pass to <code>pageFunction</code> |
| options | [FrameWaitForFunctionOptions](./puppeteer.framewaitforfunctionoptions.md) | <i>(Optional)</i> Options for configuring waiting behavior. |
| args | Params | |
**Returns:**
Promise&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
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 {ElementHandle} from './ElementHandle.js';
import {Frame} from './Frame.js';
import {MAIN_WORLD, PageBinding, PUPPETEER_WORLD} from './IsolatedWorld.js';
import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorld.js';
import {InternalQueryHandler} from './QueryHandler.js';
async function queryAXTree(
@ -121,9 +121,8 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
frame = elementOrFrame.frame;
element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame);
}
const binding: PageBinding = {
name: 'ariaQuerySelector',
pptrFunction: async (selector: string) => {
const ariaQuerySelector = async (selector: string) => {
const id = await queryOneId(
element || (await frame.worlds[PUPPETEER_WORLD].document()),
selector
@ -134,8 +133,8 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
return (await frame.worlds[PUPPETEER_WORLD].adoptBackendNode(
id
)) as ElementHandle<Node>;
},
};
const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage(
(_: Element, selector: string) => {
return (
@ -147,16 +146,13 @@ const waitFor: InternalQueryHandler['waitFor'] = async (
element,
selector,
options,
binding
new Set([ariaQuerySelector])
);
if (element) {
await element.dispose();
}
if (!result) {
return null;
}
if (!(result instanceof ElementHandle)) {
await result.dispose();
await result?.dispose();
return null;
}
return result.frame.worlds[MAIN_WORLD].transferHandle(result);

View File

@ -18,6 +18,7 @@ import {Protocol} from 'devtools-protocol';
import {CDPSession} from './Connection.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {JSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js';
import {EvaluateFunc, HandleFor} from './types.js';
import {
createJSHandle,
@ -273,7 +274,7 @@ export class ExecutionContext {
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
functionDeclaration: functionText + '\n' + suffix + '\n',
executionContextId: this._contextId,
arguments: args.map(convertArgument.bind(this)),
arguments: await Promise.all(args.map(convertArgument.bind(this))),
returnByValue,
awaitPromise: true,
userGesture: true,
@ -298,10 +299,13 @@ export class ExecutionContext {
? valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject);
function convertArgument(
async function convertArgument(
this: ExecutionContext,
arg: unknown
): Protocol.Runtime.CallArgument {
): Promise<Protocol.Runtime.CallArgument> {
if (arg instanceof LazyArg) {
arg = await arg.get();
}
if (typeof arg === 'bigint') {
// eslint-disable-line valid-typeof
return {unserializableValue: `${arg.toString()}n`};

View File

@ -36,7 +36,7 @@ export interface FrameWaitForFunctionOptions {
*
* - `mutation` - to execute `pageFunction` on every DOM mutation.
*/
polling?: string | number;
polling?: 'raf' | 'mutation' | number;
/**
* Maximum time to wait in milliseconds. Defaults to `30000` (30 seconds).
* Pass `0` to disable the timeout. Puppeteer's default timeout can be changed
@ -664,7 +664,6 @@ export class Frame {
options: FrameWaitForFunctionOptions = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
// TODO: Fix when NodeHandle has been added.
return this.worlds[MAIN_WORLD].waitForFunction(
pageFunction,
options,

View File

@ -19,37 +19,20 @@ import {source as injectedSource} from '../generated/injected.js';
import type PuppeteerUtil from '../injected/injected.js';
import {assert} from '../util/assert.js';
import {createDeferredPromise} from '../util/DeferredPromise.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession} from './Connection.js';
import {ElementHandle} from './ElementHandle.js';
import {TimeoutError} from './Errors.js';
import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js';
import {FrameManager} from './FrameManager.js';
import {MouseButton} from './Input.js';
import {JSHandle} from './JSHandle.js';
import {LazyArg} from './LazyArg.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {EvaluateFunc, HandleFor, NodeFor} from './types.js';
import {
createJSHandle,
debugError,
isNumber,
isString,
makePredicateString,
pageBindingInitString,
} from './util.js';
// predicateQueryHandler and checkWaitForOptions are declared here so that
// TypeScript knows about them when used in the predicate function below.
declare const predicateQueryHandler: (
element: Element | Document,
selector: string
) => Promise<Element | Element[] | NodeListOf<Element>>;
declare const checkWaitForOptions: (
node: Node | null,
waitForVisible: boolean,
waitForHidden: boolean
) => Element | null | boolean;
import {createJSHandle, debugError, pageBindingInitString} from './util.js';
import {TaskManager, WaitTask} from './WaitTask.js';
/**
* @public
@ -124,15 +107,15 @@ export class IsolatedWorld {
// Contains mapping from functions that should be bound to Puppeteer functions.
#boundFunctions = new Map<string, Function>();
#waitTasks = new Set<WaitTask>();
#taskManager = new TaskManager();
#puppeteerUtil = createDeferredPromise<JSHandle<PuppeteerUtil>>();
get puppeteerUtil(): Promise<JSHandle<PuppeteerUtil>> {
return this.#puppeteerUtil;
}
get _waitTasks(): Set<WaitTask> {
return this.#waitTasks;
get taskManager(): TaskManager {
return this.#taskManager;
}
get _boundFunctions(): Map<string, Function> {
@ -176,9 +159,7 @@ export class IsolatedWorld {
this.#injectPuppeteerUtil(context);
this.#ctxBindings.clear();
this.#context.resolve(context);
for (const waitTask of this._waitTasks) {
waitTask.rerun();
}
this.#taskManager.rerunAll();
}
async #injectPuppeteerUtil(context: ExecutionContext): Promise<void> {
@ -204,12 +185,10 @@ export class IsolatedWorld {
_detach(): void {
this.#detached = true;
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
for (const waitTask of this._waitTasks) {
waitTask.terminate(
this.#taskManager.terminateAll(
new Error('waitForFunction failed: frame got detached.')
);
}
}
executionContext(): Promise<ExecutionContext> {
if (this.#detached) {
@ -430,8 +409,6 @@ export class IsolatedWorld {
// TODO: In theory, it would be enough to call this just once
await context._client.send('Runtime.addBinding', {
name,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore The protocol definition is not up to date.
executionContextName: context._contextName,
});
await context.evaluate(expression);
@ -439,18 +416,19 @@ export class IsolatedWorld {
// We could have tried to evaluate in a context which was already
// destroyed. This happens, for example, if the page is navigated while
// we are trying to add the binding
const ctxDestroyed = (error as Error).message.includes(
'Execution context was destroyed'
);
const ctxNotFound = (error as Error).message.includes(
'Cannot find context with specified id'
);
if (ctxDestroyed || ctxNotFound) {
return;
} else {
debugError(error);
if (error instanceof Error) {
// Destroyed context.
if (error.message.includes('Execution context was destroyed')) {
return;
}
// Missing context.
if (error.message.includes('Cannot find context with specified id')) {
return;
}
}
debugError(error);
return;
}
this.#ctxBindings.add(
IsolatedWorld.#bindingIdentifier(name, context._contextId)
@ -495,7 +473,17 @@ export class IsolatedWorld {
throw new Error(`Bound function $name is not found`);
}
const result = await fn(...args);
await context.evaluate(deliverResult, name, seq, result);
await context.evaluate(
(name: string, seq: number, result: unknown) => {
// @ts-expect-error Code is evaluated in a different context.
const callbacks = self[name].callbacks;
callbacks.get(seq).resolve(result);
callbacks.delete(seq);
},
name,
seq,
result
);
} catch (error) {
// The WaitTask may already have been resolved by timing out, or the
// exection context may have been destroyed.
@ -507,14 +495,6 @@ export class IsolatedWorld {
}
debugError(error);
}
function deliverResult(name: string, seq: number, result: unknown): void {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Code is evaluated in a different context.
(globalThis as any)[name].callbacks.get(seq).resolve(result);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Code is evaluated in a different context.
(globalThis as any)[name].callbacks.delete(seq);
}
};
async _waitForSelectorInPage(
@ -522,59 +502,96 @@ export class IsolatedWorld {
root: ElementHandle<Node> | undefined,
selector: string,
options: WaitForSelectorOptions,
binding?: PageBinding
bindings = new Set<(...args: never[]) => unknown>()
): Promise<JSHandle<unknown> | null> {
const {
visible: waitForVisible = false,
hidden: waitForHidden = false,
timeout = this.#timeoutSettings.timeout(),
} = options;
const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
const title = `selector \`${selector}\`${
waitForHidden ? ' to be hidden' : ''
}`;
async function predicate(
root: Element | Document,
selector: string,
waitForVisible: boolean,
waitForHidden: boolean
): Promise<Node | null | boolean> {
const node = (await predicateQueryHandler(root, selector)) as Element;
return checkWaitForOptions(node, waitForVisible, waitForHidden);
try {
const handle = await this.waitForFunction(
async (PuppeteerUtil, query, selector, root, visible) => {
if (!PuppeteerUtil) {
return;
}
const waitTaskOptions: WaitTaskOptions = {
isolatedWorld: this,
predicateBody: makePredicateString(predicate, queryOne),
predicateAcceptsContextElement: true,
title,
polling,
timeout,
args: [selector, waitForVisible, waitForHidden],
binding,
const node = (await PuppeteerUtil.createFunction(query)(
root || document,
selector
)) as Node | null;
return PuppeteerUtil.checkVisibility(node, visible);
},
{
bindings,
polling: waitForVisible || waitForHidden ? 'raf' : 'mutation',
root,
};
const waitTask = new WaitTask(waitTaskOptions);
return waitTask.promise;
timeout,
},
new LazyArg(async () => {
try {
// In case CDP fails.
return await this.puppeteerUtil;
} catch {
return undefined;
}
}),
queryOne.toString(),
selector,
root,
waitForVisible ? true : waitForHidden ? false : undefined
);
const elementHandle = handle.asElement();
if (!elementHandle) {
await handle.dispose();
return null;
}
return elementHandle;
} catch (error) {
if (!isErrorLike(error)) {
throw error;
}
error.message = `Waiting for selector \`${selector}\` failed: ${error.message}`;
throw error;
}
}
waitForFunction(
pageFunction: Function | string,
options: {polling?: string | number; timeout?: number} = {},
...args: unknown[]
): Promise<JSHandle> {
const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} =
options;
const waitTaskOptions: WaitTaskOptions = {
isolatedWorld: this,
predicateBody: pageFunction,
predicateAcceptsContextElement: false,
title: 'function',
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
options: {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
bindings?: Set<(...args: never[]) => unknown>;
} = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const {
polling = 'raf',
timeout = this.#timeoutSettings.timeout(),
bindings,
root,
} = options;
if (typeof polling === 'number' && polling < 0) {
throw new Error('Cannot poll with non-positive interval');
}
const waitTask = new WaitTask(
this,
{
bindings,
polling,
root,
timeout,
args,
};
const waitTask = new WaitTask(waitTaskOptions);
return waitTask.promise;
},
pageFunction as unknown as
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
| string,
...args
);
return waitTask.result;
}
async title(): Promise<string> {
@ -612,315 +629,3 @@ export class IsolatedWorld {
return result;
}
}
/**
* @internal
*/
export interface WaitTaskOptions {
isolatedWorld: IsolatedWorld;
predicateBody: Function | string;
predicateAcceptsContextElement: boolean;
title: string;
polling: string | number;
timeout: number;
binding?: PageBinding;
args: unknown[];
root?: ElementHandle<Node>;
}
const noop = (): void => {};
/**
* @internal
*/
export class WaitTask {
#isolatedWorld: IsolatedWorld;
#polling: 'raf' | 'mutation' | number;
#timeout: number;
#predicateBody: string;
#predicateAcceptsContextElement: boolean;
#args: unknown[];
#binding?: PageBinding;
#runCount = 0;
#resolve: (x: JSHandle) => void = noop;
#reject: (x: Error) => void = noop;
#timeoutTimer?: NodeJS.Timeout;
#terminated = false;
#root: ElementHandle<Node> | null = null;
promise: Promise<JSHandle>;
constructor(options: WaitTaskOptions) {
if (isString(options.polling)) {
assert(
options.polling === 'raf' || options.polling === 'mutation',
'Unknown polling option: ' + options.polling
);
} else if (isNumber(options.polling)) {
assert(
options.polling > 0,
'Cannot poll with non-positive interval: ' + options.polling
);
} else {
throw new Error('Unknown polling options: ' + options.polling);
}
function getPredicateBody(predicateBody: Function | string) {
if (isString(predicateBody)) {
return `return (${predicateBody});`;
}
return `return (${predicateBody})(...args);`;
}
this.#isolatedWorld = options.isolatedWorld;
this.#polling = options.polling;
this.#timeout = options.timeout;
this.#root = options.root || null;
this.#predicateBody = getPredicateBody(options.predicateBody);
this.#predicateAcceptsContextElement =
options.predicateAcceptsContextElement;
this.#args = options.args;
this.#binding = options.binding;
this.#runCount = 0;
this.#isolatedWorld._waitTasks.add(this);
if (this.#binding) {
this.#isolatedWorld._boundFunctions.set(
this.#binding.name,
this.#binding.pptrFunction
);
}
this.promise = new Promise<JSHandle>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
});
// Since page navigation requires us to re-install the pageScript, we should track
// timeout on our end.
if (options.timeout) {
const timeoutError = new TimeoutError(
`waiting for ${options.title} failed: timeout ${options.timeout}ms exceeded`
);
this.#timeoutTimer = setTimeout(() => {
return this.terminate(timeoutError);
}, options.timeout);
}
this.rerun();
}
terminate(error: Error): void {
this.#terminated = true;
this.#reject(error);
this.#cleanup();
}
async rerun(): Promise<void> {
const runCount = ++this.#runCount;
let success: JSHandle | null = null;
let error: Error | null = null;
const context = await this.#isolatedWorld.executionContext();
if (this.#terminated || runCount !== this.#runCount) {
return;
}
if (this.#binding) {
await this.#isolatedWorld._addBindingToContext(
context,
this.#binding.name
);
}
if (this.#terminated || runCount !== this.#runCount) {
return;
}
try {
success = await context.evaluateHandle(
waitForPredicatePageFunction,
this.#root || null,
this.#predicateBody,
this.#predicateAcceptsContextElement,
this.#polling,
this.#timeout,
...this.#args
);
} catch (error_) {
error = error_ as Error;
}
if (this.#terminated || runCount !== this.#runCount) {
if (success) {
await success.dispose();
}
return;
}
// Ignore timeouts in pageScript - we track timeouts ourselves.
// If the frame's execution context has already changed, `frame.evaluate` will
// throw an error - ignore this predicate run altogether.
if (
!error &&
(await this.#isolatedWorld
.evaluate(s => {
return !s;
}, success)
.catch(() => {
return true;
}))
) {
if (!success) {
throw new Error('Assertion: result handle is not available');
}
await success.dispose();
return;
}
if (error) {
if (error.message.includes('TypeError: binding is not a function')) {
return this.rerun();
}
// When frame is detached the task should have been terminated by the IsolatedWorld.
// This can fail if we were adding this task while the frame was detached,
// so we terminate here instead.
if (
error.message.includes(
'Execution context is not available in detached frame'
)
) {
this.terminate(
new Error('waitForFunction failed: frame got detached.')
);
return;
}
// When the page is navigated, the promise is rejected.
// We will try again in the new execution context.
if (error.message.includes('Execution context was destroyed')) {
return;
}
// We could have tried to evaluate in a context which was already
// destroyed.
if (error.message.includes('Cannot find context with specified id')) {
return;
}
this.#reject(error);
} else {
if (!success) {
throw new Error('Assertion: result handle is not available');
}
this.#resolve(success);
}
this.#cleanup();
}
#cleanup(): void {
this.#timeoutTimer !== undefined && clearTimeout(this.#timeoutTimer);
this.#isolatedWorld._waitTasks.delete(this);
}
}
async function waitForPredicatePageFunction(
root: Node | null,
predicateBody: string,
predicateAcceptsContextElement: boolean,
polling: 'raf' | 'mutation' | number,
timeout: number,
...args: unknown[]
): Promise<unknown> {
root = root || document;
const predicate = new Function('...args', predicateBody);
let timedOut = false;
if (timeout) {
setTimeout(() => {
return (timedOut = true);
}, timeout);
}
switch (polling) {
case 'raf':
return await pollRaf();
case 'mutation':
return await pollMutation();
default:
return await pollInterval(polling);
}
async function pollMutation(): Promise<unknown> {
const success = predicateAcceptsContextElement
? await predicate(root, ...args)
: await predicate(...args);
if (success) {
return Promise.resolve(success);
}
let fulfill = (_?: unknown) => {};
const result = new Promise(x => {
return (fulfill = x);
});
const observer = new MutationObserver(async () => {
if (timedOut) {
observer.disconnect();
fulfill();
}
const success = predicateAcceptsContextElement
? await predicate(root, ...args)
: await predicate(...args);
if (success) {
observer.disconnect();
fulfill(success);
}
});
if (!root) {
throw new Error('Root element is not found.');
}
observer.observe(root, {
childList: true,
subtree: true,
attributes: true,
});
return result;
}
async function pollRaf(): Promise<unknown> {
let fulfill = (_?: unknown): void => {};
const result = new Promise(x => {
return (fulfill = x);
});
await onRaf();
return result;
async function onRaf(): Promise<void> {
if (timedOut) {
fulfill();
return;
}
const success = predicateAcceptsContextElement
? await predicate(root, ...args)
: await predicate(...args);
if (success) {
fulfill(success);
} else {
requestAnimationFrame(onRaf);
}
}
}
async function pollInterval(pollInterval: number): Promise<unknown> {
let fulfill = (_?: unknown): void => {};
const result = new Promise(x => {
return (fulfill = x);
});
await onTimeout();
return result;
async function onTimeout(): Promise<void> {
if (timedOut) {
fulfill();
return;
}
const success = predicateAcceptsContextElement
? await predicate(root, ...args)
: await predicate(...args);
if (success) {
fulfill(success);
} else {
setTimeout(onTimeout, pollInterval);
}
}
}
}

13
src/common/LazyArg.ts Normal file
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,
FrameAddScriptTagOptions,
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from './Frame.js';
import {FrameManager, FrameManagerEmittedEvents} from './FrameManager.js';
import {HTTPRequest} from './HTTPRequest.js';
@ -3564,32 +3565,14 @@ export class Page extends EventEmitter {
* ```
*
* @param pageFunction - Function to be evaluated in browser context
* @param options - Optional waiting parameters
*
* - `polling` - An interval at which the `pageFunction` is executed, defaults
* to `raf`. If `polling` is a number, then it is treated as an interval in
* milliseconds at which the function would be executed. If polling is a
* string, then it can be one of the following values:
* - `raf` - to constantly execute `pageFunction` in
* `requestAnimationFrame` callback. This is the tightest polling mode
* which is suitable to observe styling changes.
* - `mutation`- to execute pageFunction on every DOM mutation.
* - `timeout` - maximum time to wait for in milliseconds. Defaults to `30000`
* (30 seconds). Pass `0` to disable timeout. The default value can be
* changed by using the {@link Page.setDefaultTimeout} method.
* @param args - Arguments to pass to `pageFunction`
* @returns A `Promise` which resolves to a JSHandle/ElementHandle of the the
* `pageFunction`'s return value.
* @param options - Options for configuring waiting behavior.
*/
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
options: {
timeout?: number;
polling?: string | number;
} = {},
options: FrameWaitForFunctionOptions = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);

241
src/common/WaitTask.ts Normal file
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 {ElementHandle} from './ElementHandle.js';
import {LazyArg} from './LazyArg.js';
/**
* @public
@ -36,11 +37,17 @@ export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;
* @public
*/
export type FlattenHandle<T> = T extends HandleOr<infer U> ? U : never;
/**
* @internal
*/
export type FlattenLazyArg<T> = T extends LazyArg<infer U> ? U : T;
/**
* @public
*/
export type InnerParams<T extends unknown[]> = {
[K in keyof T]: FlattenHandle<T[K]>;
[K in keyof T]: FlattenHandle<FlattenLazyArg<FlattenHandle<T[K]>>>;
};
/**

View File

@ -249,28 +249,26 @@ export function evaluationString(
* @internal
*/
export function pageBindingInitString(type: string, name: string): string {
function addPageBinding(type: string, bindingName: string): void {
/* Cast window to any here as we're about to add properties to it
* via win[bindingName] which TypeScript doesn't like.
*/
const win = window as any;
const binding = win[bindingName];
function addPageBinding(type: string, name: string): void {
// This is the CDP binding.
// @ts-expect-error: In a different context.
const callCDP = self[name];
win[bindingName] = (...args: unknown[]): Promise<unknown> => {
const me = (window as any)[bindingName];
let callbacks = me.callbacks;
if (!callbacks) {
callbacks = new Map();
me.callbacks = callbacks;
}
const seq = (me.lastSeq || 0) + 1;
me.lastSeq = seq;
const promise = new Promise((resolve, reject) => {
return callbacks.set(seq, {resolve, reject});
// We replace the CDP binding with a Puppeteer binding.
Object.assign(self, {
[name](...args: unknown[]): Promise<unknown> {
// This is the Puppeteer binding.
// @ts-expect-error: In a different context.
const callPuppeteer = self[name];
callPuppeteer.callbacks ??= new Map();
const seq = (callPuppeteer.lastSeq ?? 0) + 1;
callPuppeteer.lastSeq = seq;
callCDP(JSON.stringify({type, name, seq, args}));
return new Promise((resolve, reject) => {
callPuppeteer.callbacks.set(seq, {resolve, reject});
});
},
});
binding(JSON.stringify({type, name: bindingName, seq, args}));
return promise;
};
}
return evaluationString(addPageBinding, type, name);
}
@ -328,50 +326,6 @@ export function pageBindingDeliverErrorValueString(
return evaluationString(deliverErrorValue, name, seq, value);
}
/**
* @internal
*/
export function makePredicateString(
predicate: Function,
predicateQueryHandler: Function
): string {
function checkWaitForOptions(
node: Node | null,
waitForVisible: boolean,
waitForHidden: boolean
): Node | null | boolean {
if (!node) {
return waitForHidden;
}
if (!waitForVisible && !waitForHidden) {
return node;
}
const element =
node.nodeType === Node.TEXT_NODE
? (node.parentElement as Element)
: (node as Element);
const style = window.getComputedStyle(element);
const isVisible =
style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success =
waitForVisible === isVisible || waitForHidden === !isVisible;
return success ? node : null;
function hasVisibleBoundingBox(): boolean {
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
}
}
return `
(() => {
const predicateQueryHandler = ${predicateQueryHandler};
const checkWaitForOptions = ${checkWaitForOptions};
return (${predicate})(...args)
})() `;
}
/**
* @internal
*/

View File

@ -4,12 +4,18 @@ import {
} from '../util/DeferredPromise.js';
import {assert} from '../util/assert.js';
interface Poller<T> {
start(): Promise<T>;
/**
* @internal
*/
export interface Poller<T> {
start(): Promise<void>;
stop(): Promise<void>;
result(): Promise<T>;
}
/**
* @internal
*/
export class MutationPoller<T> implements Poller<T> {
#fn: () => Promise<T>;
@ -22,15 +28,16 @@ export class MutationPoller<T> implements Poller<T> {
this.#root = root;
}
async start(): Promise<T> {
async start(): Promise<void> {
const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn();
if (result) {
promise.resolve(result);
return result;
return;
}
this.#observer = new MutationObserver(async () => {
console.log(1);
const result = await this.#fn();
if (!result) {
return;
@ -43,8 +50,6 @@ export class MutationPoller<T> implements Poller<T> {
subtree: true,
attributes: true,
});
return this.#promise;
}
async stop(): Promise<void> {
@ -54,6 +59,7 @@ export class MutationPoller<T> implements Poller<T> {
}
if (this.#observer) {
this.#observer.disconnect();
this.#observer = undefined;
}
}
@ -70,12 +76,12 @@ export class RAFPoller<T> implements Poller<T> {
this.#fn = fn;
}
async start(): Promise<T> {
async start(): Promise<void> {
const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn();
if (result) {
promise.resolve(result);
return result;
return;
}
const poll = async () => {
@ -91,8 +97,6 @@ export class RAFPoller<T> implements Poller<T> {
await this.stop();
};
window.requestAnimationFrame(poll);
return this.#promise;
}
async stop(): Promise<void> {
@ -119,12 +123,12 @@ export class IntervalPoller<T> implements Poller<T> {
this.#ms = ms;
}
async start(): Promise<T> {
async start(): Promise<void> {
const promise = (this.#promise = createDeferredPromise<T>());
const result = await this.#fn();
if (result) {
promise.resolve(result);
return result;
return;
}
this.#interval = setInterval(async () => {
@ -135,8 +139,6 @@ export class IntervalPoller<T> implements Poller<T> {
promise.resolve(result);
await this.stop();
}, this.#ms);
return this.#promise;
}
async stop(): Promise<void> {
@ -146,6 +148,7 @@ export class IntervalPoller<T> implements Poller<T> {
}
if (this.#interval) {
clearInterval(this.#interval);
this.#interval = undefined;
}
}

View File

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

View File

@ -2,6 +2,8 @@ const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
/**
* Creates a function from a string.
*
* @internal
*/
export const createFunction = (
functionValue: string
@ -16,3 +18,31 @@ export const createFunction = (
createdFunctions.set(functionValue, fn);
return fn;
};
/**
* @internal
*/
export const checkVisibility = (
node: Node | null,
visible?: boolean
): Node | boolean => {
if (!node) {
return visible === false;
}
if (visible === undefined) {
return node;
}
const element = (
node.nodeType === Node.TEXT_NODE ? node.parentElement : node
) as Element;
const style = window.getComputedStyle(element);
const isVisible =
style && style.visibility !== 'hidden' && isBoundingBoxVisible(element);
return visible === isVisible ? node : false;
};
function isBoundingBoxVisible(element: Element): boolean {
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
}

View File

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

View File

@ -6,8 +6,8 @@ import {TimeoutError} from '../common/Errors.js';
export interface DeferredPromise<T> extends Promise<T> {
finished: () => boolean;
resolved: () => boolean;
resolve: (_: T) => void;
reject: (_: Error) => void;
resolve: (value: T) => void;
reject: (reason?: unknown) => void;
}
/**
@ -32,8 +32,8 @@ export function createDeferredPromise<T>(
): DeferredPromise<T> {
let isResolved = false;
let isRejected = false;
let resolver = (_: T): void => {};
let rejector = (_: Error) => {};
let resolver: (value: T) => void;
let rejector: (reason?: unknown) => void;
const taskPromise = new Promise<T>((resolve, reject) => {
resolver = resolve;
rejector = reject;
@ -59,7 +59,7 @@ export function createDeferredPromise<T>(
isResolved = true;
resolver(value);
},
reject: (err: Error) => {
reject: (err?: unknown) => {
clearTimeout(timeoutId);
isRejected = true;
rejector(err);

View File

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

View File

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

View File

@ -31,9 +31,9 @@ describe('waittask specs', function () {
it('should accept a string', async () => {
const {page} = getTestState();
const watchdog = page.waitForFunction('window.__FOO === 1');
const watchdog = page.waitForFunction('self.__FOO === 1');
await page.evaluate(() => {
return ((globalThis as any).__FOO = 1);
return ((self as unknown as {__FOO: number}).__FOO = 1);
});
await watchdog;
});
@ -52,55 +52,18 @@ describe('waittask specs', function () {
});
it('should poll on interval', async () => {
const {page} = getTestState();
let success = false;
const startTime = Date.now();
const polling = 100;
const watchdog = page
.waitForFunction(
const watchdog = page.waitForFunction(
() => {
return (globalThis as any).__FOO === 'hit';
},
{
polling,
}
)
.then(() => {
return (success = true);
});
{polling}
);
await page.evaluate(() => {
return ((globalThis as any).__FOO = 'hit');
});
expect(success).toBe(false);
await page.evaluate(() => {
return document.body.appendChild(document.createElement('div'));
});
await watchdog;
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
});
it('should poll on interval async', async () => {
const {page} = getTestState();
let success = false;
const startTime = Date.now();
const polling = 100;
const watchdog = page
.waitForFunction(
async () => {
return (globalThis as any).__FOO === 'hit';
},
{
polling,
}
)
.then(() => {
return (success = true);
});
await page.evaluate(async () => {
return ((globalThis as any).__FOO = 'hit');
});
expect(success).toBe(false);
await page.evaluate(async () => {
return document.body.appendChild(document.createElement('div'));
setTimeout(() => {
(globalThis as any).__FOO = 'hit';
}, 50);
});
await watchdog;
expect(Date.now() - startTime).not.toBeLessThan(polling / 2);
@ -212,26 +175,6 @@ describe('waittask specs', function () {
]);
expect(error).toBeUndefined();
});
it('should throw on bad polling value', async () => {
const {page} = getTestState();
let error!: Error;
try {
await page.waitForFunction(
() => {
return !!document.body;
},
{
polling: 'unknown',
}
);
} catch (error_) {
if (isErrorLike(error_)) {
error = error_ as Error;
}
}
expect(error?.message).toContain('polling');
});
it('should throw negative polling interval', async () => {
const {page} = getTestState();
@ -299,23 +242,34 @@ describe('waittask specs', function () {
const {page, puppeteer} = getTestState();
let error!: Error;
await page.waitForFunction('false', {timeout: 10}).catch(error_ => {
await page
.waitForFunction(
() => {
return false;
},
{timeout: 10}
)
.catch(error_ => {
return (error = error_);
});
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error?.message).toContain('waiting for function failed: timeout');
expect(error?.message).toContain('Waiting failed: 10ms exceeded');
});
it('should respect default timeout', async () => {
const {page, puppeteer} = getTestState();
page.setDefaultTimeout(1);
let error!: Error;
await page.waitForFunction('false').catch(error_ => {
await page
.waitForFunction(() => {
return false;
})
.catch(error_ => {
return (error = error_);
});
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error?.message).toContain('waiting for function failed: timeout');
expect(error?.message).toContain('Waiting failed: 1ms exceeded');
});
it('should disable timeout when its set to 0', async () => {
const {page} = getTestState();
@ -341,7 +295,9 @@ describe('waittask specs', function () {
let fooFound = false;
const waitForFunction = page
.waitForFunction('globalThis.__FOO === 1')
.waitForFunction(() => {
return (globalThis as unknown as {__FOO: number}).__FOO === 1;
})
.then(() => {
return (fooFound = true);
});
@ -647,7 +603,7 @@ describe('waittask specs', function () {
});
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error?.message).toContain(
'waiting for selector `div` failed: timeout'
'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
);
});
it('should have an error message specifically for awaiting an element to be hidden', async () => {
@ -662,7 +618,7 @@ describe('waittask specs', function () {
});
expect(error).toBeTruthy();
expect(error?.message).toContain(
'waiting for selector `div` to be hidden failed: timeout'
'Waiting for selector `div` failed: Waiting failed: 10ms exceeded'
);
});
@ -698,9 +654,11 @@ describe('waittask specs', function () {
await page.waitForSelector('.zombo', {timeout: 10}).catch(error_ => {
return (error = error_);
});
expect(error?.stack).toContain('waiting for selector `.zombo` failed');
expect(error?.stack).toContain(
'Waiting for selector `.zombo` failed: Waiting failed: 10ms exceeded'
);
// The extension is ts here as Mocha maps back via sourcemaps.
expect(error?.stack).toContain('waittask.spec.ts');
expect(error?.stack).toContain('WaitTask.ts');
});
});
@ -730,9 +688,7 @@ describe('waittask specs', function () {
return (error = error_);
});
expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError);
expect(error?.message).toContain(
'waiting for selector `.//div` failed: timeout 10ms exceeded'
);
expect(error?.message).toContain('Waiting failed: 10ms exceeded');
});
it('should run in specified frame', async () => {
const {page, server} = getTestState();