Skip to content

Commit

Permalink
api: add option position to check/uncheck (#6153)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dgozman committed Apr 12, 2021
1 parent 96cee43 commit e81a3c5
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 3 deletions.
4 changes: 4 additions & 0 deletions docs/src/api/class-elementhandle.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/src/api/class-frame.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/src/api/class-page.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/protocol/channels.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/protocol/protocol.yml
Expand Up @@ -1045,6 +1045,7 @@ Frame:
selector: string
force: boolean?
noWaitAfter: boolean?
position: Point?
timeout: number?

click:
Expand Down Expand Up @@ -1352,6 +1353,7 @@ Frame:
selector: string
force: boolean?
noWaitAfter: boolean?
position: Point?
timeout: number?

waitForFunction:
Expand Down Expand Up @@ -1524,6 +1526,7 @@ ElementHandle:
parameters:
force: boolean?
noWaitAfter: boolean?
position: Point?
timeout: number?

click:
Expand Down Expand Up @@ -1753,6 +1756,7 @@ ElementHandle:
parameters:
force: boolean?
noWaitAfter: boolean?
position: Point?
timeout: number?

waitForElementState:
Expand Down
4 changes: 4 additions & 0 deletions src/protocol/validator.ts
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
6 changes: 3 additions & 3 deletions src/server/dom.ts
Expand Up @@ -608,23 +608,23 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, '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);
return assertDone(throwRetargetableDOMError(result));
}, 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);
return assertDone(throwRetargetableDOMError(result));
}, 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));
Expand Down
12 changes: 12 additions & 0 deletions tests/page-check.spec.ts
Expand Up @@ -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(`
<input id='checkbox' type='checkbox' style='width: 5px; height: 5px;'>
<label for='checkbox'>
<a href=${JSON.stringify(server.EMPTY_PAGE)}>I am a long link that goes away so that nothing good will happen if you click on me</a>
Click me
</label>`);
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);
});
60 changes: 60 additions & 0 deletions types/types.d.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -5721,6 +5761,16 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
*/
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
Expand Down Expand Up @@ -6405,6 +6455,16 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
*/
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
Expand Down

0 comments on commit e81a3c5

Please sign in to comment.