diff --git a/docs/api/puppeteer.elementhandle.waitforselector.md b/docs/api/puppeteer.elementhandle.waitforselector.md index 5f905d1715d6a..2b41e310922ac 100644 --- a/docs/api/puppeteer.elementhandle.waitforselector.md +++ b/docs/api/puppeteer.elementhandle.waitforselector.md @@ -14,17 +14,17 @@ Unlike [Frame.waitForSelector()](./puppeteer.frame.waitforselector.md), this met class ElementHandle { waitForSelector( selector: Selector, - options?: Exclude + options?: WaitForSelectorOptions ): Promise> | null>; } ``` ## Parameters -| Parameter | Type | Description | -| --------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| selector | Selector | The selector to query and wait for. | -| options | Exclude<[WaitForSelectorOptions](./puppeteer.waitforselectoroptions.md), 'root'> | (Optional) Options for customizing waiting behavior. | +| Parameter | Type | Description | +| --------- | --------------------------------------------------------------- | ----------------------------------------------------------- | +| selector | Selector | The selector to query and wait for. | +| options | [WaitForSelectorOptions](./puppeteer.waitforselectoroptions.md) | (Optional) Options for customizing waiting behavior. | **Returns:** diff --git a/docs/api/puppeteer.page.waitforselector.md b/docs/api/puppeteer.page.waitforselector.md index 36d002654b5d0..5be61d737f2df 100644 --- a/docs/api/puppeteer.page.waitforselector.md +++ b/docs/api/puppeteer.page.waitforselector.md @@ -34,17 +34,17 @@ const puppeteer = require('puppeteer'); class Page { waitForSelector( selector: Selector, - options?: Exclude + options?: WaitForSelectorOptions ): Promise> | null>; } ``` ## Parameters -| Parameter | Type | Description | -| --------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| selector | Selector | A [selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) of an element to wait for | -| options | Exclude<[WaitForSelectorOptions](./puppeteer.waitforselectoroptions.md), 'root'> | (Optional) Optional waiting parameters | +| Parameter | Type | Description | +| --------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| selector | Selector | A [selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) of an element to wait for | +| options | [WaitForSelectorOptions](./puppeteer.waitforselectoroptions.md) | (Optional) Optional waiting parameters | **Returns:** diff --git a/docs/api/puppeteer.waitforselectoroptions.md b/docs/api/puppeteer.waitforselectoroptions.md index b100914d2cfcd..c8aabccd0d9cd 100644 --- a/docs/api/puppeteer.waitforselectoroptions.md +++ b/docs/api/puppeteer.waitforselectoroptions.md @@ -12,9 +12,8 @@ export interface WaitForSelectorOptions ## Properties -| Property | Modifiers | Type | Description | -| --------------------------------------------------------- | --------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [hidden?](./puppeteer.waitforselectoroptions.hidden.md) | | boolean | (Optional) Wait for the selected element to not be found in the DOM or to be hidden, i.e. have display: none or visibility: hidden CSS properties. | -| [root?](./puppeteer.waitforselectoroptions.root.md) | | [ElementHandle](./puppeteer.elementhandle.md)<Node> | (Optional) | -| [timeout?](./puppeteer.waitforselectoroptions.timeout.md) | | number |

(Optional) Maximum time to wait in milliseconds. Pass 0 to disable timeout.

The default value can be changed by using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md)

| -| [visible?](./puppeteer.waitforselectoroptions.visible.md) | | boolean | (Optional) Wait for the selected element to be present in DOM and to be visible, i.e. to not have display: none or visibility: hidden CSS properties. | +| Property | Modifiers | Type | Description | +| --------------------------------------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [hidden?](./puppeteer.waitforselectoroptions.hidden.md) | | boolean | (Optional) Wait for the selected element to not be found in the DOM or to be hidden, i.e. have display: none or visibility: hidden CSS properties. | +| [timeout?](./puppeteer.waitforselectoroptions.timeout.md) | | number |

(Optional) Maximum time to wait in milliseconds. Pass 0 to disable timeout.

The default value can be changed by using [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md)

| +| [visible?](./puppeteer.waitforselectoroptions.visible.md) | | boolean | (Optional) Wait for the selected element to be present in DOM and to be visible, i.e. to not have display: none or visibility: hidden CSS properties. | diff --git a/docs/api/puppeteer.waitforselectoroptions.root.md b/docs/api/puppeteer.waitforselectoroptions.root.md deleted file mode 100644 index d8a2b946f28e4..0000000000000 --- a/docs/api/puppeteer.waitforselectoroptions.root.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -sidebar_label: WaitForSelectorOptions.root ---- - -# WaitForSelectorOptions.root property - -> Warning: This API is now obsolete. -> -> Do not use. Use the [ElementHandle.waitForSelector()](./puppeteer.elementhandle.waitforselector.md) - -**Signature:** - -```typescript -interface WaitForSelectorOptions { - root?: ElementHandle; -} -``` diff --git a/src/common/AriaQueryHandler.ts b/src/common/AriaQueryHandler.ts index 45400dd2c5de4..2ec86f5bf2477 100644 --- a/src/common/AriaQueryHandler.ts +++ b/src/common/AriaQueryHandler.ts @@ -18,11 +18,8 @@ import {Protocol} from 'devtools-protocol'; import {assert} from '../util/assert.js'; import {CDPSession} from './Connection.js'; import {ElementHandle} from './ElementHandle.js'; -import { - IsolatedWorld, - PageBinding, - WaitForSelectorOptions, -} from './IsolatedWorld.js'; +import {Frame} from './Frame.js'; +import {MAIN_WORLD, PageBinding, PUPPETEER_WORLD} from './IsolatedWorld.js'; import {InternalQueryHandler} from './QueryHandler.js'; async function queryAXTree( @@ -89,52 +86,86 @@ function parseAriaSelector(selector: string): ARIAQueryOption { return queryOptions; } -const queryOne = async ( - element: ElementHandle, - selector: string -): Promise | null> => { - const exeCtx = element.executionContext(); +const queryOneId = async (element: ElementHandle, selector: string) => { const {name, role} = parseAriaSelector(selector); - const res = await queryAXTree(exeCtx._client, element, name, role); + const res = await queryAXTree(element.client, element, name, role); if (!res[0] || !res[0].backendDOMNodeId) { return null; } - return (await exeCtx._world!.adoptBackendNode( - res[0].backendDOMNodeId + return res[0].backendDOMNodeId; +}; + +const queryOne: InternalQueryHandler['queryOne'] = async ( + element, + selector +) => { + const id = await queryOneId(element, selector); + if (!id) { + return null; + } + return (await element.frame.worlds[MAIN_WORLD].adoptBackendNode( + id )) as ElementHandle; }; -const waitFor = async ( - isolatedWorld: IsolatedWorld, - selector: string, - options: WaitForSelectorOptions -): Promise | null> => { +const waitFor: InternalQueryHandler['waitFor'] = async ( + elementOrFrame, + selector, + options +) => { + let frame: Frame; + let element: ElementHandle | undefined; + if (elementOrFrame instanceof Frame) { + frame = elementOrFrame; + } else { + frame = elementOrFrame.frame; + element = await frame.worlds[PUPPETEER_WORLD].adoptHandle(elementOrFrame); + } const binding: PageBinding = { name: 'ariaQuerySelector', pptrFunction: async (selector: string) => { - const root = options.root || (await isolatedWorld.document()); - const element = await queryOne(root, selector); - return element; + 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; }, }; - return (await isolatedWorld._waitForSelectorInPage( + const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage( (_: Element, selector: string) => { return ( globalThis as unknown as { - ariaQuerySelector(selector: string): void; + ariaQuerySelector(selector: string): Node | null; } ).ariaQuerySelector(selector); }, + element, selector, options, binding - )) as ElementHandle | null; + ); + if (element) { + await element.dispose(); + } + if (!result) { + return null; + } + if (!(result instanceof ElementHandle)) { + await result.dispose(); + return null; + } + return result.frame.worlds[MAIN_WORLD].transferHandle(result); }; -const queryAll = async ( - element: ElementHandle, - selector: string -): Promise>> => { +const queryAll: InternalQueryHandler['queryAll'] = async ( + element, + selector +) => { const exeCtx = element.executionContext(); const {name, role} = parseAriaSelector(selector); const res = await queryAXTree(exeCtx._client, element, name, role); diff --git a/src/common/ElementHandle.ts b/src/common/ElementHandle.ts index bdb686ddaacdb..ed80b69bd0449 100644 --- a/src/common/ElementHandle.ts +++ b/src/common/ElementHandle.ts @@ -3,11 +3,7 @@ import {assert} from '../util/assert.js'; import {ExecutionContext} from './ExecutionContext.js'; import {Frame} from './Frame.js'; import {FrameManager} from './FrameManager.js'; -import { - MAIN_WORLD, - PUPPETEER_WORLD, - WaitForSelectorOptions, -} from './IsolatedWorld.js'; +import {WaitForSelectorOptions} from './IsolatedWorld.js'; import { BoundingBox, BoxModel, @@ -310,26 +306,16 @@ export class ElementHandle< */ async waitForSelector( selector: Selector, - options: Exclude = {} + options: WaitForSelectorOptions = {} ): Promise> | null> { - const frame = this.#frame; - const adoptedRoot = await frame.worlds[PUPPETEER_WORLD].adoptHandle(this); - const handle = await frame.worlds[PUPPETEER_WORLD].waitForSelector( - selector, - { - ...options, - root: adoptedRoot, - } - ); - await adoptedRoot.dispose(); - if (!handle) { - return null; - } - const result = (await frame.worlds[MAIN_WORLD].adoptHandle( - handle - )) as ElementHandle>; - await handle.dispose(); - return result; + const {updatedSelector, queryHandler} = + getQueryHandlerAndSelector(selector); + assert(queryHandler.waitFor, 'Query handler does not support waiting'); + return (await queryHandler.waitFor( + this, + updatedSelector, + options + )) as ElementHandle> | null; } /** diff --git a/src/common/Frame.ts b/src/common/Frame.ts index e48719aa1d08d..2714157ad0340 100644 --- a/src/common/Frame.ts +++ b/src/common/Frame.ts @@ -1,4 +1,5 @@ import {Protocol} from 'devtools-protocol'; +import {assert} from '../util/assert.js'; import {isErrorLike} from '../util/ErrorLike.js'; import {CDPSession} from './Connection.js'; import {ElementHandle} from './ElementHandle.js'; @@ -15,6 +16,7 @@ import { } from './IsolatedWorld.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {Page} from './Page.js'; +import {getQueryHandlerAndSelector} from './QueryHandler.js'; import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; /** @@ -579,18 +581,14 @@ export class Frame { selector: Selector, options: WaitForSelectorOptions = {} ): Promise> | null> { - const handle = await this.worlds[PUPPETEER_WORLD].waitForSelector( - selector, + const {updatedSelector, queryHandler} = + getQueryHandlerAndSelector(selector); + assert(queryHandler.waitFor, 'Query handler does not support waiting'); + return (await queryHandler.waitFor( + this, + updatedSelector, options - ); - if (!handle) { - return null; - } - const mainHandle = (await this.worlds[MAIN_WORLD].adoptHandle( - handle - )) as ElementHandle>; - await handle.dispose(); - return mainHandle; + )) as ElementHandle> | null; } /** diff --git a/src/common/IsolatedWorld.ts b/src/common/IsolatedWorld.ts index 66a79b56471e9..e969bdea607b7 100644 --- a/src/common/IsolatedWorld.ts +++ b/src/common/IsolatedWorld.ts @@ -16,16 +16,19 @@ import {Protocol} from 'devtools-protocol'; import {assert} from '../util/assert.js'; +import { + createDeferredPromise, + DeferredPromise, +} from '../util/DeferredPromise.js'; import {CDPSession} from './Connection.js'; import {ElementHandle} from './ElementHandle.js'; import {TimeoutError} from './Errors.js'; import {ExecutionContext} from './ExecutionContext.js'; -import {FrameManager} from './FrameManager.js'; import {Frame} from './Frame.js'; +import {FrameManager} from './FrameManager.js'; import {MouseButton} from './Input.js'; import {JSHandle} from './JSHandle.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; -import {getQueryHandlerAndSelector} from './QueryHandler.js'; import {TimeoutSettings} from './TimeoutSettings.js'; import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; import { @@ -37,10 +40,6 @@ import { makePredicateString, pageBindingInitString, } from './util.js'; -import { - createDeferredPromise, - DeferredPromise, -} from '../util/DeferredPromise.js'; // predicateQueryHandler and checkWaitForOptions are declared here so that // TypeScript knows about them when used in the predicate function below. @@ -80,10 +79,6 @@ export interface WaitForSelectorOptions { * @defaultValue `30000` (30 seconds) */ timeout?: number; - /** - * @deprecated Do not use. Use the {@link ElementHandle.waitForSelector} - */ - root?: ElementHandle; } /** @@ -122,7 +117,7 @@ export interface IsolatedWorldChart { */ export class IsolatedWorld { #frame: Frame; - #documentPromise: Promise> | null = null; + #document?: ElementHandle; #contextPromise: DeferredPromise = createDeferredPromise(); #detached = false; @@ -169,7 +164,7 @@ export class IsolatedWorld { } clearContext(): void { - this.#documentPromise = null; + this.#document = undefined; this.#contextPromise = createDeferredPromise(); } @@ -248,15 +243,14 @@ export class IsolatedWorld { } async document(): Promise> { - if (this.#documentPromise) { - return this.#documentPromise; + if (this.#document) { + return this.#document; } - this.#documentPromise = this.executionContext().then(async context => { - return await context.evaluateHandle(() => { - return document; - }); + const context = await this.executionContext(); + this.#document = await context.evaluateHandle(() => { + return document; }); - return this.#documentPromise; + return this.#document; } async $x(expression: string): Promise>> { @@ -294,20 +288,6 @@ export class IsolatedWorld { return document.$$eval(selector, pageFunction, ...args); } - async waitForSelector( - selector: Selector, - options: WaitForSelectorOptions - ): Promise> | null> { - const {updatedSelector, queryHandler} = - getQueryHandlerAndSelector(selector); - assert(queryHandler.waitFor, 'Query handler does not support waiting'); - return (await queryHandler.waitFor( - this, - updatedSelector, - options - )) as ElementHandle> | null; - } - async content(): Promise { return await this.evaluate(() => { let retVal = ''; @@ -707,10 +687,11 @@ export class IsolatedWorld { async _waitForSelectorInPage( queryOne: Function, + root: ElementHandle | undefined, selector: string, options: WaitForSelectorOptions, binding?: PageBinding - ): Promise | null> { + ): Promise | null> { const { visible: waitForVisible = false, hidden: waitForHidden = false, @@ -738,16 +719,10 @@ export class IsolatedWorld { timeout, args: [selector, waitForVisible, waitForHidden], binding, - root: options.root, + root, }; const waitTask = new WaitTask(waitTaskOptions); - const jsHandle = await waitTask.promise; - const elementHandle = jsHandle.asElement(); - if (!elementHandle) { - await jsHandle.dispose(); - return null; - } - return elementHandle; + return waitTask.promise; } waitForFunction( @@ -798,6 +773,12 @@ export class IsolatedWorld { }); return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T; } + + async transferHandle>(handle: T): Promise { + const result = await this.adoptHandle(handle); + await handle.dispose(); + return result; + } } /** diff --git a/src/common/Page.ts b/src/common/Page.ts index e652e5a630b4e..f7e8a769efb41 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -3396,7 +3396,7 @@ export class Page extends EventEmitter { */ async waitForSelector( selector: Selector, - options: Exclude = {} + options: WaitForSelectorOptions = {} ): Promise> | null> { return await this.mainFrame().waitForSelector(selector, options); } diff --git a/src/common/QueryHandler.ts b/src/common/QueryHandler.ts index dc73558ec43b6..5fd360e06d5d1 100644 --- a/src/common/QueryHandler.ts +++ b/src/common/QueryHandler.ts @@ -16,7 +16,12 @@ import {ariaHandler} from './AriaQueryHandler.js'; import {ElementHandle} from './ElementHandle.js'; -import {IsolatedWorld, WaitForSelectorOptions} from './IsolatedWorld.js'; +import {Frame} from './Frame.js'; +import { + MAIN_WORLD, + PUPPETEER_WORLD, + WaitForSelectorOptions, +} from './IsolatedWorld.js'; /** * @public @@ -58,11 +63,9 @@ export interface InternalQueryHandler { /** * Waits until a single node appears for a given selector and * {@link ElementHandle}. - * - * Akin to {@link Window.prototype.querySelectorAll}. */ waitFor?: ( - isolatedWorld: IsolatedWorld, + elementOrFrame: ElementHandle | Frame, selector: string, options: WaitForSelectorOptions ) => Promise | null>; @@ -84,12 +87,34 @@ function internalizeCustomQueryHandler( await jsHandle.dispose(); return null; }; - internalHandler.waitFor = ( - domWorld: IsolatedWorld, - selector: string, - options: WaitForSelectorOptions - ) => { - return domWorld._waitForSelectorInPage(queryOne, selector, options); + internalHandler.waitFor = async (elementOrFrame, selector, options) => { + let frame: Frame; + let element: ElementHandle | undefined; + if (elementOrFrame instanceof Frame) { + frame = elementOrFrame; + } else { + frame = elementOrFrame.frame; + element = await frame.worlds[PUPPETEER_WORLD].adoptHandle( + elementOrFrame + ); + } + const result = await frame.worlds[PUPPETEER_WORLD]._waitForSelectorInPage( + queryOne, + element, + selector, + options + ); + if (element) { + await element.dispose(); + } + if (!result) { + return null; + } + if (!(result instanceof ElementHandle)) { + await result.dispose(); + return null; + } + return frame.worlds[MAIN_WORLD].transferHandle(result); }; }