diff --git a/src/common/DOMWorld.ts b/src/common/DOMWorld.ts index a6d7d05141082..1db47d1fd5296 100644 --- a/src/common/DOMWorld.ts +++ b/src/common/DOMWorld.ts @@ -717,52 +717,6 @@ export class DOMWorld { return elementHandle; } - async waitForXPath( - xpath: string, - options: WaitForSelectorOptions - ): Promise | null> { - const { - visible: waitForVisible = false, - hidden: waitForHidden = false, - timeout = this.#timeoutSettings.timeout(), - } = options; - const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation'; - const title = `XPath \`${xpath}\`${waitForHidden ? ' to be hidden' : ''}`; - function predicate( - root: Element | Document, - xpath: string, - waitForVisible: boolean, - waitForHidden: boolean - ): Node | null | boolean { - const node = document.evaluate( - xpath, - root, - null, - XPathResult.FIRST_ORDERED_NODE_TYPE, - null - ).singleNodeValue; - return checkWaitForOptions(node, waitForVisible, waitForHidden); - } - const waitTaskOptions: WaitTaskOptions = { - domWorld: this, - predicateBody: makePredicateString(predicate), - predicateAcceptsContextElement: true, - title, - polling, - timeout, - args: [xpath, waitForVisible, waitForHidden], - root: options.root, - }; - const waitTask = new WaitTask(waitTaskOptions); - const jsHandle = await waitTask.promise; - const elementHandle = jsHandle.asElement(); - if (!elementHandle) { - await jsHandle.dispose(); - return null; - } - return elementHandle; - } - waitForFunction( pageFunction: Function | string, options: {polling?: string | number; timeout?: number} = {}, diff --git a/src/common/ElementHandle.ts b/src/common/ElementHandle.ts index dbd7d8ccf9217..03df178e88780 100644 --- a/src/common/ElementHandle.ts +++ b/src/common/ElementHandle.ts @@ -140,6 +140,8 @@ export class ElementHandle< } /** + * @deprecated Use {@link waitForSelector} with an xpath selector. + * * Wait for the `xpath` within the element. If at the moment of calling the * method the `xpath` already exists, the method will return immediately. If * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the @@ -197,27 +199,7 @@ export class ElementHandle< timeout?: number; } = {} ): Promise | null> { - const frame = this._context.frame(); - assert(frame); - const secondaryContext = await frame._secondaryWorld.executionContext(); - const adoptedRoot = await secondaryContext._adoptElementHandle(this); - xpath = xpath.startsWith('//') ? '.' + xpath : xpath; - if (!xpath.startsWith('.//')) { - await adoptedRoot.dispose(); - throw new Error('Unsupported xpath expression: ' + xpath); - } - const handle = await frame._secondaryWorld.waitForXPath(xpath, { - ...options, - root: adoptedRoot, - }); - await adoptedRoot.dispose(); - if (!handle) { - return null; - } - const mainExecutionContext = await frame._mainWorld.executionContext(); - const result = await mainExecutionContext._adoptElementHandle(handle); - await handle.dispose(); - return result; + return this.waitForSelector(xpath, options); } override asElement(): ElementHandle | null { @@ -964,36 +946,14 @@ export class ElementHandle< } /** + * @deprecated Use {@link this.$$}. + * * The method evaluates the XPath expression relative to the elementHandle. * If there are no such elements, the method will resolve to an empty array. * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate} */ async $x(expression: string): Promise>> { - const arrayHandle = await this.evaluateHandle((element, expression) => { - const doc = element.ownerDocument || document; - const iterator = doc.evaluate( - expression, - element, - null, - XPathResult.ORDERED_NODE_ITERATOR_TYPE - ); - const array = []; - let item; - while ((item = iterator.iterateNext())) { - array.push(item); - } - return array; - }, expression); - const properties = await arrayHandle.getProperties(); - await arrayHandle.dispose(); - const result = []; - for (const property of properties.values()) { - const elementHandle = property.asElement(); - if (elementHandle) { - result.push(elementHandle); - } - } - return result; + return this.$$(expression); } /** diff --git a/src/common/FrameManager.ts b/src/common/FrameManager.ts index 25f8067f2da05..a39efa369ac01 100644 --- a/src/common/FrameManager.ts +++ b/src/common/FrameManager.ts @@ -1374,7 +1374,8 @@ export class Frame { } /** - * @remarks + * @deprecated Use {@link waitForSelector}. + * * Wait for the `xpath` to appear in page. If at the moment of calling the * method the `xpath` already exists, the method will return immediately. If * the xpath doesn't appear after the `timeout` milliseconds of waiting, the @@ -1392,14 +1393,7 @@ export class Frame { xpath: string, options: WaitForSelectorOptions = {} ): Promise | null> { - const handle = await this._secondaryWorld.waitForXPath(xpath, options); - if (!handle) { - return null; - } - const mainExecutionContext = await this._mainWorld.executionContext(); - const result = await mainExecutionContext._adoptElementHandle(handle); - await handle.dispose(); - return result; + return this.waitForSelector(xpath, options); } /** diff --git a/src/common/QueryHandler.ts b/src/common/QueryHandler.ts index f9a140f8f79db..99bcda808288c 100644 --- a/src/common/QueryHandler.ts +++ b/src/common/QueryHandler.ts @@ -19,47 +19,66 @@ import {DOMWorld, WaitForSelectorOptions} from './DOMWorld.js'; import {ElementHandle} from './ElementHandle.js'; import {JSHandle} from './JSHandle.js'; +/** + * @public + */ +export interface CustomQueryHandler { + /** + * @returns A {@link Node} matching the given {@link selector} from {@link node}. + */ + queryOne?: (node: Node, selector: string) => Node | null; + /** + * @returns Some {@link Node}s matching the given {@link selector} from {@link node}. + */ + queryAll?: (node: Node, selector: string) => Node[]; +} + /** * @internal */ export interface InternalQueryHandler { + /** + * Queries for a single node given a selector and {@link ElementHandle}. + * + * Akin to {@link Window.prototype.querySelector}. + */ queryOne?: ( element: ElementHandle, selector: string ) => Promise | null>; + /** + * Queries for multiple nodes given a selector and {@link ElementHandle}. + * + * Akin to {@link Window.prototype.querySelectorAll}. + */ queryAll?: ( element: ElementHandle, selector: string ) => Promise>>; - + /** + * Queries for multiple nodes given a selector and {@link ElementHandle}. + * Unlike {@link queryAll}, this returns a handle to a node array. + * + * Akin to {@link Window.prototype.querySelectorAll}. + */ + queryAllArray?: ( + element: ElementHandle, + selector: string + ) => Promise>; + /** + * Waits until a single node appears for a given selector and + * {@link ElementHandle}. + * + * Akin to {@link Window.prototype.querySelectorAll}. + */ waitFor?: ( domWorld: DOMWorld, selector: string, options: WaitForSelectorOptions ) => Promise | null>; - queryAllArray?: ( - element: ElementHandle, - selector: string - ) => Promise>; -} - -/** - * Contains two functions `queryOne` and `queryAll` that can - * be {@link registerCustomQueryHandler | registered} - * as alternative querying strategies. The functions `queryOne` and `queryAll` - * are executed in the page context. `queryOne` should take an `Element` and a - * selector string as argument and return a single `Element` or `null` if no - * element is found. `queryAll` takes the same arguments but should instead - * return a `NodeListOf` or `Array` with all the elements - * that match the given query selector. - * @public - */ -export interface CustomQueryHandler { - queryOne?: (element: Node, selector: string) => Node | null; - queryAll?: (element: Node, selector: string) => Node[]; } -function createInternalQueryHandler( +function internalizeCustomQueryHandler( handler: CustomQueryHandler ): InternalQueryHandler { const internalHandler: InternalQueryHandler = {}; @@ -114,7 +133,7 @@ function createInternalQueryHandler( return internalHandler; } -const defaultHandler = createInternalQueryHandler({ +const defaultHandler = internalizeCustomQueryHandler({ queryOne: (element, selector) => { if (!('querySelector' in element)) { throw new Error( @@ -141,7 +160,7 @@ const defaultHandler = createInternalQueryHandler({ }, }); -const pierceHandler = createInternalQueryHandler({ +const pierceHandler = internalizeCustomQueryHandler({ queryOne: (element, selector) => { let found: Node | null = null; const search = (root: Node) => { @@ -191,11 +210,46 @@ const pierceHandler = createInternalQueryHandler({ }, }); -const builtInHandlers = new Map([ - ['aria', ariaHandler], - ['pierce', pierceHandler], +const xpathHandler = internalizeCustomQueryHandler({ + queryOne: (element, selector) => { + const doc = element.ownerDocument || document; + const iterator = doc.evaluate( + selector, + element, + null, + XPathResult.ORDERED_NODE_ITERATOR_TYPE + ); + return iterator.iterateNext(); + }, + + queryAll: (element, selector) => { + const doc = element.ownerDocument || document; + const iterator = doc.evaluate( + selector, + element, + null, + XPathResult.ORDERED_NODE_ITERATOR_TYPE + ); + const array: Node[] = []; + let item; + while ((item = iterator.iterateNext())) { + array.push(item); + } + return array; + }, +}); + +interface InternalQueryHandlerInfo { + test: RegExp; + handler: InternalQueryHandler; +} + +const INTERNAL_QUERY_HANDLERS = new Map([ + ['aria', {test: /$aria\//g, handler: ariaHandler}], + ['pierce', {test: /$pierce\//g, handler: pierceHandler}], + ['xpath', {test: /$\.?\/\//, handler: xpathHandler}], ]); -const queryHandlers = new Map(builtInHandlers); +const QUERY_HANDLERS = new Map(); /** * Registers a {@link CustomQueryHandler | custom query handler}. @@ -222,7 +276,10 @@ export function registerCustomQueryHandler( name: string, handler: CustomQueryHandler ): void { - if (queryHandlers.get(name)) { + if (INTERNAL_QUERY_HANDLERS.has(name)) { + throw new Error(`A query handler named "${name}" already exists`); + } + if (QUERY_HANDLERS.has(name)) { throw new Error(`A custom query handler named "${name}" already exists`); } @@ -231,9 +288,7 @@ export function registerCustomQueryHandler( throw new Error(`Custom query handler names may only contain [a-zA-Z]`); } - const internalHandler = createInternalQueryHandler(handler); - - queryHandlers.set(name, internalHandler); + QUERY_HANDLERS.set(name, internalizeCustomQueryHandler(handler)); } /** @@ -242,9 +297,7 @@ export function registerCustomQueryHandler( * @public */ export function unregisterCustomQueryHandler(name: string): void { - if (queryHandlers.has(name) && !builtInHandlers.has(name)) { - queryHandlers.delete(name); - } + QUERY_HANDLERS.delete(name); } /** @@ -253,9 +306,7 @@ export function unregisterCustomQueryHandler(name: string): void { * @public */ export function customQueryHandlerNames(): string[] { - return [...queryHandlers.keys()].filter(name => { - return !builtInHandlers.has(name); - }); + return [...QUERY_HANDLERS.keys()]; } /** @@ -264,7 +315,7 @@ export function customQueryHandlerNames(): string[] { * @public */ export function clearCustomQueryHandlers(): void { - customQueryHandlerNames().forEach(unregisterCustomQueryHandler); + QUERY_HANDLERS.clear(); } /** @@ -274,23 +325,15 @@ export function getQueryHandlerAndSelector(selector: string): { updatedSelector: string; queryHandler: InternalQueryHandler; } { - const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector); - if (!hasCustomQueryHandler) { - return {updatedSelector: selector, queryHandler: defaultHandler}; + for (const [name, handler] of QUERY_HANDLERS) { + if (selector.startsWith(`${name}/`)) { + return {updatedSelector: selector, queryHandler: handler}; + } } - - const index = selector.indexOf('/'); - const name = selector.slice(0, index); - const updatedSelector = selector.slice(index + 1); - const queryHandler = queryHandlers.get(name); - if (!queryHandler) { - throw new Error( - `Query set to use "${name}", but no query handler of that name was found` - ); + for (const [, handlerInfo] of INTERNAL_QUERY_HANDLERS) { + if (selector.match(handlerInfo.test)) { + return {updatedSelector: selector, queryHandler: handlerInfo.handler}; + } } - - return { - updatedSelector, - queryHandler, - }; + return {updatedSelector: selector, queryHandler: defaultHandler}; }