feat(force): add fill, selectOption, selectText ({force}) (#7286)

This commit is contained in:
Pavel Feldman 2021-06-24 08:18:09 -07:00 committed by GitHub
parent ba3f0ff6f5
commit e6bf0a07fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 98 additions and 41 deletions

View File

@ -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
@ -717,9 +717,8 @@ 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
@ -727,6 +726,7 @@ await handle.SelectOptionAsync(new[] {
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

View File

@ -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
@ -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

View File

@ -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
@ -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

View File

@ -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[],

View File

@ -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,
};
@ -1681,6 +1683,7 @@ export type FrameSelectOptionParams = {
label?: string,
index?: number,
}[],
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
@ -1691,6 +1694,7 @@ export type FrameSelectOptionOptions = {
label?: string,
index?: number,
}[],
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
@ -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,
};
@ -2196,6 +2202,7 @@ export type ElementHandleSelectOptionParams = {
label?: string,
index?: number,
}[],
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
@ -2206,6 +2213,7 @@ export type ElementHandleSelectOptionOptions = {
label?: string,
index?: number,
}[],
force?: boolean,
timeout?: number,
noWaitAfter?: boolean,
};
@ -2213,9 +2221,11 @@ export type ElementHandleSelectOptionResult = {
values: string[],
};
export type ElementHandleSelectTextParams = {
force?: boolean,
timeout?: number,
};
export type ElementHandleSelectTextOptions = {
force?: boolean,
timeout?: number,
};
export type ElementHandleSelectTextResult = void;

View File

@ -1178,6 +1178,7 @@ Frame:
parameters:
selector: string
value: string
force: boolean?
timeout: number?
noWaitAfter: boolean?
@ -1328,6 +1329,7 @@ Frame:
value: string?
label: string?
index: number?
force: boolean?
timeout: number?
noWaitAfter: boolean?
returns:
@ -1641,6 +1643,7 @@ ElementHandle:
fill:
parameters:
value: string
force: boolean?
timeout: number?
noWaitAfter: boolean?
@ -1759,6 +1762,7 @@ ElementHandle:
value: string?
label: string?
index: number?
force: boolean?
timeout: number?
noWaitAfter: boolean?
returns:
@ -1768,6 +1772,7 @@ ElementHandle:
selectText:
parameters:
force: boolean?
timeout: number?
setInputFiles:

View File

@ -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),
});
@ -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),
});
@ -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),
});
@ -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({

View File

@ -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());
@ -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();
@ -469,7 +467,7 @@ 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);
@ -477,15 +475,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, 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();
@ -493,7 +491,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
});
}
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);
@ -501,14 +499,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, 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.
@ -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));
@ -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())));
@ -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)

View File

@ -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)));
@ -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));

View File

@ -344,7 +344,7 @@ 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;
@ -352,6 +352,11 @@ export class InjectedScript {
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);

View File

@ -33,8 +33,11 @@ export type NavigatingActionWaitOptions = TimeoutOptions & {
noWaitAfter?: boolean,
};
export type PointerActionWaitOptions = TimeoutOptions & {
export type ForceOptions = {
force?: boolean,
};
export type PointerActionWaitOptions = TimeoutOptions & ForceOptions & {
trial?: boolean;
};

35
types/types.d.ts vendored
View File

@ -1756,6 +1756,11 @@ export interface Page {
* @param options
*/
fill(selector: string, value: string, options?: {
/**
* Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`.
*/
force?: boolean;
/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
@ -2508,6 +2513,11 @@ export interface Page {
*/
index?: number;
}>, options?: {
/**
* Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`.
*/
force?: boolean;
/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
@ -3870,6 +3880,11 @@ export interface Frame {
* @param options
*/
fill(selector: string, value: string, options?: {
/**
* Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`.
*/
force?: boolean;
/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
@ -4299,6 +4314,11 @@ export interface Frame {
*/
index?: number;
}>, options?: {
/**
* Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`.
*/
force?: boolean;
/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
@ -6313,6 +6333,11 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
* @param options
*/
fill(value: string, options?: {
/**
* Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`.
*/
force?: boolean;
/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
@ -6612,6 +6637,11 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
*/
index?: number;
}>, options?: {
/**
* Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`.
*/
force?: boolean;
/**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can
* opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to
@ -6634,6 +6664,11 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
* @param options
*/
selectText(options?: {
/**
* Whether to bypass the [actionability](https://playwright.dev/docs/actionability) checks. Defaults to `false`.
*/
force?: boolean;
/**
* Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by
* using the