From 5955affab025bdc9506fe47485b0e6c0bacaa828 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Fri, 29 Jun 2018 12:03:02 -0700 Subject: [PATCH] fix(page.click): teach puppeteer click wrapped links (#2822) This patch teaches Puppeteer to click elements that are part of inline layout and that wrap on multiple lines. Fixes #2798. --- lib/ElementHandle.js | 63 +++++++++++++++++++++++------------- test/assets/wrappedlink.html | 29 +++++++++++++++++ test/input.spec.js | 9 ++++++ 3 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 test/assets/wrappedlink.html diff --git a/lib/ElementHandle.js b/lib/ElementHandle.js index ef356ba18fb..07972fd1313 100644 --- a/lib/ElementHandle.js +++ b/lib/ElementHandle.js @@ -15,7 +15,7 @@ */ const path = require('path'); const {JSHandle} = require('./ExecutionContext'); -const {helper, debugError} = require('./helper'); +const {helper, assert, debugError} = require('./helper'); class ElementHandle extends JSHandle { /** @@ -76,13 +76,29 @@ class ElementHandle extends JSHandle { } /** - * @return {!Promise<{x: number, y: number}>} + * @return {!Promise} */ - async _boundingBoxCenter() { - const box = await this._assertBoundingBox(); + async _clickablePoint() { + const result = await this._client.send('DOM.getContentQuads', { + objectId: this._remoteObject.objectId + }).catch(debugError); + if (!result || !result.quads.length) + throw new Error('Node is either not visible or not an HTMLElement'); + // 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); + if (!quads.length) + throw new Error('Node is either not visible or not an HTMLElement'); + // Return the middle point of the first quad. + const quad = quads[0]; + let x = 0; + let y = 0; + for (const point of quad) { + x += point.x; + y += point.y; + } return { - x: box.x + box.width / 2, - y: box.y + box.height / 2 + x: x / 4, + y: y / 4 }; } @@ -110,7 +126,7 @@ class ElementHandle extends JSHandle { async hover() { await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._boundingBoxCenter(); + const {x, y} = await this._clickablePoint(); await this._page.mouse.move(x, y); } @@ -119,7 +135,7 @@ class ElementHandle extends JSHandle { */ async click(options = {}) { await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._boundingBoxCenter(); + const {x, y} = await this._clickablePoint(); await this._page.mouse.click(x, y, options); } @@ -135,7 +151,7 @@ class ElementHandle extends JSHandle { async tap() { await this._scrollIntoViewIfNeeded(); - const {x, y} = await this._boundingBoxCenter(); + const {x, y} = await this._clickablePoint(); await this._page.touchscreen.tap(x, y); } @@ -199,17 +215,6 @@ class ElementHandle extends JSHandle { }; } - /** - * @return {!Promise} - */ - async _assertBoundingBox() { - const boundingBox = await this.boundingBox(); - if (boundingBox) - return boundingBox; - - throw new Error('Node is either not visible or not an HTMLElement'); - } - /** * * @param {!Object=} options @@ -218,7 +223,8 @@ class ElementHandle extends JSHandle { async screenshot(options = {}) { let needsViewportReset = false; - let boundingBox = await this._assertBoundingBox(); + let boundingBox = await this.boundingBox(); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); const viewport = this._page.viewport(); @@ -234,7 +240,8 @@ class ElementHandle extends JSHandle { await this._scrollIntoViewIfNeeded(); - boundingBox = await this._assertBoundingBox(); + boundingBox = await this.boundingBox(); + assert(boundingBox, 'Node is either not visible or not an HTMLElement'); const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics'); @@ -349,5 +356,17 @@ class ElementHandle extends JSHandle { } } +function computeQuadArea(quad) { + // Compute sum of all directed areas of adjacent triangles + // https://en.wikipedia.org/wiki/Polygon#Simple_polygons + let area = 0; + for (let i = 0; i < quad.length; ++i) { + const p1 = quad[i]; + const p2 = quad[(i + 1) % quad.length]; + area += (p1.x * p2.y - p2.x * p1.y) / 2; + } + return area; +} + module.exports = ElementHandle; helper.tracePublicAPI(ElementHandle); diff --git a/test/assets/wrappedlink.html b/test/assets/wrappedlink.html new file mode 100644 index 00000000000..55e9c3f9f60 --- /dev/null +++ b/test/assets/wrappedlink.html @@ -0,0 +1,29 @@ + +
+ 123321 +
+ diff --git a/test/input.spec.js b/test/input.spec.js index 7fdc2e5633c..05f735ad82e 100644 --- a/test/input.spec.js +++ b/test/input.spec.js @@ -52,6 +52,15 @@ module.exports.addTests = function({testRunner, expect, DeviceDescriptors}) { ]); }); + it('should click wrapped links', async({page, server}) => { + await page.goto(server.PREFIX + '/wrappedlink.html'); + await Promise.all([ + page.click('a'), + page.waitForNavigation() + ]); + expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#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);