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:
parent
6ca43cf761
commit
f55d005cbe
@ -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();
|
||||
|
||||
|
34
test/assets/offscreenbuttons.html
Normal file
34
test/assets/offscreenbuttons.html
Normal 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>
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user