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
This commit is contained in:
jrandolf 2022-12-19 13:25:56 +01:00 committed by GitHub
parent e3e9cc622a
commit c7a063a152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 9 deletions

View File

@ -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) | |

View File

@ -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;
```

View File

@ -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 <code>change</code> and <code>input</code> event once all the provided options have been selected. If there's no <code>&lt;select&gt;</code> element matching <code>selector</code>, 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) | | <p>Focuses the element, and then sends a <code>keydown</code>, <code>keypress</code>/<code>input</code>, and <code>keyup</code> event for each character in the text.</p><p>To press a special key, like <code>Control</code> or <code>ArrowDown</code>, use [ElementHandle.press()](./puppeteer.elementhandle.press.md).</p> |
| [uploadFile(this, filePaths)](./puppeteer.elementhandle.uploadfile.md) | | This method expects <code>elementHandle</code> to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). |
| [waitForSelector(selector, options)](./puppeteer.elementhandle.waitforselector.md) | | <p>Wait for an element matching the given selector to appear in the current element.</p><p>Unlike [Frame.waitForSelector()](./puppeteer.frame.waitforselector.md), this method does not work across navigations or if the element is detached from DOM.</p> |

View File

@ -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<K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap>(
tagName: K
): Promise<HandleFor<ElementFor<K>>>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ---- | ----------------------------------------- |
| tagName | K | The tag name of the desired element type. |
**Returns:**
Promise&lt;[HandleFor](./puppeteer.handlefor.md)&lt;[ElementFor](./puppeteer.elementfor.md)&lt;K&gt;&gt;&gt;
## Exceptions
An error if the handle does not match. **The handle will not be automatically disposed.**
## Example
```ts
const element: ElementHandle<Element> = await page.$('.class-name-of-anchor');
// DO NOT DISPOSE `element`, this will be always be the same handle.
const anchor: ElementHandle<HTMLAnchorElement> = await element.toElement('a');
```

View File

@ -9,10 +9,12 @@ sidebar_label: NodeFor
```typescript
export declare type NodeFor<ComplexSelector extends string> =
TypeSelectorOfComplexSelector<ComplexSelector> extends infer TypeSelector
? TypeSelector extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[TypeSelector]
: TypeSelector extends keyof SVGElementTagNameMap
? SVGElementTagNameMap[TypeSelector]
? TypeSelector extends
| keyof HTMLElementTagNameMap
| keyof SVGElementTagNameMap
? ElementFor<TypeSelector>
: Element
: never;
```
**References:** [ElementFor](./puppeteer.elementfor.md)

View File

@ -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<Element> = await page.$(
* '.class-name-of-anchor'
* );
* // DO NOT DISPOSE `element`, this will be always be the same handle.
* const anchor: ElementHandle<HTMLAnchorElement> = 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<HandleFor<ElementFor<K>>> {
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<ElementFor<K>>;
}
override asElement(): ElementHandle<ElementType> | null {
return this;
}

View File

@ -57,6 +57,17 @@ export type InnerParams<T extends unknown[]> = {
[K in keyof T]: FlattenHandle<T[K]>;
};
/**
* @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<T extends unknown[]> = (
*/
export type NodeFor<ComplexSelector extends string> =
TypeSelectorOfComplexSelector<ComplexSelector> extends infer TypeSelector
? TypeSelector extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[TypeSelector]
: TypeSelector extends keyof SVGElementTagNameMap
? SVGElementTagNameMap[TypeSelector]
? TypeSelector extends
| keyof HTMLElementTagNameMap
| keyof SVGElementTagNameMap
? ElementFor<TypeSelector>
: Element
: never;

View File

@ -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('<div class="foo">Foo1</div>');
const element = await page.$('.foo');
const div = await element?.toElement('div');
expect(div).toBeDefined();
});
});
});