fix(page): teach page.click() to click partially offscreen buttons (#2806)

Originally, we use `Element.scrollIntoViewIfNeeded` to make sure
button is on screen before trying to click it.

However, `Element.scrollIntoViewIfNeeded` doesn't work in certain
scenarios, e.g. when element is partially visible and horizontal
scrolling is required to make it fully visible.

This patch polyfills `element.scrollIntoViewIfNeeded` using
IntersectionObserver and `Element.scrollIntoView`.

Fixes #2804.
This commit is contained in:
Andrey Lushnikov 2018-06-26 18:00:55 -07:00 committed by GitHub
parent 6ca43cf761
commit f55d005cbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 75 additions and 10 deletions

View File

@ -55,12 +55,20 @@ class ElementHandle extends JSHandle {
}
async _scrollIntoViewIfNeeded() {
const error = await this.executionContext().evaluate(element => {
const error = await this.executionContext().evaluate(async element => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
element.scrollIntoViewIfNeeded();
const visibleRatio = await new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
});
if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return false;
}, this);
if (error)
@ -70,8 +78,7 @@ class ElementHandle extends JSHandle {
/**
* @return {!Promise<{x: number, y: number}>}
*/
async _visibleCenter() {
await this._scrollIntoViewIfNeeded();
async _boundingBoxCenter() {
const box = await this._assertBoundingBox();
return {
x: box.x + box.width / 2,
@ -102,7 +109,8 @@ class ElementHandle extends JSHandle {
}
async hover() {
const {x, y} = await this._visibleCenter();
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._boundingBoxCenter();
await this._page.mouse.move(x, y);
}
@ -110,7 +118,8 @@ class ElementHandle extends JSHandle {
* @param {!Object=} options
*/
async click(options = {}) {
const {x, y} = await this._visibleCenter();
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._boundingBoxCenter();
await this._page.mouse.click(x, y, options);
}
@ -125,7 +134,8 @@ class ElementHandle extends JSHandle {
}
async tap() {
const {x, y} = await this._visibleCenter();
await this._scrollIntoViewIfNeeded();
const {x, y} = await this._boundingBoxCenter();
await this._page.touchscreen.tap(x, y);
}
@ -222,9 +232,7 @@ class ElementHandle extends JSHandle {
needsViewportReset = true;
}
await this.executionContext().evaluate(function(element) {
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
}, this);
await this._scrollIntoViewIfNeeded();
boundingBox = await this._assertBoundingBox();

View File

@ -0,0 +1,34 @@
<style>
button {
position: absolute;
width: 100px;
height: 20px;
}
#btn0 { right: 0px; top: 0; }
#btn1 { right: -10px; top: 25px; }
#btn2 { right: -20px; top: 50px; }
#btn3 { right: -30px; top: 75px; }
#btn4 { right: -40px; top: 100px; }
#btn5 { right: -50px; top: 125px; }
#btn6 { right: -60px; top: 150px; }
#btn7 { right: -70px; top: 175px; }
#btn8 { right: -80px; top: 200px; }
#btn9 { right: -90px; top: 225px; }
</style>
<button id=btn0>0</button>
<button id=btn1>1</button>
<button id=btn2>2</button>
<button id=btn3>3</button>
<button id=btn4>4</button>
<button id=btn5>5</button>
<button id=btn6>6</button>
<button id=btn7>7</button>
<button id=btn8>8</button>
<button id=btn9>9</button>
<script>
window.addEventListener('DOMContentLoaded', () => {
for (const button of Array.from(document.querySelectorAll('button')))
button.addEventListener('click', () => console.log('button #' + button.textContent + ' clicked'), false);
}, false);
</script>

View File

@ -29,6 +29,29 @@ module.exports.addTests = function({testRunner, expect, DeviceDescriptors}) {
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should click offscreen buttons', async({page, server}) => {
await page.goto(server.PREFIX + '/offscreenbuttons.html');
const messages = [];
page.on('console', msg => messages.push(msg.text()));
for (let i = 0; i < 10; ++i) {
// We might've scrolled to click a button - reset to (0, 0).
await page.evaluate(() => window.scrollTo(0, 0));
await page.click(`#btn${i}`);
}
expect(messages).toEqual([
'button #0 clicked',
'button #1 clicked',
'button #2 clicked',
'button #3 clicked',
'button #4 clicked',
'button #5 clicked',
'button #6 clicked',
'button #7 clicked',
'button #8 clicked',
'button #9 clicked'
]);
});
it('should click on checkbox input and toggle', async({page, server}) => {
await page.goto(server.PREFIX + '/input/checkbox.html');
expect(await page.evaluate(() => result.check)).toBe(null);