feat: add ElementHandle.scrollIntoView (#10005)

This commit is contained in:
Alex Rudenko 2023-04-12 07:17:18 +02:00 committed by GitHub
parent 656b562c74
commit 0d556a71d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 41 deletions

View File

@ -70,6 +70,7 @@ The constructor for this class is marked as internal. Third-party code should no
| [isIntersectingViewport(this, options)](./puppeteer.elementhandle.isintersectingviewport.md) | | Resolves to true if the element is visible in the current viewport. If an element is an SVG, we check if the svg owner element is in the viewport instead. See https://crbug.com/963246. | | [isIntersectingViewport(this, options)](./puppeteer.elementhandle.isintersectingviewport.md) | | Resolves to true if the element is visible in the current viewport. If an element is an SVG, we check if the svg owner element is in the viewport instead. See https://crbug.com/963246. |
| [press(key, options)](./puppeteer.elementhandle.press.md) | | Focuses the element, and then uses [Keyboard.down()](./puppeteer.keyboard.down.md) and [Keyboard.up()](./puppeteer.keyboard.up.md). | | [press(key, options)](./puppeteer.elementhandle.press.md) | | Focuses the element, and then uses [Keyboard.down()](./puppeteer.keyboard.down.md) and [Keyboard.up()](./puppeteer.keyboard.up.md). |
| [screenshot(this, options)](./puppeteer.elementhandle.screenshot.md) | | This method scrolls element into view if needed, and then uses [Page.screenshot()](./puppeteer.page.screenshot_2.md) to take a screenshot of the element. If the element is detached from DOM, the method throws an error. | | [screenshot(this, options)](./puppeteer.elementhandle.screenshot.md) | | This method scrolls element into view if needed, and then uses [Page.screenshot()](./puppeteer.page.screenshot_2.md) to take a screenshot of the element. If the element is detached from DOM, the method throws an error. |
| [scrollIntoView(this)](./puppeteer.elementhandle.scrollintoview.md) | | Scrolls the element into view using either the automation protocol client or by calling element.scrollIntoView. |
| [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. | | [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. | | [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. | | [toElement(tagName)](./puppeteer.elementhandle.toelement.md) | | Converts the current handle to the given element type. |

View File

@ -0,0 +1,25 @@
---
sidebar_label: ElementHandle.scrollIntoView
---
# ElementHandle.scrollIntoView() method
Scrolls the element into view using either the automation protocol client or by calling element.scrollIntoView.
#### Signature:
```typescript
class ElementHandle {
scrollIntoView(this: ElementHandle<Element>): Promise<void>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | ------------------------------------------------------------ | ----------- |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
**Returns:**
Promise&lt;void&gt;

View File

@ -811,6 +811,27 @@ export class ElementHandle<
throw new Error('Not implemented'); throw new Error('Not implemented');
} }
/**
* @internal
*/
protected async assertConnectedElement(): Promise<void> {
const error = await this.evaluate(
async (element): Promise<string | undefined> => {
if (!element.isConnected) {
return 'Node is detached from document';
}
if (element.nodeType !== Node.ELEMENT_NODE) {
return 'Node is not of type HTMLElement';
}
return;
}
);
if (error) {
throw new Error(error);
}
}
/** /**
* Resolves to true if the element is visible in the current viewport. If an * Resolves to true if the element is visible in the current viewport. If an
* element is an SVG, we check if the svg owner element is in the viewport * element is an SVG, we check if the svg owner element is in the viewport
@ -822,6 +843,8 @@ export class ElementHandle<
threshold?: number; threshold?: number;
} }
): Promise<boolean> { ): Promise<boolean> {
await this.assertConnectedElement();
const {threshold = 0} = options ?? {}; const {threshold = 0} = options ?? {};
const svgHandle = await this.#asSVGElementHandle(this); const svgHandle = await this.#asSVGElementHandle(this);
const intersectionTarget: ElementHandle<Element> = svgHandle const intersectionTarget: ElementHandle<Element> = svgHandle
@ -846,6 +869,14 @@ export class ElementHandle<
} }
} }
/**
* Scrolls the element into view using either the automation protocol client
* or by calling element.scrollIntoView.
*/
async scrollIntoView(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
}
/** /**
* Returns true if an element is an SVGElement (included svg, path, rect * Returns true if an element is an SVGElement (included svg, path, rect
* etc.). * etc.).

View File

@ -231,58 +231,44 @@ export class CDPElementHandle<
return this.#frameManager.frame(nodeInfo.node.frameId); return this.#frameManager.frame(nodeInfo.node.frameId);
} }
async #scrollIntoViewIfNeeded( override async scrollIntoView(
this: CDPElementHandle<Element> this: CDPElementHandle<Element>
): Promise<void> { ): Promise<void> {
const error = await this.evaluate( await this.assertConnectedElement();
async (element): Promise<string | undefined> => {
if (!element.isConnected) {
return 'Node is detached from document';
}
if (element.nodeType !== Node.ELEMENT_NODE) {
return 'Node is not of type HTMLElement';
}
return;
}
);
if (error) {
throw new Error(error);
}
try { try {
await this.client.send('DOM.scrollIntoViewIfNeeded', { await this.client.send('DOM.scrollIntoViewIfNeeded', {
objectId: this.remoteObject().objectId, objectId: this.remoteObject().objectId,
}); });
} catch (_err) { } catch (error) {
debugError(error);
// Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported // Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported
await this.evaluate( await this.evaluate(async (element): Promise<void> => {
async (element, pageJavascriptEnabled): Promise<void> => { element.scrollIntoView({
const visibleRatio = async () => { block: 'center',
return await new Promise(resolve => { inline: 'center',
const observer = new IntersectionObserver(entries => { // @ts-expect-error Chrome still supports behavior: instant but
resolve(entries[0]!.intersectionRatio); // it's not in the spec so TS shouts We don't want to make this
observer.disconnect(); // breaking change in Puppeteer yet so we'll ignore the line.
}); behavior: 'instant',
observer.observe(element); });
}); });
};
if (!pageJavascriptEnabled || (await visibleRatio()) !== 1.0) {
element.scrollIntoView({
block: 'center',
inline: 'center',
// @ts-expect-error Chrome still supports behavior: instant but
// it's not in the spec so TS shouts We don't want to make this
// breaking change in Puppeteer yet so we'll ignore the line.
behavior: 'instant',
});
}
},
this.#page.isJavaScriptEnabled()
);
} }
} }
async #scrollIntoViewIfNeeded(
this: CDPElementHandle<Element>
): Promise<void> {
if (
await this.isIntersectingViewport({
threshold: 1,
})
) {
return;
}
await this.scrollIntoView();
}
async #getOOPIFOffsets( async #getOOPIFOffsets(
frame: Frame frame: Frame
): Promise<{offsetX: number; offsetY: number}> { ): Promise<{offsetX: number; offsetY: number}> {

View File

@ -509,6 +509,12 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[click.spec] Page.click should scroll and click with disabled javascript",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{ {
"testIdPattern": "[click.spec] Page.click should select the text by triple clicking", "testIdPattern": "[click.spec] Page.click should select the text by triple clicking",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],

View File

@ -116,6 +116,18 @@ describe('Page.click', function () {
await Promise.all([page.click('a'), page.waitForNavigation()]); await Promise.all([page.click('a'), page.waitForNavigation()]);
expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
}); });
it('should scroll and click with disabled javascript', async () => {
const {page, server} = getTestState();
await page.setJavaScriptEnabled(false);
await page.goto(server.PREFIX + '/wrappedlink.html');
const body = await page.waitForSelector('body');
await body!.evaluate(el => {
el.style.paddingTop = '3000px';
});
await Promise.all([page.click('a'), page.waitForNavigation()]);
expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
});
it('should click when one of inline box children is outside of viewport', async () => { it('should click when one of inline box children is outside of viewport', async () => {
const {page} = getTestState(); const {page} = getTestState();