From c7a063a15274856184356e15f2ae4be41191d309 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Mon, 19 Dec 2022 13:25:56 +0100 Subject: [PATCH] feat: add element validation (#9352) This PR adds a method to ElementHandle that validates the tag type of that handle and returns it. Fixed: #8579, #9280 --- docs/api/index.md | 1 + docs/api/puppeteer.elementfor.md | 17 ++++++++ docs/api/puppeteer.elementhandle.md | 1 + docs/api/puppeteer.elementhandle.toelement.md | 39 +++++++++++++++++++ docs/api/puppeteer.nodefor.md | 10 +++-- .../src/common/ElementHandle.ts | 33 +++++++++++++++- packages/puppeteer-core/src/common/types.ts | 19 +++++++-- test/src/elementhandle.spec.ts | 10 +++++ 8 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 docs/api/puppeteer.elementfor.md create mode 100644 docs/api/puppeteer.elementhandle.toelement.md diff --git a/docs/api/index.md b/docs/api/index.md index 09fa283fa55..0c301b3e3e3 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -146,6 +146,7 @@ sidebar_label: API | [Awaitable](./puppeteer.awaitable.md) | | | [ChromeReleaseChannel](./puppeteer.chromereleasechannel.md) | | | [ConsoleMessageType](./puppeteer.consolemessagetype.md) | The supported types for console messages. | +| [ElementFor](./puppeteer.elementfor.md) | | | [ErrorCode](./puppeteer.errorcode.md) | | | [EvaluateFunc](./puppeteer.evaluatefunc.md) | | | [EventType](./puppeteer.eventtype.md) | | diff --git a/docs/api/puppeteer.elementfor.md b/docs/api/puppeteer.elementfor.md new file mode 100644 index 00000000000..db37a1ec04f --- /dev/null +++ b/docs/api/puppeteer.elementfor.md @@ -0,0 +1,17 @@ +--- +sidebar_label: ElementFor +--- + +# ElementFor type + +#### Signature: + +```typescript +export declare type ElementFor< + TagName extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap +> = TagName extends keyof HTMLElementTagNameMap + ? HTMLElementTagNameMap[TagName] + : TagName extends keyof SVGElementTagNameMap + ? SVGElementTagNameMap[TagName] + : never; +``` diff --git a/docs/api/puppeteer.elementhandle.md b/docs/api/puppeteer.elementhandle.md index b1f6f25e501..e7663becb28 100644 --- a/docs/api/puppeteer.elementhandle.md +++ b/docs/api/puppeteer.elementhandle.md @@ -72,6 +72,7 @@ The constructor for this class is marked as internal. Third-party code should no | [screenshot(this, options)](./puppeteer.elementhandle.screenshot.md) | | This method scrolls element into view if needed, and then uses [Page.screenshot()](./puppeteer.page.screenshot.md) to take a screenshot of the element. If the element is detached from DOM, the method throws an error. | | [select(values)](./puppeteer.elementhandle.select.md) | | Triggers a change and input event once all the provided options have been selected. If there's no <select> element matching selector, the method throws an error. | | [tap(this)](./puppeteer.elementhandle.tap.md) | | This method scrolls element into view if needed, and then uses [Touchscreen.tap()](./puppeteer.touchscreen.tap.md) to tap in the center of the element. If the element is detached from DOM, the method throws an error. | +| [toElement(tagName)](./puppeteer.elementhandle.toelement.md) | | Converts the current handle to the given element type. | | [type(text, options)](./puppeteer.elementhandle.type.md) | |

Focuses the element, and then sends a keydown, keypress/input, and keyup event for each character in the text.

To press a special key, like Control or ArrowDown, use [ElementHandle.press()](./puppeteer.elementhandle.press.md).

| | [uploadFile(this, filePaths)](./puppeteer.elementhandle.uploadfile.md) | | This method expects elementHandle to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). | | [waitForSelector(selector, options)](./puppeteer.elementhandle.waitforselector.md) | |

Wait for an element matching the given selector to appear in the current element.

Unlike [Frame.waitForSelector()](./puppeteer.frame.waitforselector.md), this method does not work across navigations or if the element is detached from DOM.

| diff --git a/docs/api/puppeteer.elementhandle.toelement.md b/docs/api/puppeteer.elementhandle.toelement.md new file mode 100644 index 00000000000..73f2636a968 --- /dev/null +++ b/docs/api/puppeteer.elementhandle.toelement.md @@ -0,0 +1,39 @@ +--- +sidebar_label: ElementHandle.toElement +--- + +# ElementHandle.toElement() method + +Converts the current handle to the given element type. + +#### Signature: + +```typescript +class ElementHandle { + toElement( + tagName: K + ): Promise>>; +} +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ---- | ----------------------------------------- | +| tagName | K | The tag name of the desired element type. | + +**Returns:** + +Promise<[HandleFor](./puppeteer.handlefor.md)<[ElementFor](./puppeteer.elementfor.md)<K>>> + +## Exceptions + +An error if the handle does not match. **The handle will not be automatically disposed.** + +## Example + +```ts +const element: ElementHandle = await page.$('.class-name-of-anchor'); +// DO NOT DISPOSE `element`, this will be always be the same handle. +const anchor: ElementHandle = await element.toElement('a'); +``` diff --git a/docs/api/puppeteer.nodefor.md b/docs/api/puppeteer.nodefor.md index 3099cf1ad77..d56bede8f4f 100644 --- a/docs/api/puppeteer.nodefor.md +++ b/docs/api/puppeteer.nodefor.md @@ -9,10 +9,12 @@ sidebar_label: NodeFor ```typescript export declare type NodeFor = TypeSelectorOfComplexSelector extends infer TypeSelector - ? TypeSelector extends keyof HTMLElementTagNameMap - ? HTMLElementTagNameMap[TypeSelector] - : TypeSelector extends keyof SVGElementTagNameMap - ? SVGElementTagNameMap[TypeSelector] + ? TypeSelector extends + | keyof HTMLElementTagNameMap + | keyof SVGElementTagNameMap + ? ElementFor : Element : never; ``` + +**References:** [ElementFor](./puppeteer.elementfor.md) diff --git a/packages/puppeteer-core/src/common/ElementHandle.ts b/packages/puppeteer-core/src/common/ElementHandle.ts index a59daf92c70..cb1e475c7db 100644 --- a/packages/puppeteer-core/src/common/ElementHandle.ts +++ b/packages/puppeteer-core/src/common/ElementHandle.ts @@ -31,7 +31,7 @@ import { } from './JSHandle.js'; import {Page, ScreenshotOptions} from '../api/Page.js'; import {getQueryHandlerAndSelector} from './QueryHandler.js'; -import {EvaluateFunc, HandleFor, NodeFor} from './types.js'; +import {ElementFor, EvaluateFunc, HandleFor, NodeFor} from './types.js'; import {KeyInput} from './USKeyboardLayout.js'; import {debugError, isString} from './util.js'; import {CDPPage} from './Page.js'; @@ -412,6 +412,37 @@ export class ElementHandle< return this.waitForSelector(`xpath/${xpath}`, options); } + /** + * Converts the current handle to the given element type. + * + * @example + * + * ```ts + * const element: ElementHandle = await page.$( + * '.class-name-of-anchor' + * ); + * // DO NOT DISPOSE `element`, this will be always be the same handle. + * const anchor: ElementHandle = await element.toElement( + * 'a' + * ); + * ``` + * + * @param tagName - The tag name of the desired element type. + * @throws An error if the handle does not match. **The handle will not be + * automatically disposed.** + */ + async toElement< + K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap + >(tagName: K): Promise>> { + const isMatchingTagName = await this.evaluate((node, tagName) => { + return node.nodeName === tagName.toUpperCase(); + }, tagName); + if (!isMatchingTagName) { + throw new Error(`Element is not a(n) \`${tagName}\` element`); + } + return this as unknown as HandleFor>; + } + override asElement(): ElementHandle | null { return this; } diff --git a/packages/puppeteer-core/src/common/types.ts b/packages/puppeteer-core/src/common/types.ts index 75dda2e433e..2317a91f1ba 100644 --- a/packages/puppeteer-core/src/common/types.ts +++ b/packages/puppeteer-core/src/common/types.ts @@ -57,6 +57,17 @@ export type InnerParams = { [K in keyof T]: FlattenHandle; }; +/** + * @public + */ +export type ElementFor< + TagName extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap +> = TagName extends keyof HTMLElementTagNameMap + ? HTMLElementTagNameMap[TagName] + : TagName extends keyof SVGElementTagNameMap + ? SVGElementTagNameMap[TagName] + : never; + /** * @public */ @@ -69,10 +80,10 @@ export type EvaluateFunc = ( */ export type NodeFor = TypeSelectorOfComplexSelector extends infer TypeSelector - ? TypeSelector extends keyof HTMLElementTagNameMap - ? HTMLElementTagNameMap[TypeSelector] - : TypeSelector extends keyof SVGElementTagNameMap - ? SVGElementTagNameMap[TypeSelector] + ? TypeSelector extends + | keyof HTMLElementTagNameMap + | keyof SVGElementTagNameMap + ? ElementFor : Element : never; diff --git a/test/src/elementhandle.spec.ts b/test/src/elementhandle.spec.ts index 5b2b7e5e2fc..0d6a4fec309 100644 --- a/test/src/elementhandle.spec.ts +++ b/test/src/elementhandle.spec.ts @@ -605,4 +605,14 @@ describe('ElementHandle specs', function () { expect(txtContents).toBe('textcontent'); }); }); + + describe('Element.toElement', () => { + it('should work', async () => { + const {page} = getTestState(); + await page.setContent('
Foo1
'); + const element = await page.$('.foo'); + const div = await element?.toElement('div'); + expect(div).toBeDefined(); + }); + }); });