From 7c4f41fadcbdf627458ad41f5e4d232f3c585b94 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Thu, 15 Sep 2022 08:22:20 +0200 Subject: [PATCH] chore: use injections for wait tasks (#8943) This PR refactors wait tasks to use injected scripts. --- .../puppeteer.framewaitforfunctionoptions.md | 8 +- ...eer.framewaitforfunctionoptions.polling.md | 2 +- docs/api/puppeteer.innerparams.md | 2 +- docs/api/puppeteer.page.waitforfunction.md | 17 +- src/common/AriaQueryHandler.ts | 36 +- src/common/ExecutionContext.ts | 10 +- src/common/Frame.ts | 3 +- src/common/IsolatedWorld.ts | 533 ++++-------------- src/common/LazyArg.ts | 13 + src/common/Page.ts | 23 +- src/common/WaitTask.ts | 241 ++++++++ src/common/types.ts | 9 +- src/common/util.ts | 84 +-- src/injected/Poller.ts | 31 +- src/injected/injected.ts | 2 + src/injected/util.ts | 30 + src/types.ts | 2 + src/util/DeferredPromise.ts | 10 +- test/src/ariaqueryhandler.spec.ts | 8 +- test/src/page.spec.ts | 16 +- test/src/waittask.spec.ts | 124 ++-- 21 files changed, 550 insertions(+), 654 deletions(-) create mode 100644 src/common/LazyArg.ts create mode 100644 src/common/WaitTask.ts 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();