Skip to content

Commit

Permalink
feat(force): add fill, selectOption, selectText ({force}) (#7286)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Jun 24, 2021
1 parent ba3f0ff commit e6bf0a0
Show file tree
Hide file tree
Showing 12 changed files with 98 additions and 41 deletions.
6 changes: 3 additions & 3 deletions docs/src/api/class-elementhandle.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,8 @@ To send fine-grained keyboard events, use [`method: ElementHandle.type`].

Value to set for the `<input>`, `<textarea>` or `[contenteditable]` element.

### option: ElementHandle.fill.force = %%-input-force-%%
### option: ElementHandle.fill.noWaitAfter = %%-input-no-wait-after-%%

### option: ElementHandle.fill.timeout = %%-input-timeout-%%

## async method: ElementHandle.focus
Expand Down Expand Up @@ -717,16 +717,16 @@ await handle.SelectOptionAsync(new[] {
```

### param: ElementHandle.selectOption.values = %%-select-options-values-%%

### option: ElementHandle.selectOption.force = %%-input-force-%%
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-%%

### option: ElementHandle.selectOption.timeout = %%-input-timeout-%%

## async method: ElementHandle.selectText

This method waits for [actionability](./actionability.md) checks, then focuses the element and selects all its text
content.

### option: ElementHandle.selectText.force = %%-input-force-%%
### option: ElementHandle.selectText.timeout = %%-input-timeout-%%

## async method: ElementHandle.setInputFiles
Expand Down
6 changes: 2 additions & 4 deletions docs/src/api/class-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,8 +699,8 @@ To send fine-grained keyboard events, use [`method: Frame.type`].

Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.

### option: Frame.fill.force = %%-input-force-%%
### option: Frame.fill.noWaitAfter = %%-input-no-wait-after-%%

### option: Frame.fill.timeout = %%-input-timeout-%%

## async method: Frame.focus
Expand Down Expand Up @@ -1065,11 +1065,9 @@ await frame.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" })
```

### param: Frame.selectOption.selector = %%-query-selector-%%

### param: Frame.selectOption.values = %%-select-options-values-%%

### option: Frame.selectOption.force = %%-input-force-%%
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-%%

### option: Frame.selectOption.timeout = %%-input-timeout-%%

## async method: Frame.setContent
Expand Down
6 changes: 2 additions & 4 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -1746,8 +1746,8 @@ Shortcut for main frame's [`method: Frame.fill`].

Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.

### option: Page.fill.force = %%-input-force-%%
### option: Page.fill.noWaitAfter = %%-input-no-wait-after-%%

### option: Page.fill.timeout = %%-input-timeout-%%

## async method: Page.focus
Expand Down Expand Up @@ -2631,11 +2631,9 @@ await page.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" });
Shortcut for main frame's [`method: Frame.selectOption`].

### param: Page.selectOption.selector = %%-input-selector-%%

### param: Page.selectOption.values = %%-select-options-values-%%

### option: Page.selectOption.force = %%-input-force-%%
### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-%%

### option: Page.selectOption.timeout = %%-input-timeout-%%

## async method: Page.setContent
Expand Down
2 changes: 1 addition & 1 deletion src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type WaitForEventOptions = Function | { predicate?: Function, timeout?: n
export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number };

export type SelectOption = { value?: string, label?: string, index?: number };
export type SelectOptionOptions = { timeout?: number, noWaitAfter?: boolean };
export type SelectOptionOptions = { force?: boolean, timeout?: number, noWaitAfter?: boolean };
export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
export type StorageState = {
cookies: channels.NetworkCookie[],
Expand Down
10 changes: 10 additions & 0 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1490,10 +1490,12 @@ export type FrameEvaluateExpressionHandleResult = {
export type FrameFillParams = {
selector: string,
value: string,
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
export type FrameFillOptions = {
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
Expand Down Expand Up @@ -1681,6 +1683,7 @@ export type FrameSelectOptionParams = {
label?: string,
index?: number,
}[],
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
Expand All @@ -1691,6 +1694,7 @@ export type FrameSelectOptionOptions = {
label?: string,
index?: number,
}[],
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
Expand Down Expand Up @@ -2052,10 +2056,12 @@ export type ElementHandleDispatchEventOptions = {
export type ElementHandleDispatchEventResult = void;
export type ElementHandleFillParams = {
value: string,
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
export type ElementHandleFillOptions = {
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
Expand Down Expand Up @@ -2196,6 +2202,7 @@ export type ElementHandleSelectOptionParams = {
label?: string,
index?: number,
}[],
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
Expand All @@ -2206,16 +2213,19 @@ export type ElementHandleSelectOptionOptions = {
label?: string,
index?: number,
}[],
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
export type ElementHandleSelectOptionResult = {
values: string[],
};
export type ElementHandleSelectTextParams = {
force?: boolean,
timeout?: number,
};
export type ElementHandleSelectTextOptions = {
force?: boolean,
timeout?: number,
};
export type ElementHandleSelectTextResult = void;
Expand Down
5 changes: 5 additions & 0 deletions src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,7 @@ Frame:
parameters:
selector: string
value: string
force: boolean?
timeout: number?
noWaitAfter: boolean?

Expand Down Expand Up @@ -1328,6 +1329,7 @@ Frame:
value: string?
label: string?
index: number?
force: boolean?
timeout: number?
noWaitAfter: boolean?
returns:
Expand Down Expand Up @@ -1641,6 +1643,7 @@ ElementHandle:
fill:
parameters:
value: string
force: boolean?
timeout: number?
noWaitAfter: boolean?

Expand Down Expand Up @@ -1759,6 +1762,7 @@ ElementHandle:
value: string?
label: string?
index: number?
force: boolean?
timeout: number?
noWaitAfter: boolean?
returns:
Expand All @@ -1768,6 +1772,7 @@ ElementHandle:

selectText:
parameters:
force: boolean?
timeout: number?

setInputFiles:
Expand Down
5 changes: 5 additions & 0 deletions src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.FrameFillParams = tObject({
selector: tString,
value: tString,
force: tOptional(tBoolean),
timeout: tOptional(tNumber),
noWaitAfter: tOptional(tBoolean),
});
Expand Down Expand Up @@ -692,6 +693,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
label: tOptional(tString),
index: tOptional(tNumber),
}))),
force: tOptional(tBoolean),
timeout: tOptional(tNumber),
noWaitAfter: tOptional(tBoolean),
});
Expand Down Expand Up @@ -831,6 +833,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
});
scheme.ElementHandleFillParams = tObject({
value: tString,
force: tOptional(tBoolean),
timeout: tOptional(tNumber),
noWaitAfter: tOptional(tBoolean),
});
Expand Down Expand Up @@ -883,10 +886,12 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
label: tOptional(tString),
index: tOptional(tNumber),
}))),
force: tOptional(tBoolean),
timeout: tOptional(tNumber),
noWaitAfter: tOptional(tBoolean),
});
scheme.ElementHandleSelectTextParams = tObject({
force: tOptional(tBoolean),
timeout: tOptional(tNumber),
});
scheme.ElementHandleSetInputFilesParams = tObject({
Expand Down
48 changes: 23 additions & 25 deletions src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {

async _waitAndScrollIntoViewIfNeeded(progress: Progress): Promise<void> {
while (progress.isRunning()) {
assertDone(throwRetargetableDOMError(await this._waitForDisplayedAtStablePosition(progress, false /* waitForEnabled */)));
assertDone(throwRetargetableDOMError(await this._waitForDisplayedAtStablePosition(progress, false /* force */, false /* waitForEnabled */)));

progress.throwIfAborted(); // Avoid action that has side-effects.
const result = throwRetargetableDOMError(await this._scrollRectIntoViewIfNeeded());
Expand Down Expand Up @@ -356,11 +356,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const { force = false, position } = options;
if ((options as any).__testHookBeforeStable)
await (options as any).__testHookBeforeStable();
if (!force) {
const result = await this._waitForDisplayedAtStablePosition(progress, waitForEnabled);
if (result !== 'done')
return result;
}
const result = await this._waitForDisplayedAtStablePosition(progress, force, waitForEnabled);
if (result !== 'done')
return result;
if ((options as any).__testHookAfterStable)
await (options as any).__testHookAfterStable();

Expand Down Expand Up @@ -469,46 +467,46 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), options);
}

async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise<string[]> {
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions & types.ForceOptions): Promise<string[]> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._selectOption(progress, elements, values, options);
return throwRetargetableDOMError(result);
}, this._page._timeoutSettings.timeout(options));
}

async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise<string[] | 'error:notconnected'> {
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions & types.ForceOptions): Promise<string[] | 'error:notconnected'> {
const optionsToSelect = [...elements, ...values];
await progress.beforeInputAction(this);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects.
progress.log(' selecting specified option(s)');
const poll = await this.evaluateHandleInUtility(([injected, node, optionsToSelect]) => {
return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled'], injected.selectOptions.bind(injected, optionsToSelect));
}, optionsToSelect);
const poll = await this.evaluateHandleInUtility(([injected, node, { optionsToSelect, force }]) => {
return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled'], force, injected.selectOptions.bind(injected, optionsToSelect));
}, { optionsToSelect, force: options.force });
const pollHandler = new InjectedScriptPollHandler(progress, poll);
const result = throwFatalDOMError(await pollHandler.finish());
await this._page._doSlowMo();
return result;
});
}

async fill(metadata: CallMetadata, value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
async fill(metadata: CallMetadata, value: string, options: types.NavigatingActionWaitOptions & types.ForceOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
const result = await this._fill(progress, value, options);
assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options));
}

async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions & types.ForceOptions): Promise<'error:notconnected' | 'done'> {
progress.log(`elementHandle.fill("${value}")`);
await progress.beforeInputAction(this);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.log(' waiting for element to be visible, enabled and editable');
const poll = await this.evaluateHandleInUtility(([injected, node, value]) => {
return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled', 'editable'], injected.fill.bind(injected, value));
}, value);
const poll = await this.evaluateHandleInUtility(([injected, node, { value, force }]) => {
return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled', 'editable'], force, injected.fill.bind(injected, value));
}, { value, force: options.force });
const pollHandler = new InjectedScriptPollHandler(progress, poll);
const filled = throwFatalDOMError(await pollHandler.finish());
progress.throwIfAborted(); // Avoid action that has side-effects.
Expand All @@ -528,13 +526,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, 'input');
}

async selectText(metadata: CallMetadata, options: types.TimeoutOptions = {}): Promise<void> {
async selectText(metadata: CallMetadata, options: types.TimeoutOptions & types.ForceOptions = {}): Promise<void> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.throwIfAborted(); // Avoid action that has side-effects.
const poll = await this.evaluateHandleInUtility(([injected, node]) => {
return injected.waitForElementStatesAndPerformAction(node, ['visible'], injected.selectText.bind(injected));
}, {});
const poll = await this.evaluateHandleInUtility(([injected, node, force]) => {
return injected.waitForElementStatesAndPerformAction(node, ['visible'], force, injected.selectText.bind(injected));
}, options.force);
const pollHandler = new InjectedScriptPollHandler(progress, poll);
const result = throwFatalDOMError(await pollHandler.finish());
assertDone(throwRetargetableDOMError(result));
Expand Down Expand Up @@ -732,7 +730,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return controller.run(async progress => {
progress.log(` waiting for element to be ${state}`);
const poll = await this.evaluateHandleInUtility(([injected, node, state]) => {
return injected.waitForElementStatesAndPerformAction(node, [state], () => 'done' as const);
return injected.waitForElementStatesAndPerformAction(node, [state], false, () => 'done' as const);
}, state);
const pollHandler = new InjectedScriptPollHandler(progress, poll);
assertDone(throwRetargetableDOMError(throwFatalDOMError(await pollHandler.finish())));
Expand Down Expand Up @@ -770,15 +768,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this;
}

async _waitForDisplayedAtStablePosition(progress: Progress, waitForEnabled: boolean): Promise<'error:notconnected' | 'done'> {
async _waitForDisplayedAtStablePosition(progress: Progress, force: boolean, waitForEnabled: boolean): Promise<'error:notconnected' | 'done'> {
if (waitForEnabled)
progress.log(` waiting for element to be visible, enabled and stable`);
else
progress.log(` waiting for element to be visible and stable`);
const poll = this.evaluateHandleInUtility(([injected, node, waitForEnabled]) => {
const poll = this.evaluateHandleInUtility(([injected, node, { waitForEnabled, force }]) => {
return injected.waitForElementStatesAndPerformAction(node,
waitForEnabled ? ['visible', 'stable', 'enabled'] : ['visible', 'stable'], () => 'done' as const);
}, waitForEnabled);
waitForEnabled ? ['visible', 'stable', 'enabled'] : ['visible', 'stable'], force, () => 'done' as const);
}, { waitForEnabled, force });
const pollHandler = new InjectedScriptPollHandler(progress, await poll);
const result = await pollHandler.finish();
if (waitForEnabled)
Expand Down
4 changes: 2 additions & 2 deletions src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,7 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options));
}

async fill(metadata: CallMetadata, selector: string, value: string, options: types.NavigatingActionWaitOptions) {
async fill(metadata: CallMetadata, selector: string, value: string, options: types.NavigatingActionWaitOptions & { force?: boolean }) {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._fill(progress, value, options)));
Expand Down Expand Up @@ -1097,7 +1097,7 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options));
}

async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions & types.ForceOptions = {}): Promise<string[]> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return await this._retryWithProgressIfNotConnected(progress, selector, handle => handle._selectOption(progress, elements, values, options));
Expand Down
7 changes: 6 additions & 1 deletion src/server/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,19 @@ export class InjectedScript {
return element;
}

waitForElementStatesAndPerformAction<T>(node: Node, states: ElementState[],
waitForElementStatesAndPerformAction<T>(node: Node, states: ElementState[], force: boolean | undefined,
callback: (node: Node, progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol): InjectedScriptPoll<T | 'error:notconnected' | FatalDOMError> {
let lastRect: { x: number, y: number, width: number, height: number } | undefined;
let counter = 0;
let samePositionCounter = 0;
let lastTime = 0;

const predicate = (progress: InjectedScriptProgress, continuePolling: symbol) => {
if (force) {
progress.log(` forcing action`);
return callback(node, progress, continuePolling);
}

for (const state of states) {
if (state !== 'stable') {
const result = this.checkElementState(node, state);
Expand Down

0 comments on commit e6bf0a0

Please sign in to comment.