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() {
|
async _scrollIntoViewIfNeeded() {
|
||||||
const error = await this.executionContext().evaluate(element => {
|
const error = await this.executionContext().evaluate(async element => {
|
||||||
if (!element.isConnected)
|
if (!element.isConnected)
|
||||||
return 'Node is detached from document';
|
return 'Node is detached from document';
|
||||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
if (element.nodeType !== Node.ELEMENT_NODE)
|
||||||
return 'Node is not of type HTMLElement';
|
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;
|
return false;
|
||||||
}, this);
|
}, this);
|
||||||
if (error)
|
if (error)
|
||||||
@ -70,8 +78,7 @@ class ElementHandle extends JSHandle {
|
|||||||
/**
|
/**
|
||||||
* @return {!Promise<{x: number, y: number}>}
|
* @return {!Promise<{x: number, y: number}>}
|
||||||
*/
|
*/
|
||||||
async _visibleCenter() {
|
async _boundingBoxCenter() {
|
||||||
await this._scrollIntoViewIfNeeded();
|
|
||||||
const box = await this._assertBoundingBox();
|
const box = await this._assertBoundingBox();
|
||||||
return {
|
return {
|
||||||
x: box.x + box.width / 2,
|
x: box.x + box.width / 2,
|
||||||
@ -102,7 +109,8 @@ class ElementHandle extends JSHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async hover() {
|
async hover() {
|
||||||
const {x, y} = await this._visibleCenter();
|
await this._scrollIntoViewIfNeeded();
|
||||||
|
const {x, y} = await this._boundingBoxCenter();
|
||||||
await this._page.mouse.move(x, y);
|
await this._page.mouse.move(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +118,8 @@ class ElementHandle extends JSHandle {
|
|||||||
* @param {!Object=} options
|
* @param {!Object=} options
|
||||||
*/
|
*/
|
||||||
async click(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);
|
await this._page.mouse.click(x, y, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +134,8 @@ class ElementHandle extends JSHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async tap() {
|
async tap() {
|
||||||
const {x, y} = await this._visibleCenter();
|
await this._scrollIntoViewIfNeeded();
|
||||||
|
const {x, y} = await this._boundingBoxCenter();
|
||||||
await this._page.touchscreen.tap(x, y);
|
await this._page.touchscreen.tap(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,9 +232,7 @@ class ElementHandle extends JSHandle {
|
|||||||
needsViewportReset = true;
|
needsViewportReset = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.executionContext().evaluate(function(element) {
|
await this._scrollIntoViewIfNeeded();
|
||||||
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
|
||||||
}, this);
|
|
||||||
|
|
||||||
boundingBox = await this._assertBoundingBox();
|
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');
|
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}) => {
|
it('should click on checkbox input and toggle', async({page, server}) => {
|
||||||
await page.goto(server.PREFIX + '/input/checkbox.html');
|
await page.goto(server.PREFIX + '/input/checkbox.html');
|
||||||
expect(await page.evaluate(() => result.check)).toBe(null);
|
expect(await page.evaluate(() => result.check)).toBe(null);
|
||||||
|
Loading…
Reference in New Issue
Block a user