Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to specify offsets for JSHandle.click #7573

Merged
merged 1 commit into from
Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@
* [elementHandle.boundingBox()](#elementhandleboundingbox)
* [elementHandle.boxModel()](#elementhandleboxmodel)
* [elementHandle.click([options])](#elementhandleclickoptions)
* [elementHandle.clickablePoint()](#elementhandleclickablepoint)
* [elementHandle.clickablePoint([offset])](#elementhandleclickablepointoffset)
* [elementHandle.contentFrame()](#elementhandlecontentframe)
* [elementHandle.dispose()](#elementhandledispose)
* [elementHandle.drag(target)](#elementhandledragtarget)
Expand Down Expand Up @@ -4445,13 +4445,19 @@ This method returns boxes of the element, or `null` if the element is not visibl
- `button` <"left"|"right"|"middle"> Defaults to `left`.
- `clickCount` <[number]> defaults to 1. See [UIEvent.detail].
- `delay` <[number]> Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0.
- `offset` <[Object]> Offset in pixels relative to the top-left corder of the border box of the element.
- `x` <number> x-offset in pixels relative to the top-left corder of the border box of the element.
- `y` <number> y-offset in pixels relative to the top-left corder of the border box of the element.
- returns: <[Promise]> Promise which resolves when the element is successfully clicked. Promise gets rejected if the element is detached from DOM.

This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element.
If the element is detached from DOM, the method throws an error.

#### elementHandle.clickablePoint()
#### elementHandle.clickablePoint([offset])

- `offset` <[Object]>
- `x` <number> x-offset in pixels relative to the top-left corder of the border box of the element.
- `y` <number> y-offset in pixels relative to the top-left corder of the border box of the element.
- returns: <[Promise<[Point]>]> Resolves to the x, y point that describes the element's position.

#### elementHandle.contentFrame()
Expand Down
49 changes: 46 additions & 3 deletions src/common/JSHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,10 @@ export class ElementHandle<
if (error) throw new Error(error);
}

async clickablePoint(): Promise<Point> {
/**
* Returns the middle point within an element unless a specific offset is provided.
*/
async clickablePoint(offset?: Offset): Promise<Point> {
const [result, layoutMetrics] = await Promise.all([
this._client
.send('DOM.getContentQuads', {
Expand All @@ -434,8 +437,30 @@ export class ElementHandle<
.filter((quad) => computeQuadArea(quad) > 1);
if (!quads.length)
throw new Error('Node is either not clickable or not an HTMLElement');
// Return the middle point of the first quad.
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) {
Expand Down Expand Up @@ -495,7 +520,7 @@ export class ElementHandle<
*/
async click(options: ClickOptions = {}): Promise<void> {
await this._scrollIntoViewIfNeeded();
const { x, y } = await this.clickablePoint();
const { x, y } = await this.clickablePoint(options.offset);
await this._page.mouse.click(x, y, options);
}

Expand Down Expand Up @@ -1011,6 +1036,20 @@ export class ElementHandle<
}
}

/**
* @public
*/
export interface Offset {
/**
* x-offset for the clickable point relative to the top-left corder of the border box.
*/
x: number;
/**
* y-offset for the clickable point relative to the top-left corder of the border box.
*/
y: number;
}

/**
* @public
*/
Expand All @@ -1029,6 +1068,10 @@ export interface ClickOptions {
* @defaultValue 1
*/
clickCount?: number;
/**
* Offset for the clickable point relative to the top-left corder of the border box.
*/
offset?: Offset;
}

/**
Expand Down
91 changes: 91 additions & 0 deletions test/jshandle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,95 @@ describe('JSHandle', function () {
);
});
});

describe('JSHandle.clickablePoint', function () {
it('should work', async () => {
const { page } = getTestState();

await page.evaluate(() => {
document.body.style.padding = '0';
document.body.style.margin = '0';
document.body.innerHTML = `
<div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
`;
});

const divHandle = await page.$('div');
expect(await divHandle.clickablePoint()).toEqual({
x: 45 + 60, // margin + middle point offset
y: 45 + 30, // margin + middle point offset
});
expect(
await divHandle.clickablePoint({
x: 10,
y: 15,
})
).toEqual({
x: 30 + 10, // margin + offset
y: 30 + 15, // margin + offset
});
});

it('should work for iframes', async () => {
const { page } = getTestState();
await page.evaluate(() => {
document.body.style.padding = '10px';
document.body.style.margin = '10px';
document.body.innerHTML = `
<iframe style="border: none; margin: 0; padding: 0;" seamless sandbox srcdoc="<style>* { margin: 0; padding: 0;}</style><div style='cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;' />"></iframe>
`;
});
const frame = page.frames()[1];
const divHandle = await frame.$('div');
expect(await divHandle.clickablePoint()).toEqual({
x: 20 + 45 + 60, // iframe pos + margin + middle point offset
y: 20 + 45 + 30, // iframe pos + margin + middle point offset
});
expect(
await divHandle.clickablePoint({
x: 10,
y: 15,
})
).toEqual({
x: 20 + 30 + 10, // iframe pos + margin + offset
y: 20 + 30 + 15, // iframe pos + margin + offset
});
});
});

describe('JSHandle.click', function () {
itFailsFirefox('should work', async () => {
const { page } = getTestState();

const clicks = [];

await page.exposeFunction('reportClick', (x: number, y: number): void => {
clicks.push([x, y]);
});

await page.evaluate(() => {
document.body.style.padding = '0';
document.body.style.margin = '0';
document.body.innerHTML = `
<div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
`;
document.body.addEventListener('click', (e) => {
(window as any).reportClick(e.clientX, e.clientY);
});
});

const divHandle = await page.$('div');
await divHandle.click();
await divHandle.click({
offset: {
x: 10,
y: 15,
},
});
expect(clicks).toEqual([
[45 + 60, 45 + 30], // margin + middle point offset
[30 + 10, 30 + 15], // margin + offset
]);
});
});
});
7 changes: 7 additions & 0 deletions utils/doclint/check_public_api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,13 @@ function compareDocumentations(actual, expected) {
expectedName: 'ClickOptions',
},
],
[
'Method ElementHandle.clickablePoint() offset',
{
actualName: 'Object',
expectedName: 'Offset',
},
],
[
'Method ElementHandle.press() options',
{
Expand Down