feat(scrollIntoView): expose scrollIntoViewIfNeeded in api (#382)

This also replaces isIntersectingViewport with visibleRatio for more flexibility.
This commit is contained in:
Dmitry Gozman 2020-01-06 13:16:56 -08:00 committed by Pavel Feldman
parent 58b8e66df8
commit 491eeeef7e
5 changed files with 62 additions and 19 deletions

View File

@ -58,15 +58,16 @@
* [elementHandle.fill(value)](#elementhandlefillvalue)
* [elementHandle.focus()](#elementhandlefocus)
* [elementHandle.hover([options])](#elementhandlehoveroptions)
* [elementHandle.isIntersectingViewport()](#elementhandleisintersectingviewport)
* [elementHandle.ownerFrame()](#elementhandleownerframe)
* [elementHandle.press(key[, options])](#elementhandlepresskey-options)
* [elementHandle.screenshot([options])](#elementhandlescreenshotoptions)
* [elementHandle.scrollIntoViewIfNeeded()](#elementhandlescrollintoviewifneeded)
* [elementHandle.select(...values)](#elementhandleselectvalues)
* [elementHandle.setInputFiles(...files)](#elementhandlesetinputfilesfiles)
* [elementHandle.toString()](#elementhandletostring)
* [elementHandle.tripleclick([options])](#elementhandletripleclickoptions)
* [elementHandle.type(text[, options])](#elementhandletypetext-options)
* [elementHandle.visibleRatio()](#elementhandlevisibleratio)
- [class: Frame](#class-frame)
* [frame.$(selector)](#frameselector)
* [frame.$$(selector)](#frameselector-1)
@ -786,9 +787,6 @@ Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to hover over the center of the element.
If the element is detached from DOM, the method throws an error.
#### elementHandle.isIntersectingViewport()
- returns: <[Promise]<[boolean]>> Resolves to true if the element is visible in the current viewport.
#### elementHandle.ownerFrame()
- returns: <[Promise]<[Frame]>> Returns the frame containing the given element.
@ -816,6 +814,15 @@ If `key` is a single character and no modifier keys besides `Shift` are being he
This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element.
If the element is detached from DOM, the method throws an error.
#### elementHandle.scrollIntoViewIfNeeded()
- returns: <[Promise]> Resolves after the element has been scrolled into view.
This method tries to scroll element into view, unless it is completely visible as defined by [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)'s ```ratio```. See also [elementHandle.visibleRatio()](#elementhandlevisibleratio).
Throws when ```elementHandle``` does not point to an element [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot.
> **NOTE** If javascript is disabled, element is scrolled into view even when already completely visible.
#### elementHandle.select(...values)
- `...values` <...[string]|[ElementHandle]|[Object]> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]> Matches by `option.value`.
@ -891,6 +898,11 @@ await elementHandle.type('some text');
await elementHandle.press('Enter');
```
#### elementHandle.visibleRatio()
- returns: <[Promise]<[number]>> Returns the visible ratio as defined by [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
Positive ratio means that some part of the element is visible in the current viewport. Ratio equal to one means that element is completely visible.
### class: Frame
At every point of time, page exposes its current frame tree via the [page.mainFrame()](#pagemainframe) and [frame.childFrames()](#framechildframes) methods.
@ -1954,8 +1966,7 @@ Get the browser context that the page belongs to.
- `relativePoint` <[Object]> A point to click relative to the top-left corner of element padding box. If not specified, clicks to some visible point of the element.
- x <[number]>
- y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified,
currently pressed modifiers are used.
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`.
- `timeout` <[number]> Maximum navigation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`.

View File

@ -134,7 +134,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._page._delegate.getContentFrame(this);
}
async _scrollIntoViewIfNeeded() {
async scrollIntoViewIfNeeded() {
const error = await this._evaluateInUtility(async (node: Node, pageJavascriptEnabled: boolean) => {
if (!node.isConnected)
return 'Node is detached from document';
@ -168,7 +168,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
private async _ensurePointerActionPoint(relativePoint?: types.Point): Promise<types.Point> {
await this._scrollIntoViewIfNeeded();
await this.scrollIntoViewIfNeeded();
if (!relativePoint)
return this._clickablePoint();
let r = await this._viewportPointAndScroll(relativePoint);
@ -464,12 +464,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._context._$$('xpath=' + expression, this);
}
isIntersectingViewport(): Promise<boolean> {
visibleRatio(): Promise<number> {
return this._evaluateInUtility(async (node: Node) => {
if (node.nodeType !== Node.ELEMENT_NODE)
throw new Error('Node is not of type HTMLElement');
const element = node as Element;
const visibleRatio = await new Promise(resolve => {
const visibleRatio = await new Promise<number>(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
@ -479,7 +479,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
// there are rafs.
requestAnimationFrame(() => {});
});
return visibleRatio > 0;
return visibleRatio;
});
}
}

View File

@ -105,7 +105,7 @@ export class Screenshotter {
await this._page.setViewport(overridenViewport);
}
await handle._scrollIntoViewIfNeeded();
await handle.scrollIntoViewIfNeeded();
boundingBox = enclosingIntRect(await this._page._delegate.getBoundingBoxForScreenshot(handle));
}

View File

@ -3,6 +3,23 @@
position: absolute;
width: 100px;
height: 20px;
margin: 0;
}
body, html {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
position: relative;
}
div {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
#btn0 { right: 0px; top: 0; }
@ -17,6 +34,7 @@
#btn9 { right: -90px; top: 225px; }
#btn10 { right: -100px; top: 250px; }
</style>
<div>
<button id=btn0>0</button>
<button id=btn1>1</button>
<button id=btn2>2</button>
@ -28,6 +46,7 @@
<button id=btn8>8</button>
<button id=btn9>9</button>
<button id=btn10>10</button>
</div>
<script>
window.addEventListener('DOMContentLoaded', () => {
for (const button of Array.from(document.querySelectorAll('button')))

View File

@ -248,14 +248,13 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
});
});
describe('ElementHandle.isIntersectingViewport', function() {
describe('ElementHandle.visibleRatio', function() {
it('should work', async({page, server}) => {
await page.goto(server.PREFIX + '/offscreenbuttons.html');
for (let i = 0; i < 11; ++i) {
const button = await page.$('#btn' + i);
// All but last button are visible.
const visible = i < 10;
expect(await button.isIntersectingViewport()).toBe(visible);
const ratio = await button.visibleRatio();
expect(Math.round(ratio * 10)).toBe(10 - i);
}
});
it.skip(FFOX)('should work when Node is removed', async({page, server}) => {
@ -263,9 +262,23 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROME, WEBKIT}) {
await page.evaluate(() => delete window['Node']);
for (let i = 0; i < 11; ++i) {
const button = await page.$('#btn' + i);
// All but last button are visible.
const visible = i < 10;
expect(await button.isIntersectingViewport()).toBe(visible);
const ratio = await button.visibleRatio();
expect(Math.round(ratio * 10)).toBe(10 - i);
}
});
});
describe('ElementHandle.scrollIntoViewIfNeeded', function() {
it('should work', async({page, server}) => {
await page.goto(server.PREFIX + '/offscreenbuttons.html');
for (let i = 0; i < 11; ++i) {
const button = await page.$('#btn' + i);
const before = await button.visibleRatio();
expect(Math.round(before * 10)).toBe(10 - i);
await button.scrollIntoViewIfNeeded();
const after = await button.visibleRatio();
expect(Math.round(after * 10)).toBe(10);
await page.evaluate(() => window.scrollTo(0, 0));
}
});
});