From d73d57ab1066df815a5c59e9a763a4aa3fecf440 Mon Sep 17 00:00:00 2001 From: Randolf Date: Tue, 22 Aug 2023 23:53:07 +0200 Subject: [PATCH] chore: implement `ElementHandle.prototype.clickablePoint` --- .../puppeteer-core/src/api/ElementHandle.ts | 18 +++- .../src/common/ElementHandle.ts | 99 +------------------ test/TestExpectations.json | 12 +++ test/src/elementhandle.spec.ts | 6 +- 4 files changed, 31 insertions(+), 104 deletions(-) diff --git a/packages/puppeteer-core/src/api/ElementHandle.ts b/packages/puppeteer-core/src/api/ElementHandle.ts index af02951e47b44..bb5dee4329898 100644 --- a/packages/puppeteer-core/src/api/ElementHandle.ts +++ b/packages/puppeteer-core/src/api/ElementHandle.ts @@ -626,9 +626,21 @@ export abstract class ElementHandle< /** * Returns the middle point within an element unless a specific offset is provided. */ - async clickablePoint(offset?: Offset): Promise; - async clickablePoint(): Promise { - throw new Error('Not implemented'); + async clickablePoint(offset?: Offset): Promise { + const model = await this.boundingBox(); + if (!model) { + throw new Error('Node is either not clickable or not an Element'); + } + if (offset !== undefined) { + return { + x: model.x + offset.x, + y: model.y + offset.y, + }; + } + return { + x: model.x + model.width / 2, + y: model.y + model.height / 2, + }; } /** diff --git a/packages/puppeteer-core/src/common/ElementHandle.ts b/packages/puppeteer-core/src/common/ElementHandle.ts index 67adf8155977b..3fcd46d9ea2b4 100644 --- a/packages/puppeteer-core/src/common/ElementHandle.ts +++ b/packages/puppeteer-core/src/common/ElementHandle.ts @@ -22,11 +22,10 @@ import { BoxModel, ClickOptions, ElementHandle, - Offset, Point, Quad, } from '../api/ElementHandle.js'; -import {KeyPressOptions, KeyboardTypeOptions} from '../api/Input.js'; +import {KeyboardTypeOptions, KeyPressOptions} from '../api/Input.js'; import {Page, ScreenshotOptions} from '../api/Page.js'; import {assert} from '../util/assert.js'; @@ -36,7 +35,6 @@ import {Frame} from './Frame.js'; import {FrameManager} from './FrameManager.js'; import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {CDPJSHandle} from './JSHandle.js'; -import {CDPPage} from './Page.js'; import {NodeFor} from './types.js'; import {KeyInput} from './USKeyboardLayout.js'; import {debugError} from './util.js'; @@ -188,75 +186,6 @@ export class CDPElementHandle< return {offsetX, offsetY}; } - override async clickablePoint(offset?: Offset): Promise { - const [result, layoutMetrics] = await Promise.all([ - this.client - .send('DOM.getContentQuads', { - objectId: this.id, - }) - .catch(debugError), - (this.#page as CDPPage)._client().send('Page.getLayoutMetrics'), - ]); - if (!result || !result.quads.length) { - throw new Error('Node is either not clickable or not an HTMLElement'); - } - // Filter out quads that have too small area to click into. - // Fallback to `layoutViewport` in case of using Firefox. - const {clientWidth, clientHeight} = - layoutMetrics.cssLayoutViewport || layoutMetrics.layoutViewport; - const {offsetX, offsetY} = await this.#getOOPIFOffsets(this.#frame); - const quads = result.quads - .map(quad => { - return this.#fromProtocolQuad(quad); - }) - .map(quad => { - return applyOffsetsToQuad(quad, offsetX, offsetY); - }) - .map(quad => { - return this.#intersectQuadWithViewport(quad, clientWidth, clientHeight); - }) - .filter(quad => { - return computeQuadArea(quad) > 1; - }); - if (!quads.length) { - throw new Error('Node is either not clickable or not an HTMLElement'); - } - const quad = quads[0]!; - if (offset) { - // Return the point of the first quad identified by offset. - let minX = Number.MAX_SAFE_INTEGER; - let minY = Number.MAX_SAFE_INTEGER; - for (const point of quad) { - if (point.x < minX) { - minX = point.x; - } - if (point.y < minY) { - minY = point.y; - } - } - if ( - minX !== Number.MAX_SAFE_INTEGER && - minY !== Number.MAX_SAFE_INTEGER - ) { - return { - x: minX + offset.x, - y: minY + offset.y, - }; - } - } - // Return the middle point of the first quad. - let x = 0; - let y = 0; - for (const point of quad) { - x += point.x; - y += point.y; - } - return { - x: x / 4, - y: y / 4, - }; - } - #getBoxModel(): Promise { const params: Protocol.DOM.GetBoxModelRequest = { objectId: this.id, @@ -275,19 +204,6 @@ export class CDPElementHandle< ]; } - #intersectQuadWithViewport( - quad: Point[], - width: number, - height: number - ): Point[] { - return quad.map(point => { - return { - x: Math.min(Math.max(point.x, 0), width), - y: Math.min(Math.max(point.y, 0), height), - }; - }); - } - /** * This method scrolls element into view if needed, and then * uses {@link Page.mouse} to hover over the center of the element. @@ -596,16 +512,3 @@ export class CDPElementHandle< assert(this.executionContext()._world); } } - -function computeQuadArea(quad: Point[]): number { - /* 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 Math.abs(area); -} diff --git a/test/TestExpectations.json b/test/TestExpectations.json index 008d61cce3577..e5e89cfa7f970 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -665,6 +665,12 @@ "parameters": ["webDriverBiDi"], "expectations": ["PASS"] }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, { "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.contentFrame should work", "platforms": ["darwin", "linux", "win32"], @@ -2123,6 +2129,12 @@ "parameters": ["chrome", "webDriverBiDi"], "expectations": ["PASS"] }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work for iframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, { "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.contentFrame should work", "platforms": ["darwin", "linux", "win32"], diff --git a/test/src/elementhandle.spec.ts b/test/src/elementhandle.spec.ts index 60e95a790dc38..b0dcc5b578065 100644 --- a/test/src/elementhandle.spec.ts +++ b/test/src/elementhandle.spec.ts @@ -294,7 +294,7 @@ describe('ElementHandle specs', function () { return error_; }); expect(error.message).atLeastOneToContain([ - 'Node is either not clickable or not an HTMLElement', + 'Node is either not clickable or not an Element', 'no such element', ]); }); @@ -310,7 +310,7 @@ describe('ElementHandle specs', function () { return error_; }); expect(error.message).atLeastOneToContain([ - 'Node is either not clickable or not an HTMLElement', + 'Node is either not clickable or not an Element', 'no such element', ]); }); @@ -323,7 +323,7 @@ describe('ElementHandle specs', function () { return error_; }); expect(error.message).atLeastOneToContain([ - 'Node is either not clickable or not an HTMLElement', + 'Node is either not clickable or not an Element', 'no such node', ]); });