From e81a3c5901e024f7c41edf023daad60c68ed093d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 12 Apr 2021 12:41:25 -0700 Subject: [PATCH] api: add option `position` to check/uncheck (#6153) Since check/uncheck does click under the hood, sometimes it might need to click at a different position. One example would be a long label that contains links inside, and clicking in the center happens to hit the link instead of the label itself. --- docs/src/api/class-elementhandle.md | 4 ++ docs/src/api/class-frame.md | 4 ++ docs/src/api/class-page.md | 4 ++ src/protocol/channels.ts | 8 ++++ src/protocol/protocol.yml | 4 ++ src/protocol/validator.ts | 4 ++ src/server/dom.ts | 6 +-- tests/page-check.spec.ts | 12 ++++++ types/types.d.ts | 60 +++++++++++++++++++++++++++++ 9 files changed, 103 insertions(+), 3 deletions(-) diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index f418307a45..e87fe5cb96 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -134,6 +134,8 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.check.noWaitAfter = %%-input-no-wait-after-%% +### option: ElementHandle.check.position = %%-input-position-%% + ### option: ElementHandle.check.timeout = %%-input-timeout-%% ## async method: ElementHandle.click @@ -782,6 +784,8 @@ When all steps combined have not finished during the specified [`option: timeout ### option: ElementHandle.uncheck.noWaitAfter = %%-input-no-wait-after-%% +### option: ElementHandle.uncheck.position = %%-input-position-%% + ### option: ElementHandle.uncheck.timeout = %%-input-timeout-%% ## async method: ElementHandle.waitForElementState diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index b3cb1f6155..1e95ef0714 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -173,6 +173,8 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.check.noWaitAfter = %%-input-no-wait-after-%% +### option: Frame.check.position = %%-input-position-%% + ### option: Frame.check.timeout = %%-input-timeout-%% ## method: Frame.childFrames @@ -1102,6 +1104,8 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.uncheck.noWaitAfter = %%-input-no-wait-after-%% +### option: Frame.uncheck.position = %%-input-position-%% + ### option: Frame.uncheck.timeout = %%-input-timeout-%% ## method: Frame.url diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index af1602be50..fba95fac1f 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -531,6 +531,8 @@ Shortcut for main frame's [`method: Frame.check`]. ### option: Page.check.noWaitAfter = %%-input-no-wait-after-%% +### option: Page.check.position = %%-input-position-%% + ### option: Page.check.timeout = %%-input-timeout-%% ## async method: Page.click @@ -2505,6 +2507,8 @@ Shortcut for main frame's [`method: Frame.uncheck`]. ### option: Page.uncheck.noWaitAfter = %%-input-no-wait-after-%% +### option: Page.uncheck.position = %%-input-position-%% + ### option: Page.uncheck.timeout = %%-input-timeout-%% ## async method: Page.unroute diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 213f126414..3dc64c6eb4 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -1336,11 +1336,13 @@ export type FrameCheckParams = { selector: string, force?: boolean, noWaitAfter?: boolean, + position?: Point, timeout?: number, }; export type FrameCheckOptions = { force?: boolean, noWaitAfter?: boolean, + position?: Point, timeout?: number, }; export type FrameCheckResult = void; @@ -1698,11 +1700,13 @@ export type FrameUncheckParams = { selector: string, force?: boolean, noWaitAfter?: boolean, + position?: Point, timeout?: number, }; export type FrameUncheckOptions = { force?: boolean, noWaitAfter?: boolean, + position?: Point, timeout?: number, }; export type FrameUncheckResult = void; @@ -1902,11 +1906,13 @@ export type ElementHandleBoundingBoxResult = { export type ElementHandleCheckParams = { force?: boolean, noWaitAfter?: boolean, + position?: Point, timeout?: number, }; export type ElementHandleCheckOptions = { force?: boolean, noWaitAfter?: boolean, + position?: Point, timeout?: number, }; export type ElementHandleCheckResult = void; @@ -2174,11 +2180,13 @@ export type ElementHandleTypeResult = void; export type ElementHandleUncheckParams = { force?: boolean, noWaitAfter?: boolean, + position?: Point, timeout?: number, }; export type ElementHandleUncheckOptions = { force?: boolean, noWaitAfter?: boolean, + position?: Point, timeout?: number, }; export type ElementHandleUncheckResult = void; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 3b47e44789..f481853776 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -1045,6 +1045,7 @@ Frame: selector: string force: boolean? noWaitAfter: boolean? + position: Point? timeout: number? click: @@ -1352,6 +1353,7 @@ Frame: selector: string force: boolean? noWaitAfter: boolean? + position: Point? timeout: number? waitForFunction: @@ -1524,6 +1526,7 @@ ElementHandle: parameters: force: boolean? noWaitAfter: boolean? + position: Point? timeout: number? click: @@ -1753,6 +1756,7 @@ ElementHandle: parameters: force: boolean? noWaitAfter: boolean? + position: Point? timeout: number? waitForElementState: diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 081afa0584..b3193c11c9 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -539,6 +539,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { selector: tString, force: tOptional(tBoolean), noWaitAfter: tOptional(tBoolean), + position: tOptional(tType('Point')), timeout: tOptional(tNumber), }); scheme.FrameClickParams = tObject({ @@ -705,6 +706,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { selector: tString, force: tOptional(tBoolean), noWaitAfter: tOptional(tBoolean), + position: tOptional(tType('Point')), timeout: tOptional(tNumber), }); scheme.FrameWaitForFunctionParams = tObject({ @@ -767,6 +769,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { scheme.ElementHandleCheckParams = tObject({ force: tOptional(tBoolean), noWaitAfter: tOptional(tBoolean), + position: tOptional(tType('Point')), timeout: tOptional(tNumber), }); scheme.ElementHandleClickParams = tObject({ @@ -877,6 +880,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { scheme.ElementHandleUncheckParams = tObject({ force: tOptional(tBoolean), noWaitAfter: tOptional(tBoolean), + position: tOptional(tType('Point')), timeout: tOptional(tNumber), }); scheme.ElementHandleWaitForElementStateParams = tObject({ diff --git a/src/server/dom.ts b/src/server/dom.ts index 2c58b358f2..8f4be23ccd 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -608,7 +608,7 @@ export class ElementHandle extends js.JSHandle { }, 'input'); } - async check(metadata: CallMetadata, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + async check(metadata: CallMetadata, options: { position?: types.Point } & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._setChecked(progress, true, options); @@ -616,7 +616,7 @@ export class ElementHandle extends js.JSHandle { }, this._page._timeoutSettings.timeout(options)); } - async uncheck(metadata: CallMetadata, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + async uncheck(metadata: CallMetadata, options: { position?: types.Point } & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._setChecked(progress, false, options); @@ -624,7 +624,7 @@ export class ElementHandle extends js.JSHandle { }, this._page._timeoutSettings.timeout(options)); } - async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { + async _setChecked(progress: Progress, state: boolean, options: { position?: types.Point } & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { const isChecked = async () => { const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'checked'), {}); return throwRetargetableDOMError(throwFatalDOMError(result)); diff --git a/tests/page-check.spec.ts b/tests/page-check.spec.ts index 6d85173370..7f2a58a6a6 100644 --- a/tests/page-check.spec.ts +++ b/tests/page-check.spec.ts @@ -107,3 +107,15 @@ it('should check the box inside a button', async ({page}) => { expect(await page.isChecked('input')).toBe(true); expect(await (await page.$('input')).isChecked()).toBe(true); }); + +it('should check the label with position', async ({page, server}) => { + await page.setContent(` + + `); + const box = await (await page.$('text=Click me')).boundingBox(); + await page.check('text=Click me', { position: { x: box.width - 10, y: 2 } }); + expect(await page.$eval('input', input => input.checked)).toBe(true); +}); diff --git a/types/types.d.ts b/types/types.d.ts index 096326f515..e562c795bb 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1375,6 +1375,16 @@ export interface Page { */ noWaitAfter?: boolean; + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the @@ -2812,6 +2822,16 @@ export interface Page { */ noWaitAfter?: boolean; + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the @@ -3567,6 +3587,16 @@ export interface Frame { */ noWaitAfter?: boolean; + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the @@ -4420,6 +4450,16 @@ export interface Frame { */ noWaitAfter?: boolean; + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the @@ -5721,6 +5761,16 @@ export interface ElementHandle extends JSHandle { */ noWaitAfter?: boolean; + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the @@ -6405,6 +6455,16 @@ export interface ElementHandle extends JSHandle { */ noWaitAfter?: boolean; + /** + * A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the + * element. + */ + position?: { + x: number; + + y: number; + }; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the