From 83ca4eff4317938f89b7811fbbf76534ec1b7b89 Mon Sep 17 00:00:00 2001 From: Randolf J Date: Thu, 23 Jun 2022 04:59:31 +0200 Subject: [PATCH] feat!: use `ElementHandle` and `Iterable`s --- src/common/Accessibility.ts | 2 +- src/common/AriaQueryHandler.ts | 12 +-- src/common/DOMWorld.ts | 123 ++++++++++--------------- src/common/ElementHandle.ts | 90 +++++++++++------- src/common/ExecutionContext.ts | 8 +- src/common/FrameManager.ts | 57 +++++++----- src/common/JSHandle.ts | 2 +- src/common/Page.ts | 55 ++++++----- src/common/QueryHandler.ts | 123 ++++++++++++------------- src/common/types.ts | 3 +- src/common/util.ts | 2 +- test/src/ariaqueryhandler.spec.ts | 71 +++++++------- test/src/elementhandle.spec.ts | 115 ++++++++++++++++------- test/src/queryselector.spec.ts | 148 +++++++++++++++++------------- test/src/utils.ts | 35 +++++++ 15 files changed, 486 insertions(+), 360 deletions(-) diff --git a/src/common/Accessibility.ts b/src/common/Accessibility.ts index 2881040e93dfa..0406942108db0 100644 --- a/src/common/Accessibility.ts +++ b/src/common/Accessibility.ts @@ -104,7 +104,7 @@ export interface SnapshotOptions { * Root node to get the accessibility tree for * @defaultValue The root node of the entire page. */ - root?: ElementHandle; + root?: ElementHandle; } /** diff --git a/src/common/AriaQueryHandler.ts b/src/common/AriaQueryHandler.ts index 1e32307823806..430ac1191a33b 100644 --- a/src/common/AriaQueryHandler.ts +++ b/src/common/AriaQueryHandler.ts @@ -24,7 +24,7 @@ import {InternalQueryHandler} from './QueryHandler.js'; async function queryAXTree( client: CDPSession, - element: ElementHandle, + element: ElementHandle, accessibleName?: string, role?: string ): Promise { @@ -86,9 +86,9 @@ function parseAriaSelector(selector: string): ARIAQueryOption { } const queryOne = async ( - element: ElementHandle, + element: ElementHandle, selector: string -): Promise => { +): Promise | null> => { const exeCtx = element.executionContext(); const {name, role} = parseAriaSelector(selector); const res = await queryAXTree(exeCtx._client, element, name, role); @@ -126,9 +126,9 @@ const waitFor = async ( }; const queryAll = async ( - element: ElementHandle, + element: ElementHandle, selector: string -): Promise => { +): Promise[]> => { const exeCtx = element.executionContext(); const {name, role} = parseAriaSelector(selector); const res = await queryAXTree(exeCtx._client, element, name, role); @@ -140,7 +140,7 @@ const queryAll = async ( }; const queryAllArray = async ( - element: ElementHandle, + element: ElementHandle, selector: string ): Promise> => { const elementHandles = await queryAll(element, selector); diff --git a/src/common/DOMWorld.ts b/src/common/DOMWorld.ts index ca8bcdf8bfcfc..3a613b774d303 100644 --- a/src/common/DOMWorld.ts +++ b/src/common/DOMWorld.ts @@ -26,7 +26,12 @@ import {JSHandle} from './JSHandle.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {_getQueryHandlerAndSelector} from './QueryHandler.js'; import {TimeoutSettings} from './TimeoutSettings.js'; -import {EvaluateFunc, EvaluateParams, HandleFor} from './types.js'; +import { + AwaitableIteratable, + EvaluateFunc, + EvaluateParams, + HandleFor, +} from './types.js'; import { debugError, isNumber, @@ -54,7 +59,7 @@ export interface WaitForSelectorOptions { visible?: boolean; hidden?: boolean; timeout?: number; - root?: ElementHandle; + root?: ElementHandle; } /** @@ -73,7 +78,7 @@ export class DOMWorld { #client: CDPSession; #frame: Frame; #timeoutSettings: TimeoutSettings; - #documentPromise: Promise | null = null; + #documentPromise: Promise> | null = null; #contextPromise: Promise | null = null; #contextResolveCallback: ((x: ExecutionContext) => void) | null = null; #detached = false; @@ -203,8 +208,8 @@ export class DOMWorld { async $( selector: Selector ): Promise | null>; - async $(selector: string): Promise; - async $(selector: string): Promise { + async $(selector: string): Promise | null>; + async $(selector: string): Promise | null> { const document = await this._document(); const value = await document.$(selector); return value; @@ -212,33 +217,35 @@ export class DOMWorld { async $$( selector: Selector - ): Promise[]>; - async $$(selector: string): Promise; - async $$(selector: string): Promise { + ): Promise< + AwaitableIteratable> + >; + async $$(selector: string): Promise>>; + async $$( + selector: string + ): Promise>> { const document = await this._document(); - const value = await document.$$(selector); - return value; + return document.$$(selector); } /** * @internal */ - async _document(): Promise { + async _document(): Promise> { if (this.#documentPromise) { return this.#documentPromise; } this.#documentPromise = this.executionContext().then(async context => { - const document = await context.evaluateHandle('document'); - const element = document.asElement(); - if (element === null) { - throw new Error('Document is null'); - } - return element; + return await context.evaluateHandle(() => { + return document; + }); }); return this.#documentPromise; } - async $x(expression: string): Promise { + async $x( + expression: string + ): Promise>> { const document = await this._document(); const value = await document.$x(expression); return value; @@ -257,8 +264,8 @@ export class DOMWorld { ): Promise>>; async $eval< Params extends unknown[], - Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< - [Element, ...Params] + Func extends EvaluateFunc<[Node, ...Params]> = EvaluateFunc< + [Node, ...Params] > >( selector: string, @@ -267,8 +274,8 @@ export class DOMWorld { ): Promise>>; async $eval< Params extends unknown[], - Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< - [Element, ...Params] + Func extends EvaluateFunc<[Node, ...Params]> = EvaluateFunc< + [Node, ...Params] > >( selector: string, @@ -283,8 +290,8 @@ export class DOMWorld { Selector extends keyof HTMLElementTagNameMap, Params extends unknown[], Func extends EvaluateFunc< - [HTMLElementTagNameMap[Selector][], ...Params] - > = EvaluateFunc<[HTMLElementTagNameMap[Selector][], ...Params]> + [Iterable, ...Params] + > = EvaluateFunc<[Iterable, ...Params]> >( selector: Selector, pageFunction: Func | string, @@ -292,8 +299,8 @@ export class DOMWorld { ): Promise>>; async $$eval< Params extends unknown[], - Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< - [Element[], ...Params] + Func extends EvaluateFunc<[Iterable, ...Params]> = EvaluateFunc< + [Iterable, ...Params] > >( selector: string, @@ -302,8 +309,8 @@ export class DOMWorld { ): Promise>>; async $$eval< Params extends unknown[], - Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< - [Element[], ...Params] + Func extends EvaluateFunc<[Iterable, ...Params]> = EvaluateFunc< + [Iterable, ...Params] > >( selector: string, @@ -377,7 +384,7 @@ export class DOMWorld { content?: string; id?: string; type?: string; - }): Promise { + }): Promise> { const { url = null, path = null, @@ -388,17 +395,7 @@ export class DOMWorld { if (url !== null) { try { const context = await this.executionContext(); - const handle = await context.evaluateHandle( - addScriptUrl, - url, - id, - type - ); - const elementHandle = handle.asElement(); - if (elementHandle === null) { - throw new Error('Script element is not found'); - } - return elementHandle; + return await context.evaluateHandle(addScriptUrl, url, id, type); } catch (error) { throw new Error(`Loading script from ${url} failed`); } @@ -419,43 +416,19 @@ export class DOMWorld { let contents = await fs.readFile(path, 'utf8'); contents += '//# sourceURL=' + path.replace(/\n/g, ''); const context = await this.executionContext(); - const handle = await context.evaluateHandle( - addScriptContent, - contents, - id, - type - ); - const elementHandle = handle.asElement(); - if (elementHandle === null) { - throw new Error('Script element is not found'); - } - return elementHandle; + return await context.evaluateHandle(addScriptContent, contents, id, type); } if (content !== null) { const context = await this.executionContext(); - const handle = await context.evaluateHandle( - addScriptContent, - content, - id, - type - ); - const elementHandle = handle.asElement(); - if (elementHandle === null) { - throw new Error('Script element is not found'); - } - return elementHandle; + return await context.evaluateHandle(addScriptContent, content, id, type); } throw new Error( 'Provide an object with a `url`, `path` or `content` property' ); - async function addScriptUrl( - url: string, - id: string, - type: string - ): Promise { + async function addScriptUrl(url: string, id: string, type: string) { const script = document.createElement('script'); script.src = url; if (id) { @@ -477,7 +450,7 @@ export class DOMWorld { content: string, id: string, type = 'text/javascript' - ): HTMLElement { + ) { const script = document.createElement('script'); script.type = type; script.text = content; @@ -510,7 +483,7 @@ export class DOMWorld { url?: string; path?: string; content?: string; - }): Promise { + }): Promise> { const {url = null, path = null, content = null} = options; if (url !== null) { try { @@ -647,11 +620,11 @@ export class DOMWorld { async waitForSelector( selector: string, options: WaitForSelectorOptions - ): Promise; + ): Promise | null>; async waitForSelector( selector: string, options: WaitForSelectorOptions - ): Promise { + ): Promise | null> { const {updatedSelector, queryHandler} = _getQueryHandlerAndSelector(selector); assert(queryHandler.waitFor, 'Query handler does not support waiting'); @@ -784,7 +757,7 @@ export class DOMWorld { selector: string, options: WaitForSelectorOptions, binding?: PageBinding - ): Promise { + ): Promise | null> { const { visible: waitForVisible = false, hidden: waitForHidden = false, @@ -829,7 +802,7 @@ export class DOMWorld { async waitForXPath( xpath: string, options: WaitForSelectorOptions - ): Promise { + ): Promise | null> { const { visible: waitForVisible = false, hidden: waitForHidden = false, @@ -911,7 +884,7 @@ export interface WaitTaskOptions { timeout: number; binding?: PageBinding; args: unknown[]; - root?: ElementHandle; + root?: ElementHandle; } const noop = (): void => {}; @@ -932,7 +905,7 @@ export class WaitTask { #reject: (x: Error) => void = noop; #timeoutTimer?: NodeJS.Timeout; #terminated = false; - #root: ElementHandle | null = null; + #root: ElementHandle | null = null; promise: Promise; diff --git a/src/common/ElementHandle.ts b/src/common/ElementHandle.ts index aaf06c2352f3c..5e3633986c5ae 100644 --- a/src/common/ElementHandle.ts +++ b/src/common/ElementHandle.ts @@ -16,7 +16,7 @@ import { } from './JSHandle.js'; import {Page, ScreenshotOptions} from './Page.js'; import {_getQueryHandlerAndSelector} from './QueryHandler.js'; -import {EvaluateFunc, EvaluateParams} from './types.js'; +import {AwaitableIteratable, EvaluateFunc, EvaluateParams} from './types.js'; import {KeyInput} from './USKeyboardLayout.js'; import {debugError, isString} from './util.js'; @@ -66,7 +66,7 @@ const applyOffsetsToQuad = ( */ export class ElementHandle< - ElementType extends Element = Element + ElementType extends Node = Element > extends JSHandle { #frame: Frame; #page: Page; @@ -126,11 +126,11 @@ export class ElementHandle< async waitForSelector( selector: string, options?: Exclude - ): Promise; + ): Promise | null>; async waitForSelector( selector: string, options: Exclude = {} - ): Promise { + ): Promise | null> { const frame = this._context.frame(); assert(frame); const secondaryContext = await frame._secondaryWorld.executionContext(); @@ -206,7 +206,7 @@ export class ElementHandle< hidden?: boolean; timeout?: number; } = {} - ): Promise { + ): Promise | null> { const frame = this._context.frame(); assert(frame); const secondaryContext = await frame._secondaryWorld.executionContext(); @@ -248,7 +248,7 @@ export class ElementHandle< return this.#frameManager.frame(nodeInfo.node.frameId); } - async #scrollIntoViewIfNeeded(): Promise { + async #scrollIntoViewIfNeeded(this: ElementHandle): Promise { const error = await this.evaluate( async (element, pageJavascriptEnabled): Promise => { if (!element.isConnected) { @@ -434,7 +434,7 @@ export class ElementHandle< * uses {@link Page.mouse} to hover over the center of the element. * If the element is detached from DOM, the method throws an error. */ - async hover(): Promise { + async hover(this: ElementHandle): Promise { await this.#scrollIntoViewIfNeeded(); const {x, y} = await this.clickablePoint(); await this.#page.mouse.move(x, y); @@ -445,7 +445,10 @@ export class ElementHandle< * uses {@link Page.mouse} to click in the center of the element. * If the element is detached from DOM, the method throws an error. */ - async click(options: ClickOptions = {}): Promise { + async click( + this: ElementHandle, + options: ClickOptions = {} + ): Promise { await this.#scrollIntoViewIfNeeded(); const {x, y} = await this.clickablePoint(options.offset); await this.#page.mouse.click(x, y, options); @@ -454,7 +457,10 @@ export class ElementHandle< /** * This method creates and captures a dragevent from the element. */ - async drag(target: Point): Promise { + async drag( + this: ElementHandle, + target: Point + ): Promise { assert( this.#page.isDragInterceptionEnabled(), 'Drag Interception is not enabled!' @@ -468,6 +474,7 @@ export class ElementHandle< * This method creates a `dragenter` event on the element. */ async dragEnter( + this: ElementHandle, data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -479,6 +486,7 @@ export class ElementHandle< * This method creates a `dragover` event on the element. */ async dragOver( + this: ElementHandle, data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -490,6 +498,7 @@ export class ElementHandle< * This method triggers a drop on the element. */ async drop( + this: ElementHandle, data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -501,7 +510,8 @@ export class ElementHandle< * This method triggers a dragenter, dragover, and drop on the element. */ async dragAndDrop( - target: ElementHandle, + this: ElementHandle, + target: ElementHandle, options?: {delay: number} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -639,7 +649,7 @@ export class ElementHandle< * {@link Touchscreen.tap} to tap in the center of the element. * If the element is detached from DOM, the method throws an error. */ - async tap(): Promise { + async tap(this: ElementHandle): Promise { await this.#scrollIntoViewIfNeeded(); const {x, y} = await this.clickablePoint(); await this.#page.touchscreen.tap(x, y); @@ -773,7 +783,10 @@ export class ElementHandle< * {@link Page.screenshot} to take a screenshot of the element. * If the element is detached from DOM, the method throws an error. */ - async screenshot(options: ScreenshotOptions = {}): Promise { + async screenshot( + this: ElementHandle, + options: ScreenshotOptions = {} + ): Promise { let needsViewportReset = false; let boundingBox = await this.boundingBox(); @@ -838,8 +851,8 @@ export class ElementHandle< async $( selector: Selector ): Promise | null>; - async $(selector: string): Promise; - async $(selector: string): Promise { + async $(selector: string): Promise | null>; + async $(selector: string): Promise | null> { const {updatedSelector, queryHandler} = _getQueryHandlerAndSelector(selector); assert( @@ -862,16 +875,20 @@ export class ElementHandle< */ async $$( selector: Selector - ): Promise[]>; - async $$(selector: string): Promise; - async $$(selector: string): Promise { + ): Promise< + AwaitableIteratable> + >; + async $$(selector: string): Promise>>; + async $$( + selector: string + ): Promise>> { const {updatedSelector, queryHandler} = _getQueryHandlerAndSelector(selector); assert( queryHandler.queryAll, 'Cannot handle queries for a multiple element with the given selector' ); - return queryHandler.queryAll(this, updatedSelector); + return await queryHandler.queryAll(this, updatedSelector); } /** @@ -902,8 +919,8 @@ export class ElementHandle< ): Promise>>; async $eval< Params extends unknown[], - Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< - [Element, ...Params] + Func extends EvaluateFunc<[Node, ...Params]> = EvaluateFunc< + [Node, ...Params] > >( selector: string, @@ -912,8 +929,8 @@ export class ElementHandle< ): Promise>>; async $eval< Params extends unknown[], - Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< - [Element, ...Params] + Func extends EvaluateFunc<[Node, ...Params]> = EvaluateFunc< + [Node, ...Params] > >( selector: string, @@ -958,8 +975,8 @@ export class ElementHandle< Selector extends keyof HTMLElementTagNameMap, Params extends unknown[], Func extends EvaluateFunc< - [HTMLElementTagNameMap[Selector][], ...Params] - > = EvaluateFunc<[HTMLElementTagNameMap[Selector][], ...Params]> + [Iterable, ...Params] + > = EvaluateFunc<[Iterable, ...Params]> >( selector: Selector, pageFunction: Func | string, @@ -967,8 +984,8 @@ export class ElementHandle< ): Promise>>; async $$eval< Params extends unknown[], - Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< - [Element[], ...Params] + Func extends EvaluateFunc<[Iterable, ...Params]> = EvaluateFunc< + [Iterable, ...Params] > >( selector: string, @@ -977,8 +994,8 @@ export class ElementHandle< ): Promise>>; async $$eval< Params extends unknown[], - Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< - [Element[], ...Params] + Func extends EvaluateFunc<[Iterable, ...Params]> = EvaluateFunc< + [Iterable, ...Params] > >( selector: string, @@ -999,10 +1016,12 @@ export class 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 { + async $x( + expression: string + ): Promise>> { const arrayHandle = await this.evaluateHandle((element, expression) => { - const document = element.ownerDocument || element; - const iterator = document.evaluate( + const doc = element.ownerDocument || document; + const iterator = doc.evaluate( expression, element, null, @@ -1030,9 +1049,12 @@ export class ElementHandle< /** * Resolves to true if the element is visible in the current viewport. */ - async isIntersectingViewport(options?: { - threshold?: number; - }): Promise { + async isIntersectingViewport( + this: ElementHandle, + options?: { + threshold?: number; + } + ): Promise { const {threshold = 0} = options ?? {}; return await this.evaluate(async (element, threshold) => { const visibleRatio = await new Promise(resolve => { diff --git a/src/common/ExecutionContext.ts b/src/common/ExecutionContext.ts index fd02fd9d07963..c8704ac551e11 100644 --- a/src/common/ExecutionContext.ts +++ b/src/common/ExecutionContext.ts @@ -415,20 +415,20 @@ export class ExecutionContext { */ async _adoptBackendNodeId( backendNodeId?: Protocol.DOM.BackendNodeId - ): Promise { + ): Promise> { const {object} = await this._client.send('DOM.resolveNode', { backendNodeId: backendNodeId, executionContextId: this._contextId, }); - return _createJSHandle(this, object) as ElementHandle; + return _createJSHandle(this, object) as ElementHandle; } /** * @internal */ async _adoptElementHandle( - elementHandle: ElementHandle - ): Promise { + elementHandle: ElementHandle + ): Promise> { assert( elementHandle.executionContext() !== this, 'Cannot adopt handle that already belongs to this execution context' diff --git a/src/common/FrameManager.ts b/src/common/FrameManager.ts index b744f333938e1..8a78473cd77a7 100644 --- a/src/common/FrameManager.ts +++ b/src/common/FrameManager.ts @@ -27,7 +27,12 @@ import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {NetworkManager} from './NetworkManager.js'; import {Page} from './Page.js'; import {TimeoutSettings} from './TimeoutSettings.js'; -import {EvaluateFunc, EvaluateParams, HandleFor} from './types.js'; +import { + AwaitableIteratable, + EvaluateFunc, + EvaluateParams, + HandleFor, +} from './types.js'; import {debugError, isErrorLike} from './util.js'; const UTILITY_WORLD_NAME = '__puppeteer_utility_world__'; @@ -923,8 +928,8 @@ export class Frame { async $( selector: Selector ): Promise | null>; - async $(selector: string): Promise; - async $(selector: string): Promise { + async $(selector: string): Promise | null>; + async $(selector: string): Promise | null> { return this._mainWorld.$(selector); } @@ -936,9 +941,13 @@ export class Frame { */ async $$( selector: Selector - ): Promise[]>; - async $$(selector: string): Promise; - async $$(selector: string): Promise { + ): Promise< + AwaitableIteratable> + >; + async $$(selector: string): Promise>>; + async $$( + selector: string + ): Promise>> { return this._mainWorld.$$(selector); } @@ -947,7 +956,9 @@ export class Frame { * * @param expression - the XPath expression to evaluate. */ - async $x(expression: string): Promise { + async $x( + expression: string + ): Promise>> { return this._mainWorld.$x(expression); } @@ -983,8 +994,8 @@ export class Frame { ): Promise>>; async $eval< Params extends unknown[], - Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< - [Element, ...Params] + Func extends EvaluateFunc<[Node, ...Params]> = EvaluateFunc< + [Node, ...Params] > >( selector: string, @@ -993,8 +1004,8 @@ export class Frame { ): Promise>>; async $eval< Params extends unknown[], - Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< - [Element, ...Params] + Func extends EvaluateFunc<[Node, ...Params]> = EvaluateFunc< + [Node, ...Params] > >( selector: string, @@ -1027,8 +1038,8 @@ export class Frame { Selector extends keyof HTMLElementTagNameMap, Params extends unknown[], Func extends EvaluateFunc< - [HTMLElementTagNameMap[Selector][], ...Params] - > = EvaluateFunc<[HTMLElementTagNameMap[Selector][], ...Params]> + [Iterable, ...Params] + > = EvaluateFunc<[Iterable, ...Params]> >( selector: Selector, pageFunction: Func | string, @@ -1036,8 +1047,8 @@ export class Frame { ): Promise>>; async $$eval< Params extends unknown[], - Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< - [Element[], ...Params] + Func extends EvaluateFunc<[Iterable, ...Params]> = EvaluateFunc< + [Iterable, ...Params] > >( selector: string, @@ -1046,8 +1057,8 @@ export class Frame { ): Promise>>; async $$eval< Params extends unknown[], - Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< - [Element[], ...Params] + Func extends EvaluateFunc<[Iterable, ...Params]> = EvaluateFunc< + [Iterable, ...Params] > >( selector: string, @@ -1134,7 +1145,7 @@ export class Frame { */ async addScriptTag( options: FrameAddScriptTagOptions - ): Promise { + ): Promise> { return this._mainWorld.addScriptTag(options); } @@ -1148,7 +1159,9 @@ export class Frame { * `onload` event fires or when the CSS content was injected into the * frame. */ - async addStyleTag(options: FrameAddStyleTagOptions): Promise { + async addStyleTag( + options: FrameAddStyleTagOptions + ): Promise> { return this._mainWorld.addStyleTag(options); } @@ -1352,11 +1365,11 @@ export class Frame { async waitForSelector( selector: string, options?: WaitForSelectorOptions - ): Promise; + ): Promise | null>; async waitForSelector( selector: string, options: WaitForSelectorOptions = {} - ): Promise { + ): Promise | null> { const handle = await this._secondaryWorld.waitForSelector( selector, options @@ -1388,7 +1401,7 @@ export class Frame { async waitForXPath( xpath: string, options: WaitForSelectorOptions = {} - ): Promise { + ): Promise | null> { const handle = await this._secondaryWorld.waitForXPath(xpath, options); if (!handle) { return null; diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index 7cd17f2bbb78f..a2a390de3c1b0 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -245,7 +245,7 @@ export class JSHandle { * @returns Either `null` or the object handle itself, if the object * handle is an instance of {@link ElementHandle}. */ - asElement(): ElementHandle | null { + asElement(): ElementHandle | null { /* This always returns null, but subclasses can override this and return an ElementHandle. */ diff --git a/src/common/Page.ts b/src/common/Page.ts index a58a9bed1582d..49ad8d97f7369 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -49,7 +49,12 @@ import {Target} from './Target.js'; import {TaskQueue} from './TaskQueue.js'; import {TimeoutSettings} from './TimeoutSettings.js'; import {Tracing} from './Tracing.js'; -import {EvaluateFunc, EvaluateParams, HandleFor} from './types.js'; +import { + AwaitableIteratable, + EvaluateFunc, + EvaluateParams, + HandleFor, +} from './types.js'; import { debugError, evaluationString, @@ -1009,8 +1014,8 @@ export class Page extends EventEmitter { async $( selector: Selector ): Promise | null>; - async $(selector: string): Promise; - async $(selector: string): Promise { + async $(selector: string): Promise | null>; + async $(selector: string): Promise | null> { return this.mainFrame().$(selector); } @@ -1023,9 +1028,13 @@ export class Page extends EventEmitter { */ async $$( selector: Selector - ): Promise[]>; - async $$(selector: string): Promise; - async $$(selector: string): Promise { + ): Promise< + AwaitableIteratable> + >; + async $$(selector: string): Promise>>; + async $$( + selector: string + ): Promise>> { return this.mainFrame().$$(selector); } @@ -1194,8 +1203,8 @@ export class Page extends EventEmitter { ): Promise>>; async $eval< Params extends unknown[], - Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< - [Element, ...Params] + Func extends EvaluateFunc<[Node, ...Params]> = EvaluateFunc< + [Node, ...Params] > >( selector: string, @@ -1204,8 +1213,8 @@ export class Page extends EventEmitter { ): Promise>>; async $eval< Params extends unknown[], - Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< - [Element, ...Params] + Func extends EvaluateFunc<[Node, ...Params]> = EvaluateFunc< + [Node, ...Params] > >( selector: string, @@ -1281,8 +1290,8 @@ export class Page extends EventEmitter { Selector extends keyof HTMLElementTagNameMap, Params extends unknown[], Func extends EvaluateFunc< - [HTMLElementTagNameMap[Selector][], ...Params] - > = EvaluateFunc<[HTMLElementTagNameMap[Selector][], ...Params]> + [Iterable, ...Params] + > = EvaluateFunc<[Iterable, ...Params]> >( selector: Selector, pageFunction: Func | string, @@ -1290,8 +1299,8 @@ export class Page extends EventEmitter { ): Promise>>; async $$eval< Params extends unknown[], - Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< - [Element[], ...Params] + Func extends EvaluateFunc<[Iterable, ...Params]> = EvaluateFunc< + [Iterable, ...Params] > >( selector: string, @@ -1300,8 +1309,8 @@ export class Page extends EventEmitter { ): Promise>>; async $$eval< Params extends unknown[], - Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< - [Element[], ...Params] + Func extends EvaluateFunc<[Iterable, ...Params]> = EvaluateFunc< + [Iterable, ...Params] > >( selector: string, @@ -1319,7 +1328,9 @@ export class Page extends EventEmitter { * Shortcut for {@link Frame.$x | Page.mainFrame().$x(expression) }. * @param expression - Expression to evaluate */ - async $x(expression: string): Promise { + async $x( + expression: string + ): Promise>> { return this.mainFrame().$x(expression); } @@ -1402,7 +1413,7 @@ export class Page extends EventEmitter { content?: string; type?: string; id?: string; - }): Promise { + }): Promise> { return this.mainFrame().addScriptTag(options); } @@ -1416,7 +1427,7 @@ export class Page extends EventEmitter { url?: string; path?: string; content?: string; - }): Promise { + }): Promise> { return this.mainFrame().addStyleTag(options); } @@ -3322,11 +3333,11 @@ export class Page extends EventEmitter { async waitForSelector( selector: string, options?: Exclude - ): Promise; + ): Promise | null>; async waitForSelector( selector: string, options: Exclude = {} - ): Promise { + ): Promise | null> { return await this.mainFrame().waitForSelector(selector, options); } @@ -3385,7 +3396,7 @@ export class Page extends EventEmitter { hidden?: boolean; timeout?: number; } = {} - ): Promise { + ): Promise | null> { return this.mainFrame().waitForXPath(xpath, options); } diff --git a/src/common/QueryHandler.ts b/src/common/QueryHandler.ts index b66385f47dbda..1ff8aa02902d4 100644 --- a/src/common/QueryHandler.ts +++ b/src/common/QueryHandler.ts @@ -14,32 +14,34 @@ * limitations under the License. */ -import {WaitForSelectorOptions, DOMWorld} from './DOMWorld.js'; -import {JSHandle} from './JSHandle.js'; import {ariaHandler} from './AriaQueryHandler.js'; +import {DOMWorld, WaitForSelectorOptions} from './DOMWorld.js'; import {ElementHandle} from './ElementHandle.js'; +import {JSHandle} from './JSHandle.js'; +import {AwaitableIteratable} from './types.js'; /** * @internal */ export interface InternalQueryHandler { queryOne?: ( - element: ElementHandle, + element: ElementHandle, + selector: string + ) => Promise | null>; + queryAll?: ( + element: ElementHandle, selector: string - ) => Promise; + ) => Promise>>; + waitFor?: ( domWorld: DOMWorld, selector: string, options: WaitForSelectorOptions - ) => Promise; - queryAll?: ( - element: ElementHandle, - selector: string - ) => Promise; + ) => Promise | null>; queryAllArray?: ( - element: ElementHandle, + element: ElementHandle, selector: string - ) => Promise>; + ) => Promise>>; } /** @@ -54,14 +56,13 @@ export interface InternalQueryHandler { * @public */ export interface CustomQueryHandler { - queryOne?: (element: Element | Document, selector: string) => Element | null; - queryAll?: ( - element: Element | Document, - selector: string - ) => Element[] | NodeListOf; + queryOne?: (element: Node, selector: string) => Node | null; + queryAll?: (element: Node, selector: string) => Iterable; } -function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler { +function createInternalQueryHandler( + handler: CustomQueryHandler +): InternalQueryHandler { const internalHandler: InternalQueryHandler = {}; if (handler.queryOne) { @@ -86,90 +87,84 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler { if (handler.queryAll) { const queryAll = handler.queryAll; - internalHandler.queryAll = async (element, selector) => { - const jsHandle = await element.evaluateHandle(queryAll, selector); - const properties = await jsHandle.getProperties(); - await jsHandle.dispose(); - const result = []; - for (const property of properties.values()) { - const elementHandle = property.asElement(); - if (elementHandle) { - result.push(elementHandle); - } + internalHandler.queryAll = async function (element, selector) { + const iterableHandle = await element.evaluateHandle(queryAll, selector); + const iteratorHandle = await iterableHandle.evaluateHandle(iterable => { + return iterable[Symbol.iterator](); + }); + await iterableHandle.dispose(); + async function* generate(handle: JSHandle>) { + let elementHandle: ElementHandle | null = null; + do { + const nextHandle = await handle.evaluateHandle(iterator => { + return iterator.next().value; + }); + elementHandle = nextHandle.asElement(); + if (elementHandle) { + yield elementHandle; + } + await nextHandle.dispose(); + } while (elementHandle); + await handle.dispose(); } - return result; + return generate(iteratorHandle); }; internalHandler.queryAllArray = async (element, selector) => { - const resultHandle = (await element.evaluateHandle( - queryAll, - selector - )) as JSHandle>; - const arrayHandle = await resultHandle.evaluateHandle(res => { - return Array.from(res); - }); - return arrayHandle; + return await element.evaluateHandle(queryAll, selector); }; } return internalHandler; } -const _defaultHandler = makeQueryHandler({ - queryOne: (element: Element | Document, selector: string) => { - return element.querySelector(selector); +const _defaultHandler = createInternalQueryHandler({ + queryOne: (element, selector) => { + if (element instanceof Element || element instanceof Document) { + return element.querySelector(selector); + } + return null; }, - queryAll: (element: Element | Document, selector: string) => { - return element.querySelectorAll(selector); + queryAll: (element, selector) => { + if (element instanceof Element || element instanceof Document) { + return element.querySelectorAll(selector); + } + return []; }, }); -const pierceHandler = makeQueryHandler({ +const pierceHandler = createInternalQueryHandler({ queryOne: (element, selector) => { let found: Element | null = null; - const search = (root: Element | ShadowRoot) => { + function search(root: Node) { const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); do { - const currentNode = iter.currentNode as HTMLElement; + const currentNode = iter.currentNode as Element; if (currentNode.shadowRoot) { search(currentNode.shadowRoot); } - if (currentNode instanceof ShadowRoot) { - continue; - } if (currentNode !== root && !found && currentNode.matches(selector)) { found = currentNode; } } while (!found && iter.nextNode()); - }; - if (element instanceof Document) { - element = element.documentElement; } search(element); return found; }, queryAll: (element, selector) => { - const result: Element[] = []; - const collect = (root: Element | ShadowRoot) => { + function* collect(root: Node): Generator { const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); do { - const currentNode = iter.currentNode as HTMLElement; + const currentNode = iter.currentNode as Element; if (currentNode.shadowRoot) { - collect(currentNode.shadowRoot); - } - if (currentNode instanceof ShadowRoot) { - continue; + yield* collect(currentNode.shadowRoot); } if (currentNode !== root && currentNode.matches(selector)) { - result.push(currentNode); + yield currentNode; } } while (iter.nextNode()); - }; - if (element instanceof Document) { - element = element.documentElement; } - collect(element); - return result; + return collect(element); }, }); @@ -195,7 +190,7 @@ export function _registerCustomQueryHandler( throw new Error(`Custom query handler names may only contain [a-zA-Z]`); } - const internalHandler = makeQueryHandler(handler); + const internalHandler = createInternalQueryHandler(handler); queryHandlers.set(name, internalHandler); } diff --git a/src/common/types.ts b/src/common/types.ts index 01c31d7f822d6..5da153041e893 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -17,9 +17,10 @@ import {JSHandle} from './JSHandle.js'; import {ElementHandle} from './ElementHandle.js'; +export type AwaitableIteratable = AsyncIterable | Iterable; export type Awaitable = T | PromiseLike; -export type HandleFor = T extends Element ? ElementHandle : JSHandle; +export type HandleFor = T extends Node ? ElementHandle : JSHandle; export type HandleOr = HandleFor | JSHandle | T; export type EvaluateParams = { diff --git a/src/common/util.ts b/src/common/util.ts index 0ca5715f09bc9..203ea90ddf5e0 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -185,7 +185,7 @@ export async function waitForEvent( export function _createJSHandle( context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject -): JSHandle | ElementHandle { +): JSHandle | ElementHandle { const frame = context.frame(); if (remoteObject.subtype === 'node' && frame) { const frameManager = frame._frameManager; diff --git a/test/src/ariaqueryhandler.spec.ts b/test/src/ariaqueryhandler.spec.ts index f5ecfdd666c4a..a895d0db59f88 100644 --- a/test/src/ariaqueryhandler.spec.ts +++ b/test/src/ariaqueryhandler.spec.ts @@ -16,15 +16,26 @@ import expect from 'expect'; import { + describeChromeOnly, getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks, - describeChromeOnly, } from './mocha-utils.js'; -import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js'; -import utils from './utils.js'; import assert from 'assert'; +import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js'; +import {AwaitableIteratable} from '../../lib/cjs/puppeteer/common/types.js'; +import utils, {Iterable} from './utils.js'; + +const getIds = async ( + elements: AwaitableIteratable> +) => { + return Iterable.map(elements, element => { + return element.evaluate(element => { + return element.id; + }); + }); +}; describeChromeOnly('AriaQueryHandler', () => { setupTestBrowserHooks(); @@ -98,8 +109,10 @@ describeChromeOnly('AriaQueryHandler', () => { await page.setContent( '
' ); - const button = (await page.$('aria/[role="button"]'))!; - const id = await button!.evaluate((button: Element) => { + const button = (await page.$( + 'aria/[role="button"]' + )) as ElementHandle; + const id = await button.evaluate(button => { return button.id; }); expect(id).toBe('btn'); @@ -110,8 +123,10 @@ describeChromeOnly('AriaQueryHandler', () => { await page.setContent( '
' ); - const button = (await page.$('aria/Submit[role="button"]'))!; - const id = await button!.evaluate((button: Element) => { + const button = (await page.$( + 'aria/Submit[role="button"]' + )) as ElementHandle; + const id = await button.evaluate(button => { return button.id; }); expect(id).toBe('btn'); @@ -125,8 +140,10 @@ describeChromeOnly('AriaQueryHandler', () => { ` ); - const div = (await page.$('aria/menu div'))!; - const id = await div!.evaluate((div: Element) => { + const div = (await page.$( + 'aria/menu div' + )) as ElementHandle; + const id = await div.evaluate(div => { return div.id; }); expect(id).toBe('mnu1'); @@ -140,8 +157,10 @@ describeChromeOnly('AriaQueryHandler', () => { ` ); - const menu = (await page.$('aria/menu-label1'))!; - const id = await menu!.evaluate((div: Element) => { + const menu = (await page.$( + 'aria/menu-label1' + )) as ElementHandle; + const id = await menu!.evaluate(div => { return div.id; }); expect(id).toBe('mnu1'); @@ -155,8 +174,10 @@ describeChromeOnly('AriaQueryHandler', () => { ` ); - const menu = (await page.$('aria/menu-label2'))!; - const id = await menu!.evaluate((div: Element) => { + const menu = (await page.$( + 'aria/menu-label2' + )) as ElementHandle; + const id = await menu!.evaluate(div => { return div.id; }); expect(id).toBe('mnu2'); @@ -173,14 +194,7 @@ describeChromeOnly('AriaQueryHandler', () => { ` ); const divs = await page.$$('aria/menu div'); - const ids = await Promise.all( - divs.map(n => { - return n.evaluate((div: Element) => { - return div.id; - }); - }) - ); - expect(ids.join(', ')).toBe('mnu1, mnu2'); + expect(await getIds(divs)).toEqual(['mnu1', 'mnu2']); }); }); describe('queryAllArray', () => { @@ -197,7 +211,7 @@ describeChromeOnly('AriaQueryHandler', () => { ` ); const sum = await page.$$eval('aria/[role="button"]', buttons => { - return buttons.reduce((acc, button) => { + return [...buttons].reduce((acc, button) => { return acc + Number(button.textContent); }, 0); }); @@ -623,15 +637,6 @@ describeChromeOnly('AriaQueryHandler', () => { ` ); }); - const getIds = async (elements: ElementHandle[]) => { - return Promise.all( - elements.map(element => { - return element.evaluate((element: Element) => { - return element.id; - }); - }) - ); - }; it('should find by name "foo"', async () => { const {page} = getTestState(); const found = await page.$$('aria/foo'); @@ -652,9 +657,7 @@ describeChromeOnly('AriaQueryHandler', () => { }); it('should find by role "button"', async () => { const {page} = getTestState(); - const found = (await page.$$( - 'aria/[role="button"]' - )) as ElementHandle[]; + const found = await page.$$('aria/[role="button"]'); const ids = await getIds(found); expect(ids).toEqual([ 'node5', diff --git a/test/src/elementhandle.spec.ts b/test/src/elementhandle.spec.ts index ec59d1d8d7ccc..2ef3ee8045534 100644 --- a/test/src/elementhandle.spec.ts +++ b/test/src/elementhandle.spec.ts @@ -16,6 +16,8 @@ import expect from 'expect'; import sinon from 'sinon'; +import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js'; +import {AwaitableIteratable} from '../../lib/cjs/puppeteer/common/types.js'; import { describeFailsFirefox, getTestState, @@ -24,7 +26,7 @@ import { setupTestPageAndContextHooks, } from './mocha-utils.js'; -import utils from './utils.js'; +import utils, {Iterable} from './utils.js'; describe('ElementHandle specs', function () { setupTestBrowserHooks(); @@ -83,7 +85,9 @@ describe('ElementHandle specs', function () { `); - const element = (await page.$('#therect'))!; + const element = (await page.$( + '#therect' + )) as ElementHandle; const pptrBoundingBox = await element.boundingBox(); const webBoundingBox = await page.evaluate(e => { const rect = e.getBoundingClientRect(); @@ -274,19 +278,23 @@ describe('ElementHandle specs', function () { describe('Element.waitForSelector', () => { it('should wait correctly with waitForSelector on an element', async () => { const {page} = getTestState(); - const waitFor = page.waitForSelector('.foo'); + const waitFor = page.waitForSelector('.foo') as Promise< + ElementHandle + >; // Set the page content after the waitFor has been started. await page.setContent( '
bar2
Foo1
' ); - let element = (await waitFor)!; + let element = await waitFor; expect(element).toBeDefined(); - const innerWaitFor = element.waitForSelector('.bar'); + const innerWaitFor = element.waitForSelector('.bar') as Promise< + ElementHandle + >; await element.evaluate(el => { el.innerHTML = '
bar1
'; }); - element = (await innerWaitFor)!; + element = await innerWaitFor; expect(element).toBeDefined(); expect( await element.evaluate(el => { @@ -315,13 +323,17 @@ describe('ElementHandle specs', function () { const el2 = (await page.waitForSelector('#el1'))!; expect( - await (await el2.waitForXPath('//div'))!.evaluate(el => { + await ( + (await el2.waitForXPath('//div')) as ElementHandle + ).evaluate(el => { return el.id; }) ).toStrictEqual('el2'); expect( - await (await el2.waitForXPath('.//div'))!.evaluate(el => { + await ( + (await el2.waitForXPath('.//div')) as ElementHandle + ).evaluate(el => { return el.id; }) ).toStrictEqual('el2'); @@ -394,11 +406,13 @@ describe('ElementHandle specs', function () { // Register. puppeteer.registerCustomQueryHandler('getById', { - queryOne: (_element, selector) => { + queryOne: (_, selector) => { return document.querySelector(`[id="${selector}"]`); }, }); - const element = (await page.$('getById/foo'))!; + const element = (await page.$( + 'getById/foo' + )) as ElementHandle; expect( await page.evaluate(element => { return element.id; @@ -446,19 +460,21 @@ describe('ElementHandle specs', function () { '
Foo1
Foo2
' ); puppeteer.registerCustomQueryHandler('getByClass', { - queryAll: (_element, selector) => { - return document.querySelectorAll(`.${selector}`); + queryAll: (element, selector) => { + if (element instanceof Element || element instanceof Document) { + return element.querySelectorAll(`.${selector}`); + } + return []; }, }); - const elements = await page.$$('getByClass/foo'); - const classNames = await Promise.all( - elements.map(async element => { - return await page.evaluate(element => { - return element.className; - }, element); - }) - ); - + const elements = (await page.$$('getByClass/foo')) as AwaitableIteratable< + ElementHandle + >; + const classNames = await Iterable.map(elements, async element => { + return await element.evaluate(element => { + return element.className; + }); + }); expect(classNames).toStrictEqual(['foo', 'foo baz']); }); it('should eval correctly', async () => { @@ -472,7 +488,7 @@ describe('ElementHandle specs', function () { }, }); const elements = await page.$$eval('getByClass/foo', divs => { - return divs.length; + return [...divs].length; }); expect(elements).toBe(2); @@ -481,7 +497,10 @@ describe('ElementHandle specs', function () { const {page, puppeteer} = getTestState(); puppeteer.registerCustomQueryHandler('getByClass', { queryOne: (element, selector) => { - return element.querySelector(`.${selector}`); + if (element instanceof Element || element instanceof Document) { + return element.querySelector(`.${selector}`); + } + return null; }, }); const waitFor = page.waitForSelector('getByClass/foo'); @@ -499,10 +518,15 @@ describe('ElementHandle specs', function () { const {page, puppeteer} = getTestState(); puppeteer.registerCustomQueryHandler('getByClass', { queryOne: (element, selector) => { - return element.querySelector(`.${selector}`); + if (element instanceof Element || element instanceof Document) { + return element.querySelector(`.${selector}`); + } + return null; }, }); - const waitFor = page.waitForSelector('getByClass/foo'); + const waitFor = page.waitForSelector('getByClass/foo') as Promise< + ElementHandle + >; // Set the page content after the waitFor has been started. await page.setContent( @@ -511,7 +535,9 @@ describe('ElementHandle specs', function () { let element = (await waitFor)!; expect(element).toBeDefined(); - const innerWaitFor = element.waitForSelector('getByClass/bar'); + const innerWaitFor = element.waitForSelector('getByClass/bar') as Promise< + ElementHandle + >; await element.evaluate(el => { el.innerHTML = '
bar1
'; @@ -532,7 +558,10 @@ describe('ElementHandle specs', function () { const {page, puppeteer} = getTestState(); puppeteer.registerCustomQueryHandler('getByClass', { queryOne: (element, selector) => { - return element.querySelector(`.${selector}`); + if (element instanceof Element || element instanceof Document) { + return element.querySelector(`.${selector}`); + } + return null; }, }); const waitFor = page.waitForSelector('getByClass/foo'); @@ -552,10 +581,16 @@ describe('ElementHandle specs', function () { ); puppeteer.registerCustomQueryHandler('getByClass', { queryOne: (element, selector) => { - return element.querySelector(`.${selector}`); + if (element instanceof Element || element instanceof Document) { + return element.querySelector(`.${selector}`); + } + return null; }, queryAll: (element, selector) => { - return element.querySelectorAll(`.${selector}`); + if (element instanceof Element || element instanceof Document) { + return element.querySelectorAll(`.${selector}`); + } + return []; }, }); @@ -563,7 +598,15 @@ describe('ElementHandle specs', function () { expect(element).toBeDefined(); const elements = await page.$$('getByClass/foo'); - expect(elements.length).toBe(3); + expect( + await Iterable.reduce( + elements, + i => { + return ++i; + }, + 0 + ) + ).toBe(3); }); it('should eval when both queryOne and queryAll are registered', async () => { const {page, puppeteer} = getTestState(); @@ -572,10 +615,16 @@ describe('ElementHandle specs', function () { ); puppeteer.registerCustomQueryHandler('getByClass', { queryOne: (element, selector) => { - return element.querySelector(`.${selector}`); + if (element instanceof Element || element instanceof Document) { + return element.querySelector(`.${selector}`); + } + return null; }, queryAll: (element, selector) => { - return element.querySelectorAll(`.${selector}`); + if (element instanceof Element || element instanceof Document) { + return element.querySelectorAll(`.${selector}`); + } + return []; }, }); @@ -585,7 +634,7 @@ describe('ElementHandle specs', function () { expect(txtContent).toBe('text'); const txtContents = await page.$$eval('getByClass/foo', divs => { - return divs + return [...divs] .map(d => { return d.textContent; }) diff --git a/test/src/queryselector.spec.ts b/test/src/queryselector.spec.ts index 9881d5e4f965c..8510137d248af 100644 --- a/test/src/queryselector.spec.ts +++ b/test/src/queryselector.spec.ts @@ -14,12 +14,14 @@ * limitations under the License. */ import expect from 'expect'; +import {ElementHandle} from '../../lib/cjs/puppeteer/common/ElementHandle.js'; +import {CustomQueryHandler} from '../../lib/cjs/puppeteer/common/QueryHandler.js'; import { getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks, } from './mocha-utils.js'; -import {CustomQueryHandler} from '../../lib/cjs/puppeteer/common/QueryHandler.js'; +import {Iterable} from './utils.js'; describe('querySelector', function () { setupTestBrowserHooks(); @@ -99,8 +101,8 @@ describe('querySelector', function () { }); it('should find first element in shadow', async () => { const {page} = getTestState(); - const div = (await page.$('pierce/.foo'))!; - const text = await div.evaluate((element: Element) => { + const div = (await page.$('pierce/.foo')) as ElementHandle; + const text = await div.evaluate(element => { return element.textContent; }); expect(text).toBe('Hello'); @@ -108,20 +110,18 @@ describe('querySelector', function () { it('should find all elements in shadow', async () => { const {page} = getTestState(); const divs = await page.$$('pierce/.foo'); - const text = await Promise.all( - divs.map(div => { - return div.evaluate((element: Element) => { - return element.textContent; - }); - }) - ); - expect(text.join(' ')).toBe('Hello World'); + const textContents = await Iterable.map(divs, node => { + return node.evaluate(node => { + return node.textContent ?? ''; + }); + }); + expect(textContents.join(' ')).toBe('Hello World'); }); it('should find first child element', async () => { const {page} = getTestState(); const parentElement = (await page.$('html > div'))!; const childElement = (await parentElement.$('pierce/div'))!; - const text = await childElement.evaluate((element: Element) => { + const text = await childElement.evaluate(element => { return element.textContent; }); expect(text).toBe('Hello'); @@ -130,14 +130,12 @@ describe('querySelector', function () { const {page} = getTestState(); const parentElement = (await page.$('html > div'))!; const childElements = await parentElement.$$('pierce/div'); - const text = await Promise.all( - childElements.map(div => { - return div.evaluate((element: Element) => { - return element.textContent; - }); - }) - ); - expect(text.join(' ')).toBe('Hello World'); + const textContents = await Iterable.map(childElements, node => { + return node.evaluate(node => { + return node.textContent; + }); + }); + expect(textContents.join(' ')).toBe('Hello World'); }); }); @@ -152,7 +150,7 @@ describe('querySelector', function () { '
hello
beautiful
world!
' ); const divsCount = await page.$$eval('div', divs => { - return divs.length; + return [...divs].length; }); expect(divsCount).toBe(3); }); @@ -164,7 +162,7 @@ describe('querySelector', function () { const divsCountPlus5 = await page.$$eval( 'div', (divs, two, three) => { - return divs.length + (two as number) + (three as number); + return [...divs].length + two + three; }, 2, 3 @@ -181,9 +179,9 @@ describe('querySelector', function () { 'section', (sections, div) => { return ( - sections.reduce((acc, section) => { + [...sections].reduce((acc, section) => { return acc + Number(section.textContent); - }, 0) + Number((div as HTMLElement).textContent) + }, 0) + Number(div.textContent) ); }, divHandle @@ -202,7 +200,7 @@ describe('querySelector', function () { ` ); const sum = await page.$$eval('section', sections => { - return sections.reduce((acc, section) => { + return [...sections].reduce((acc, section) => { return acc + Number(section.textContent); }, 0); }); @@ -232,20 +230,24 @@ describe('querySelector', function () { await page.setContent('
A

B
'); const elements = await page.$$('div'); - expect(elements.length).toBe(2); - const promises = elements.map(element => { - return page.evaluate((e: HTMLElement) => { - return e.textContent; - }, element); + const textContents = await Iterable.map(elements, node => { + return node.evaluate(node => { + return node.textContent; + }); }); - expect(await Promise.all(promises)).toEqual(['A', 'B']); + expect(textContents).toEqual(['A', 'B']); }); it('should return empty array if nothing is found', async () => { const {page, server} = getTestState(); await page.goto(server.EMPTY_PAGE); const elements = await page.$$('div'); - expect(elements.length).toBe(0); + const textContents = await Iterable.map(elements, node => { + return node.evaluate(node => { + return node.textContent; + }); + }); + expect(textContents.length).toBe(0); }); }); @@ -255,8 +257,12 @@ describe('querySelector', function () { await page.setContent('
test
'); const elements = await page.$x('/html/body/section'); - expect(elements[0]!).toBeTruthy(); - expect(elements.length).toBe(1); + const textContents = await Iterable.map(elements, node => { + return node.evaluate(node => { + return node.textContent; + }); + }); + expect(textContents.length).toBe(1); }); it('should return empty array for non-existing element', async () => { const {page} = getTestState(); @@ -269,7 +275,12 @@ describe('querySelector', function () { await page.setContent('
'); const elements = await page.$x('/html/body/div'); - expect(elements.length).toBe(2); + const textContents = await Iterable.map(elements, node => { + return node.evaluate(node => { + return node.textContent; + }); + }); + expect(textContents.length).toBe(2); }); }); @@ -310,7 +321,7 @@ describe('querySelector', function () { ); const tweet = (await page.$('.tweet'))!; const content = await tweet.$eval('.like', node => { - return (node as HTMLElement).innerText; + return (node as HTMLDivElement).innerText; }); expect(content).toBe('100'); }); @@ -323,7 +334,7 @@ describe('querySelector', function () { await page.setContent(htmlContent); const elementHandle = (await page.$('#myId'))!; const content = await elementHandle.$eval('.a', node => { - return (node as HTMLElement).innerText; + return (node as HTMLDivElement).innerText; }); expect(content).toBe('a-child-div'); }); @@ -356,7 +367,7 @@ describe('querySelector', function () { ); const tweet = (await page.$('.tweet'))!; const content = await tweet.$$eval('.like', nodes => { - return (nodes as HTMLElement[]).map(n => { + return ([...nodes] as HTMLElement[]).map(n => { return n.innerText; }); }); @@ -371,7 +382,7 @@ describe('querySelector', function () { await page.setContent(htmlContent); const elementHandle = (await page.$('#myId'))!; const content = await elementHandle.$$eval('.a', nodes => { - return (nodes as HTMLElement[]).map(n => { + return ([...nodes] as HTMLElement[]).map(n => { return n.innerText; }); }); @@ -386,7 +397,7 @@ describe('querySelector', function () { await page.setContent(htmlContent); const elementHandle = (await page.$('#myId'))!; const nodesLength = await elementHandle.$$eval('.a', nodes => { - return nodes.length; + return [...nodes].length; }); expect(nodesLength).toBe(0); }); @@ -401,13 +412,12 @@ describe('querySelector', function () { ); const html = (await page.$('html'))!; const elements = await html.$$('div'); - expect(elements.length).toBe(2); - const promises = elements.map(element => { - return page.evaluate((e: HTMLElement) => { - return e.textContent; - }, element); + const text = await Iterable.map(elements, div => { + return div.evaluate(element => { + return element.textContent; + }); }); - expect(await Promise.all(promises)).toEqual(['A', 'B']); + expect(text).toEqual(['A', 'B']); }); it('should return empty array for non-existing elements', async () => { @@ -418,7 +428,12 @@ describe('querySelector', function () { ); const html = (await page.$('html'))!; const elements = await html.$$('div'); - expect(elements.length).toBe(0); + const text = await Iterable.map(elements, div => { + return div.evaluate(element => { + return element.textContent; + }); + }); + expect(text.length).toBe(0); }); }); @@ -432,10 +447,12 @@ describe('querySelector', function () { ); const html = (await page.$('html'))!; const second = await html.$x(`./body/div[contains(@class, 'second')]`); - const inner = await second[0]!.$x(`./div[contains(@class, 'inner')]`); + const inner = await (await Iterable.first(second))!.$x( + `./div[contains(@class, 'inner')]` + ); const content = await page.evaluate(e => { return e.textContent; - }, inner[0]!); + }, (await Iterable.first(inner))!); expect(content).toBe('A'); }); @@ -456,7 +473,10 @@ describe('querySelector', function () { describe('QueryAll', function () { const handler: CustomQueryHandler = { queryAll: (element, selector) => { - return Array.from(element.querySelectorAll(selector)); + if (element instanceof Element || element instanceof Document) { + return element.querySelectorAll(selector); + } + return []; }, }; before(() => { @@ -478,13 +498,12 @@ describe('querySelector', function () { ); const html = (await page.$('html'))!; const elements = await html.$$('allArray/div'); - expect(elements.length).toBe(2); - const promises = elements.map(element => { - return page.evaluate(e => { - return e.textContent; - }, element); + const textContents = await Iterable.map(elements, node => { + return node.evaluate(node => { + return node.textContent; + }); }); - expect(await Promise.all(promises)).toEqual(['A', 'B']); + expect(await Promise.all(textContents)).toEqual(['A', 'B']); }); it('$$ should return empty array for non-existing elements', async () => { @@ -495,7 +514,12 @@ describe('querySelector', function () { ); const html = (await page.$('html'))!; const elements = await html.$$('allArray/div'); - expect(elements.length).toBe(0); + const textContents = await Iterable.map(elements, node => { + return node.evaluate(node => { + return node.textContent; + }); + }); + expect(textContents.length).toBe(0); }); it('$$eval should work', async () => { const {page} = getTestState(); @@ -504,7 +528,7 @@ describe('querySelector', function () { '
hello
beautiful
world!
' ); const divsCount = await page.$$eval('allArray/div', divs => { - return divs.length; + return [...divs].length; }); expect(divsCount).toBe(3); }); @@ -516,7 +540,7 @@ describe('querySelector', function () { const divsCountPlus5 = await page.$$eval( 'allArray/div', (divs, two, three) => { - return divs.length + (two as number) + (three as number); + return [...divs].length + two + three; }, 2, 3 @@ -533,9 +557,9 @@ describe('querySelector', function () { 'allArray/section', (sections, div) => { return ( - sections.reduce((acc, section) => { + [...sections].reduce((acc, section) => { return acc + Number(section.textContent); - }, 0) + Number((div as HTMLElement).textContent) + }, 0) + Number(div.textContent) ); }, divHandle @@ -554,7 +578,7 @@ describe('querySelector', function () { ` ); const sum = await page.$$eval('allArray/section', sections => { - return sections.reduce((acc, section) => { + return [...sections].reduce((acc, section) => { return acc + Number(section.textContent); }, 0); }); diff --git a/test/src/utils.ts b/test/src/utils.ts index 30333aa7c26f0..5c070c9d160ba 100644 --- a/test/src/utils.ts +++ b/test/src/utils.ts @@ -20,6 +20,10 @@ import {Frame} from '../../lib/cjs/puppeteer/common/FrameManager.js'; import {Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {EventEmitter} from '../../lib/cjs/puppeteer/common/EventEmitter.js'; import {compare} from './golden-utils.js'; +import { + Awaitable, + AwaitableIteratable, +} from '../../lib/cjs/puppeteer/common/types.js'; const PROJECT_ROOT = path.join(__dirname, '..', '..'); @@ -145,6 +149,37 @@ export const waitEvent = ( }); }; +export class Iterable { + static async map( + iterable: AwaitableIteratable, + callback: (value: T) => Awaitable + ): Promise { + const promises: Awaitable[] = []; + for await (const obj of iterable) { + promises.push(callback(obj)); + } + return Promise.all(promises); + } + static async reduce( + iterable: AwaitableIteratable, + callback: (prev: U, value: T) => Awaitable, + initial: U + ): Promise { + for await (const obj of iterable) { + initial = await callback(initial, obj); + } + return initial; + } + static async first( + iterable: AwaitableIteratable + ): Promise { + for await (const value of iterable) { + return value; + } + return undefined; + } +} + /** * @deprecated Use exports directly. */