diff --git a/docs/api.md b/docs/api.md index c996ff7c53c4f..cb065dd15e571 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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) @@ -4442,13 +4442,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 border box of the element. + - `x` x-offset in pixels relative to the border box of the element. + - `y` y-offset in pixels relative to 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` x-offset in pixels relative to the border box of the element. + - `y` y-offset in pixels relative to the border box of the element. - returns: <[Promise<[Point]>]> Resolves to the x, y point that describes the element's position. #### elementHandle.contentFrame() diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index 2e138c514e821..cc1f2bdffaf23 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -411,7 +411,10 @@ export class ElementHandle< if (error) throw new Error(error); } - async clickablePoint(): Promise { + /** + * Returns the middle point within an element unless a specific offset is provided. + */ + async clickablePoint(offset?: Offset): Promise { const [result, layoutMetrics] = await Promise.all([ this._client .send('DOM.getContentQuads', { @@ -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) { @@ -495,7 +520,7 @@ export class ElementHandle< */ async click(options: ClickOptions = {}): Promise { 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); } @@ -1011,6 +1036,20 @@ export class ElementHandle< } } +/** + * @public + */ +export interface Offset { + /** + * x-offset for the clickable point relative to the border box. + */ + x: number; + /** + * y-offset for the clickable point relative to the border box. + */ + y: number; +} + /** * @public */ @@ -1029,6 +1068,10 @@ export interface ClickOptions { * @defaultValue 1 */ clickCount?: number; + /** + * Offset for the clickable point relative to the border box. + */ + offset?: Offset; } /** diff --git a/test/jshandle.spec.ts b/test/jshandle.spec.ts index 7978d4acda5db..1efbf95bd5fc1 100644 --- a/test/jshandle.spec.ts +++ b/test/jshandle.spec.ts @@ -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 = ` +
+ `; + }); + + 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 = ` + + `; + }); + 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 () { + it('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 = ` +
+ `; + 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 + ]); + }); + }); }); diff --git a/utils/doclint/check_public_api/index.js b/utils/doclint/check_public_api/index.js index 0fffb31f1628f..3e4e68a1c5d2e 100644 --- a/utils/doclint/check_public_api/index.js +++ b/utils/doclint/check_public_api/index.js @@ -371,6 +371,13 @@ function compareDocumentations(actual, expected) { expectedName: 'ClickOptions', }, ], + [ + 'Method ElementHandle.clickablePoint() offset', + { + actualName: 'Object', + expectedName: 'Offset', + }, + ], [ 'Method ElementHandle.press() options', {