chore: convert more actions to Progress (#2444)

This commit is contained in:
Dmitry Gozman 2020-06-03 11:23:24 -07:00 committed by GitHub
parent f188b0a174
commit 1accb5141d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 65 deletions

View File

@ -61,9 +61,9 @@ export class FrameExecutionContext extends js.ExecutionContext {
} }
async doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise<any> { async doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise<any> {
return await this.frame._page._frameManager.waitForSignalsCreatedBy(async () => { return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, !waitForNavigations, async () => {
return this._delegate.evaluate(this, returnByValue, pageFunction, ...args); return this._delegate.evaluate(this, returnByValue, pageFunction, ...args);
}, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { noWaitAfter: true }); });
} }
createHandle(remoteObject: js.RemoteObject): js.JSHandle { createHandle(remoteObject: js.RemoteObject): js.JSHandle {
@ -241,7 +241,6 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async _retryPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> { async _retryPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
progress.log(inputLog, progress.apiName);
while (!progress.isCanceled()) { while (!progress.isCanceled()) {
const result = await this._performPointerAction(progress, action, options); const result = await this._performPointerAction(progress, action, options);
if (result === 'done') if (result === 'done')
@ -291,7 +290,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
progress.log(inputLog, `...element does receive pointer events, continuing input action`); progress.log(inputLog, `...element does receive pointer events, continuing input action`);
} }
await this._page._frameManager.waitForSignalsCreatedBy(async () => { await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
let restoreModifiers: input.Modifier[] | undefined; let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers) if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
@ -301,7 +300,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
progress.log(inputLog, 'waiting for scheduled navigations to finish...'); progress.log(inputLog, 'waiting for scheduled navigations to finish...');
if (restoreModifiers) if (restoreModifiers)
await this._page.keyboard._ensureModifiers(restoreModifiers); await this._page.keyboard._ensureModifiers(restoreModifiers);
}, progress.deadline, options, true); }, 'input');
progress.log(inputLog, '...navigations have finished'); progress.log(inputLog, '...navigations have finished');
return 'done'; return 'done';
@ -331,9 +330,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._retryPointerAction(progress, point => this._page.mouse.dblclick(point.x, point.y, options), options); return this._retryPointerAction(progress, point => this._page.mouse.dblclick(point.x, point.y, options), options);
} }
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options?: types.NavigatingActionWaitOptions): Promise<string[]> { async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
this._page._log(inputLog, `elementHandle.selectOption(%s)`, values); return Progress.runCancelableTask(progress => this._selectOption(progress, values, options), options, this._page, this._page._timeoutSettings);
const deadline = this._page._timeoutSettings.computeDeadline(options); }
async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise<string[]> {
progress.log(inputLog, progress.apiName);
let vals: string[] | ElementHandle[] | types.SelectOption[]; let vals: string[] | ElementHandle[] | types.SelectOption[];
if (!Array.isArray(values)) if (!Array.isArray(values))
vals = [ values ] as (string[] | ElementHandle[] | types.SelectOption[]); vals = [ values ] as (string[] | ElementHandle[] | types.SelectOption[]);
@ -350,10 +352,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (option.index !== undefined) if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"'); assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
} }
return await this._page._frameManager.waitForSignalsCreatedBy<string[]>(async () => { return this._page._frameManager.waitForSignalsCreatedBy<string[]>(progress, options.noWaitAfter, async () => {
const injectedResult = await this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions); const injectedResult = await this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions);
return handleInjectedResult(injectedResult); return handleInjectedResult(injectedResult);
}, deadline, options); });
} }
async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> { async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
@ -363,7 +365,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<void> { async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<void> {
progress.log(inputLog, `elementHandle.fill("${value}")`); progress.log(inputLog, `elementHandle.fill("${value}")`);
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"'); assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
await this._page._frameManager.waitForSignalsCreatedBy(async () => { await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
const poll = await this._evaluateHandleInUtility(({ injected, node }, { value }) => { const poll = await this._evaluateHandleInUtility(({ injected, node }, { value }) => {
return injected.waitForEnabledAndFill(node, value); return injected.waitForEnabledAndFill(node, value);
}, { value }); }, { value });
@ -376,7 +378,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
else else
await this._page.keyboard.press('Delete'); await this._page.keyboard.press('Delete');
} }
}, progress.deadline, options, true); }, 'input');
} }
async selectText(): Promise<void> { async selectText(): Promise<void> {
@ -385,9 +387,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
handleInjectedResult(injectedResult); handleInjectedResult(injectedResult);
} }
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions) { async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) {
this._page._log(inputLog, `elementHandle.setInputFiles(...)`); return Progress.runCancelableTask(async progress => this._setInputFiles(progress, files, options), options, this._page, this._page._timeoutSettings);
const deadline = this._page._timeoutSettings.computeDeadline(options); }
async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions) {
progress.log(inputLog, progress.apiName);
const injectedResult = await this._evaluateInUtility(({ node }): types.InjectedScriptResult<boolean> => { const injectedResult = await this._evaluateInUtility(({ node }): types.InjectedScriptResult<boolean> => {
if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT') if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT')
return { status: 'error', error: 'Node is not an HTMLInputElement' }; return { status: 'error', error: 'Node is not an HTMLInputElement' };
@ -416,9 +421,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
filePayloads.push(item); filePayloads.push(item);
} }
} }
await this._page._frameManager.waitForSignalsCreatedBy(async () => { await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, filePayloads); await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, filePayloads);
}, deadline, options); });
} }
async focus() { async focus() {
@ -427,22 +432,28 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
handleInjectedResult(injectedResult); handleInjectedResult(injectedResult);
} }
async type(text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
this._page._log(inputLog, `elementHandle.type("${text}")`); return Progress.runCancelableTask(progress => this._type(progress, text, options), options, this._page, this._page._timeoutSettings);
const deadline = this._page._timeoutSettings.computeDeadline(options);
await this._page._frameManager.waitForSignalsCreatedBy(async () => {
await this.focus();
await this._page.keyboard.type(text, options);
}, deadline, options, true);
} }
async press(key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions) {
this._page._log(inputLog, `elementHandle.press("${key}")`); progress.log(inputLog, `elementHandle.type("${text}")`);
const deadline = this._page._timeoutSettings.computeDeadline(options); return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
await this._page._frameManager.waitForSignalsCreatedBy(async () => { await this.focus();
await this._page.keyboard.type(text, options);
}, 'input');
}
async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
return Progress.runCancelableTask(progress => this._press(progress, key, options), options, this._page, this._page._timeoutSettings);
}
async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions) {
progress.log(inputLog, `elementHandle.press("${key}")`);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
await this.focus(); await this.focus();
await this._page.keyboard.press(key, options); await this._page.keyboard.press(key, options);
}, deadline, options, true); }, 'input');
} }
async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {

View File

@ -105,23 +105,21 @@ export class FrameManager {
} }
} }
// TODO: take progress parameter. async waitForSignalsCreatedBy<T>(progress: Progress | null, noWaitAfter: boolean | undefined, action: () => Promise<T>, source?: 'input'): Promise<T> {
async waitForSignalsCreatedBy<T>(action: () => Promise<T>, deadline: number, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise<T> { if (noWaitAfter)
if (options.noWaitAfter)
return action(); return action();
const barrier = new SignalBarrier(options, deadline); const barrier = new SignalBarrier(progress);
this._signalBarriers.add(barrier); this._signalBarriers.add(barrier);
try { if (progress)
const result = await action(); progress.cleanupWhenCanceled(() => this._signalBarriers.delete(barrier));
if (input) const result = await action();
await this._page._delegate.inputActionEpilogue(); if (source === 'input')
await barrier.waitFor(); await this._page._delegate.inputActionEpilogue();
// Resolve in the next task, after all waitForNavigations. await barrier.waitFor();
await new Promise(helper.makeWaitForNextTask()); this._signalBarriers.delete(barrier);
return result; // Resolve in the next task, after all waitForNavigations.
} finally { await new Promise(helper.makeWaitForNextTask());
this._signalBarriers.delete(barrier); return result;
}
} }
frameWillPotentiallyRequestNavigation() { frameWillPotentiallyRequestNavigation() {
@ -732,8 +730,7 @@ export class Frame {
} }
async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) { async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._fill(progress, value, options));
(progress, handle) => handle._fill(progress, value, options));
} }
async focus(selector: string, options: types.TimeoutOptions = {}) { async focus(selector: string, options: types.TimeoutOptions = {}) {
@ -761,23 +758,19 @@ export class Frame {
} }
async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> { async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
return await this._retryWithSelectorIfNotConnected(selector, options, return this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._selectOption(progress, values, options));
(progress, handle) => handle.selectOption(values, helper.optionsWithUpdatedTimeout(options, progress.deadline)));
} }
async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> { async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
await this._retryWithSelectorIfNotConnected(selector, options, await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setInputFiles(progress, files, options));
(progress, handle) => handle.setInputFiles(files, helper.optionsWithUpdatedTimeout(options, progress.deadline)));
} }
async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._type(progress, text, options));
(progress, handle) => handle.type(text, helper.optionsWithUpdatedTimeout(options, progress.deadline)));
} }
async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._press(progress, key, options));
(progress, handle) => handle.press(key, helper.optionsWithUpdatedTimeout(options, progress.deadline)));
} }
async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
@ -937,15 +930,13 @@ class RerunnableTask<T> {
} }
export class SignalBarrier { export class SignalBarrier {
private _options: types.NavigatingActionWaitOptions; private _progress: Progress | null;
private _protectCount = 0; private _protectCount = 0;
private _promise: Promise<void>; private _promise: Promise<void>;
private _promiseCallback = () => {}; private _promiseCallback = () => {};
private _deadline: number;
constructor(options: types.NavigatingActionWaitOptions, deadline: number) { constructor(progress: Progress | null) {
this._options = options; this._progress = progress;
this._deadline = deadline;
this._promise = new Promise(f => this._promiseCallback = f); this._promise = new Promise(f => this._promiseCallback = f);
this.retain(); this.retain();
} }
@ -957,8 +948,8 @@ export class SignalBarrier {
async addFrameNavigation(frame: Frame) { async addFrameNavigation(frame: Frame) {
this.retain(); this.retain();
const options = helper.optionsWithUpdatedTimeout(this._options, this._deadline); const timeout = this._progress ? helper.timeUntilDeadline(this._progress.deadline) : undefined;
await frame._waitForNavigation({...options, waitUntil: 'commit'}).catch(e => {}); await frame._waitForNavigation({timeout, waitUntil: 'commit'}).catch(e => {});
this.release(); this.release();
} }

View File

@ -83,13 +83,13 @@ export class JSHandle<T = any> {
_disposed = false; _disposed = false;
readonly _objectId: string | undefined; readonly _objectId: string | undefined;
readonly _value: any; readonly _value: any;
private _type: string; private _objectType: string;
constructor(context: ExecutionContext, type: string, objectId?: string, value?: any) { constructor(context: ExecutionContext, type: string, objectId?: string, value?: any) {
this._context = context; this._context = context;
this._objectId = objectId; this._objectId = objectId;
this._value = value; this._value = value;
this._type = type; this._objectType = type;
} }
async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R>; async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R>;
@ -137,7 +137,7 @@ export class JSHandle<T = any> {
_handleToString(includeType: boolean): string { _handleToString(includeType: boolean): string {
if (this._objectId) if (this._objectId)
return 'JSHandle@' + this._type; return 'JSHandle@' + this._objectType;
return (includeType ? 'JSHandle:' : '') + this._value; return (includeType ? 'JSHandle:' : '') + this._value;
} }

View File

@ -416,3 +416,13 @@ describe('ElementHandle.check', () => {
expect(await page.evaluate(() => checkbox.checked)).toBe(false); expect(await page.evaluate(() => checkbox.checked)).toBe(false);
}); });
}); });
describe('ElementHandle.selectOption', function() {
it('should select single option', async({page, server}) => {
await page.goto(server.PREFIX + '/input/select.html');
const select = await page.$('select');
await select.selectOption('blue');
expect(await page.evaluate(() => result.onInput)).toEqual(['blue']);
expect(await page.evaluate(() => result.onChange)).toEqual(['blue']);
});
});

View File

@ -850,7 +850,7 @@ describe('Page.title', function() {
}); });
}); });
describe('Page.select', function() { describe('Page.selectOption', function() {
it('should select single option', async({page, server}) => { it('should select single option', async({page, server}) => {
await page.goto(server.PREFIX + '/input/select.html'); await page.goto(server.PREFIX + '/input/select.html');
await page.selectOption('select', 'blue'); await page.selectOption('select', 'blue');