fix(page): intersect content quads with viewport (#4277)

In certain cases inline element children might be positioned
outside of viewport.

In this case, we should intersect all content quads with viewport
before we pick one to click into.

Fixes #4274.
This commit is contained in:
Andrey Lushnikov 2019-04-11 21:11:20 -04:00 committed by GitHub
parent 20988775bf
commit 5ee21d97e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 34 additions and 4 deletions

View File

@ -189,13 +189,17 @@ class ElementHandle extends JSHandle {
* @return {!Promise<!{x: number, y: number}>} * @return {!Promise<!{x: number, y: number}>}
*/ */
async _clickablePoint() { async _clickablePoint() {
const result = await this._client.send('DOM.getContentQuads', { const [result, layoutMetrics] = await Promise.all([
objectId: this._remoteObject.objectId this._client.send('DOM.getContentQuads', {
}).catch(debugError); objectId: this._remoteObject.objectId
}).catch(debugError),
this._client.send('Page.getLayoutMetrics'),
]);
if (!result || !result.quads.length) if (!result || !result.quads.length)
throw new Error('Node is either not visible or not an HTMLElement'); throw new Error('Node is either not visible or not an HTMLElement');
// Filter out quads that have too small area to click into. // Filter out quads that have too small area to click into.
const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).filter(quad => computeQuadArea(quad) > 1); const {clientWidth, clientHeight} = layoutMetrics.layoutViewport;
const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).map(quad => this._intersectQuadWithViewport(quad, clientWidth, clientHeight)).filter(quad => computeQuadArea(quad) > 1);
if (!quads.length) if (!quads.length)
throw new Error('Node is either not visible or not an HTMLElement'); throw new Error('Node is either not visible or not an HTMLElement');
// Return the middle point of the first quad. // Return the middle point of the first quad.
@ -234,6 +238,19 @@ class ElementHandle extends JSHandle {
]; ];
} }
/**
* @param {!Array<{x: number, y: number}>} quad
* @param {number} width
* @param {number} height
* @return {!Array<{x: number, y: number}>}
*/
_intersectQuadWithViewport(quad, width, height) {
return quad.map(point => ({
x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height),
}));
}
async hover() { async hover() {
await this._scrollIntoViewIfNeeded(); await this._scrollIntoViewIfNeeded();
const {x, y} = await this._clickablePoint(); const {x, y} = await this._clickablePoint();

View File

@ -50,6 +50,19 @@ module.exports.addTests = function({testRunner, expect}) {
]); ]);
expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
}); });
it_fails_ffox('should click when one of inline box children is outside of viewport', async({page, server}) => {
await page.setContent(`
<style>
i {
position: absolute;
top: -1000px;
}
</style>
<span onclick='javascript:window.CLICKED = 42;'><i>woof</i><b>doggo</b></span>
`);
await page.click('span');
expect(await page.evaluate(() => window.CLICKED)).toBe(42);
});
it('should select the text by triple clicking', async({page, server}) => { it('should select the text by triple clicking', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html'); await page.goto(server.PREFIX + '/input/textarea.html');
await page.focus('textarea'); await page.focus('textarea');