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