feat(scrollIntoView): expose scrollIntoViewIfNeeded in api (#382)
This also replaces isIntersectingViewport with visibleRatio for more flexibility.
This commit is contained in:
parent
58b8e66df8
commit
491eeeef7e
23
docs/api.md
23
docs/api.md
|
|
@ -58,15 +58,16 @@
|
||||||
* [elementHandle.fill(value)](#elementhandlefillvalue)
|
* [elementHandle.fill(value)](#elementhandlefillvalue)
|
||||||
* [elementHandle.focus()](#elementhandlefocus)
|
* [elementHandle.focus()](#elementhandlefocus)
|
||||||
* [elementHandle.hover([options])](#elementhandlehoveroptions)
|
* [elementHandle.hover([options])](#elementhandlehoveroptions)
|
||||||
* [elementHandle.isIntersectingViewport()](#elementhandleisintersectingviewport)
|
|
||||||
* [elementHandle.ownerFrame()](#elementhandleownerframe)
|
* [elementHandle.ownerFrame()](#elementhandleownerframe)
|
||||||
* [elementHandle.press(key[, options])](#elementhandlepresskey-options)
|
* [elementHandle.press(key[, options])](#elementhandlepresskey-options)
|
||||||
* [elementHandle.screenshot([options])](#elementhandlescreenshotoptions)
|
* [elementHandle.screenshot([options])](#elementhandlescreenshotoptions)
|
||||||
|
* [elementHandle.scrollIntoViewIfNeeded()](#elementhandlescrollintoviewifneeded)
|
||||||
* [elementHandle.select(...values)](#elementhandleselectvalues)
|
* [elementHandle.select(...values)](#elementhandleselectvalues)
|
||||||
* [elementHandle.setInputFiles(...files)](#elementhandlesetinputfilesfiles)
|
* [elementHandle.setInputFiles(...files)](#elementhandlesetinputfilesfiles)
|
||||||
* [elementHandle.toString()](#elementhandletostring)
|
* [elementHandle.toString()](#elementhandletostring)
|
||||||
* [elementHandle.tripleclick([options])](#elementhandletripleclickoptions)
|
* [elementHandle.tripleclick([options])](#elementhandletripleclickoptions)
|
||||||
* [elementHandle.type(text[, options])](#elementhandletypetext-options)
|
* [elementHandle.type(text[, options])](#elementhandletypetext-options)
|
||||||
|
* [elementHandle.visibleRatio()](#elementhandlevisibleratio)
|
||||||
- [class: Frame](#class-frame)
|
- [class: Frame](#class-frame)
|
||||||
* [frame.$(selector)](#frameselector)
|
* [frame.$(selector)](#frameselector)
|
||||||
* [frame.$$(selector)](#frameselector-1)
|
* [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.
|
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.
|
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()
|
#### elementHandle.ownerFrame()
|
||||||
- returns: <[Promise]<[Frame]>> Returns the frame containing the given element.
|
- 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.
|
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.
|
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)
|
#### 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.
|
- `...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`.
|
- `value` <[string]> Matches by `option.value`.
|
||||||
|
|
@ -891,6 +898,11 @@ await elementHandle.type('some text');
|
||||||
await elementHandle.press('Enter');
|
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
|
### class: Frame
|
||||||
|
|
||||||
At every point of time, page exposes its current frame tree via the [page.mainFrame()](#pagemainframe) and [frame.childFrames()](#framechildframes) methods.
|
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.
|
- `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]>
|
- x <[number]>
|
||||||
- y <[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,
|
- `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.
|
||||||
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`.
|
- `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.
|
- `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`.
|
- 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`.
|
||||||
|
|
|
||||||
10
src/dom.ts
10
src/dom.ts
|
|
@ -134,7 +134,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
return this._page._delegate.getContentFrame(this);
|
return this._page._delegate.getContentFrame(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _scrollIntoViewIfNeeded() {
|
async scrollIntoViewIfNeeded() {
|
||||||
const error = await this._evaluateInUtility(async (node: Node, pageJavascriptEnabled: boolean) => {
|
const error = await this._evaluateInUtility(async (node: Node, pageJavascriptEnabled: boolean) => {
|
||||||
if (!node.isConnected)
|
if (!node.isConnected)
|
||||||
return 'Node is detached from document';
|
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> {
|
private async _ensurePointerActionPoint(relativePoint?: types.Point): Promise<types.Point> {
|
||||||
await this._scrollIntoViewIfNeeded();
|
await this.scrollIntoViewIfNeeded();
|
||||||
if (!relativePoint)
|
if (!relativePoint)
|
||||||
return this._clickablePoint();
|
return this._clickablePoint();
|
||||||
let r = await this._viewportPointAndScroll(relativePoint);
|
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);
|
return this._context._$$('xpath=' + expression, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
isIntersectingViewport(): Promise<boolean> {
|
visibleRatio(): Promise<number> {
|
||||||
return this._evaluateInUtility(async (node: Node) => {
|
return this._evaluateInUtility(async (node: Node) => {
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||||
throw new Error('Node is not of type HTMLElement');
|
throw new Error('Node is not of type HTMLElement');
|
||||||
const element = node as Element;
|
const element = node as Element;
|
||||||
const visibleRatio = await new Promise(resolve => {
|
const visibleRatio = await new Promise<number>(resolve => {
|
||||||
const observer = new IntersectionObserver(entries => {
|
const observer = new IntersectionObserver(entries => {
|
||||||
resolve(entries[0].intersectionRatio);
|
resolve(entries[0].intersectionRatio);
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
|
@ -479,7 +479,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
// there are rafs.
|
// there are rafs.
|
||||||
requestAnimationFrame(() => {});
|
requestAnimationFrame(() => {});
|
||||||
});
|
});
|
||||||
return visibleRatio > 0;
|
return visibleRatio;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export class Screenshotter {
|
||||||
await this._page.setViewport(overridenViewport);
|
await this._page.setViewport(overridenViewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
await handle._scrollIntoViewIfNeeded();
|
await handle.scrollIntoViewIfNeeded();
|
||||||
boundingBox = enclosingIntRect(await this._page._delegate.getBoundingBoxForScreenshot(handle));
|
boundingBox = enclosingIntRect(await this._page._delegate.getBoundingBoxForScreenshot(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,23 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 20px;
|
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; }
|
#btn0 { right: 0px; top: 0; }
|
||||||
|
|
@ -17,6 +34,7 @@
|
||||||
#btn9 { right: -90px; top: 225px; }
|
#btn9 { right: -90px; top: 225px; }
|
||||||
#btn10 { right: -100px; top: 250px; }
|
#btn10 { right: -100px; top: 250px; }
|
||||||
</style>
|
</style>
|
||||||
|
<div>
|
||||||
<button id=btn0>0</button>
|
<button id=btn0>0</button>
|
||||||
<button id=btn1>1</button>
|
<button id=btn1>1</button>
|
||||||
<button id=btn2>2</button>
|
<button id=btn2>2</button>
|
||||||
|
|
@ -28,6 +46,7 @@
|
||||||
<button id=btn8>8</button>
|
<button id=btn8>8</button>
|
||||||
<button id=btn9>9</button>
|
<button id=btn9>9</button>
|
||||||
<button id=btn10>10</button>
|
<button id=btn10>10</button>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
for (const button of Array.from(document.querySelectorAll('button')))
|
for (const button of Array.from(document.querySelectorAll('button')))
|
||||||
|
|
|
||||||
|
|
@ -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}) => {
|
it('should work', async({page, server}) => {
|
||||||
await page.goto(server.PREFIX + '/offscreenbuttons.html');
|
await page.goto(server.PREFIX + '/offscreenbuttons.html');
|
||||||
for (let i = 0; i < 11; ++i) {
|
for (let i = 0; i < 11; ++i) {
|
||||||
const button = await page.$('#btn' + i);
|
const button = await page.$('#btn' + i);
|
||||||
// All but last button are visible.
|
const ratio = await button.visibleRatio();
|
||||||
const visible = i < 10;
|
expect(Math.round(ratio * 10)).toBe(10 - i);
|
||||||
expect(await button.isIntersectingViewport()).toBe(visible);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it.skip(FFOX)('should work when Node is removed', async({page, server}) => {
|
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']);
|
await page.evaluate(() => delete window['Node']);
|
||||||
for (let i = 0; i < 11; ++i) {
|
for (let i = 0; i < 11; ++i) {
|
||||||
const button = await page.$('#btn' + i);
|
const button = await page.$('#btn' + i);
|
||||||
// All but last button are visible.
|
const ratio = await button.visibleRatio();
|
||||||
const visible = i < 10;
|
expect(Math.round(ratio * 10)).toBe(10 - i);
|
||||||
expect(await button.isIntersectingViewport()).toBe(visible);
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue