Skip to content

Commit

Permalink
chore: implement ElementHandle.prototype.clickablePoint
Browse files Browse the repository at this point in the history
  • Loading branch information
jrandolf committed Aug 22, 2023
1 parent 0242748 commit d73d57a
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 104 deletions.
18 changes: 15 additions & 3 deletions packages/puppeteer-core/src/api/ElementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Point>;
async clickablePoint(): Promise<Point> {
throw new Error('Not implemented');
async clickablePoint(offset?: Offset): Promise<Point> {
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,
};
}

/**
Expand Down
99 changes: 1 addition & 98 deletions packages/puppeteer-core/src/common/ElementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand Down Expand Up @@ -188,75 +186,6 @@ export class CDPElementHandle<
return {offsetX, offsetY};
}

override async clickablePoint(offset?: Offset): Promise<Point> {
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<void | Protocol.DOM.GetBoxModelResponse> {
const params: Protocol.DOM.GetBoxModelRequest = {
objectId: this.id,
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
12 changes: 12 additions & 0 deletions test/TestExpectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down
6 changes: 3 additions & 3 deletions test/src/elementhandle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);
});
Expand All @@ -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',
]);
});
Expand All @@ -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',
]);
});
Expand Down

0 comments on commit d73d57a

Please sign in to comment.