chore: run most actions through page._runAbortableTask (#2721)

This introduces a single place for future snapshots.
This commit is contained in:
Dmitry Gozman 2020-06-25 16:57:21 -07:00 committed by GitHub
parent bab6833232
commit ab6a6c9b82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 80 additions and 90 deletions

View File

@ -27,7 +27,7 @@ import * as js from './javascript';
import { Page } from './page'; import { Page } from './page';
import { selectors } from './selectors'; import { selectors } from './selectors';
import * as types from './types'; import * as types from './types';
import { Progress, ProgressController } from './progress'; import { Progress } from './progress';
import DebugScript from './debug/injected/debugScript'; import DebugScript from './debug/injected/debugScript';
import { FatalDOMError, RetargetableDOMError } from './common/domErrors'; import { FatalDOMError, RetargetableDOMError } from './common/domErrors';
@ -122,11 +122,6 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
this._initializePreview().catch(e => {}); this._initializePreview().catch(e => {});
} }
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
const controller = new ProgressController(this._page._logger, timeout, `elementHandle.${apiName}`);
return controller.run(task);
}
async _initializePreview() { async _initializePreview() {
const utility = await this._context.injectedScript(); const utility = await this._context.injectedScript();
this._preview = await utility.evaluate((injected, e) => 'JSHandle@' + injected.previewNode(e), this); this._preview = await utility.evaluate((injected, e) => 'JSHandle@' + injected.previewNode(e), this);
@ -229,9 +224,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async scrollIntoViewIfNeeded(options: types.TimeoutOptions = {}) { async scrollIntoViewIfNeeded(options: types.TimeoutOptions = {}) {
return this._runAbortableTask( return this._page._runAbortableTask(
progress => this._waitAndScrollIntoViewIfNeeded(progress), progress => this._waitAndScrollIntoViewIfNeeded(progress),
this._page._timeoutSettings.timeout(options), 'scrollIntoViewIfNeeded'); this._page._timeoutSettings.timeout(options), 'elementHandle.scrollIntoViewIfNeeded');
} }
private async _waitForVisible(progress: Progress): Promise<'error:notconnected' | 'done'> { private async _waitForVisible(progress: Progress): Promise<'error:notconnected' | 'done'> {
@ -378,10 +373,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
hover(options: types.PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> { hover(options: types.PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._hover(progress, options); const result = await this._hover(progress, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'hover'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.hover');
} }
_hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> { _hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<'error:notconnected' | 'done'> {
@ -389,10 +384,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
click(options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> { click(options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._click(progress, options); const result = await this._click(progress, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'click'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.click');
} }
_click(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { _click(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
@ -400,10 +395,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
dblclick(options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> { dblclick(options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._dblclick(progress, options); const result = await this._dblclick(progress, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'dblclick'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.dblclick');
} }
_dblclick(progress: Progress, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { _dblclick(progress: Progress, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
@ -411,10 +406,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> { async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._selectOption(progress, values, options); const result = await this._selectOption(progress, values, options);
return throwRetargetableDOMError(result); return throwRetargetableDOMError(result);
}, this._page._timeoutSettings.timeout(options), 'selectOption'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.selectOption');
} }
async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions): Promise<string[] | 'error:notconnected'> { async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions): Promise<string[] | 'error:notconnected'> {
@ -444,10 +439,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> { async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._fill(progress, value, options); const result = await this._fill(progress, value, options);
assertDone(throwRetargetableDOMError(result)); assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'fill'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.fill');
} }
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
@ -478,7 +473,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async selectText(options: types.TimeoutOptions = {}): Promise<void> { async selectText(options: types.TimeoutOptions = {}): Promise<void> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
const poll = await this._evaluateHandleInUtility(([injected, node]) => { const poll = await this._evaluateHandleInUtility(([injected, node]) => {
return injected.waitForVisibleAndSelectText(node); return injected.waitForVisibleAndSelectText(node);
@ -486,14 +481,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const pollHandler = new InjectedScriptPollHandler(progress, poll); const pollHandler = new InjectedScriptPollHandler(progress, poll);
const result = throwFatalDOMError(await pollHandler.finish()); const result = throwFatalDOMError(await pollHandler.finish());
assertDone(throwRetargetableDOMError(result)); assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'selectText'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.selectText');
} }
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) { async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._setInputFiles(progress, files, options); const result = await this._setInputFiles(progress, files, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'setInputFiles'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.setInputFiles');
} }
async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
@ -534,10 +529,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async focus(): Promise<void> { async focus(): Promise<void> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._focus(progress); const result = await this._focus(progress);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, 0, 'focus'); }, 0, 'elementHandle.focus');
} }
async _focus(progress: Progress): Promise<'error:notconnected' | 'done'> { async _focus(progress: Progress): Promise<'error:notconnected' | 'done'> {
@ -547,10 +542,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> { async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._type(progress, text, options); const result = await this._type(progress, text, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'type'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.type');
} }
async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
@ -566,10 +561,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> { async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._press(progress, key, options); const result = await this._press(progress, key, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'press'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.press');
} }
async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
@ -585,17 +580,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._setChecked(progress, true, options); const result = await this._setChecked(progress, true, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'check'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.check');
} }
async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
const result = await this._setChecked(progress, false, options); const result = await this._setChecked(progress, false, options);
return assertDone(throwRetargetableDOMError(result)); return assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options), 'uncheck'); }, this._page._timeoutSettings.timeout(options), 'elementHandle.uncheck');
} }
async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
@ -614,9 +609,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async screenshot(options: types.ElementScreenshotOptions = {}): Promise<Buffer> { async screenshot(options: types.ElementScreenshotOptions = {}): Promise<Buffer> {
return this._runAbortableTask( return this._page._runAbortableTask(
progress => this._page._screenshotter.screenshotElement(progress, this, options), progress => this._page._screenshotter.screenshotElement(progress, this, options),
this._page._timeoutSettings.timeout(options), 'screenshot'); this._page._timeoutSettings.timeout(options), 'elementHandle.screenshot');
} }
async $(selector: string): Promise<ElementHandle | null> { async $(selector: string): Promise<ElementHandle | null> {

View File

@ -334,20 +334,13 @@ export class Frame {
this._parentFrame._childFrames.add(this); this._parentFrame._childFrames.add(this);
} }
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
const controller = new ProgressController(this._page._logger, timeout, this._apiName(apiName));
return controller.run(task);
}
private _apiName(method: string) { private _apiName(method: string) {
const subject = this._page._callingPageAPI ? 'page' : 'frame'; const subject = this._page._callingPageAPI ? 'page' : 'frame';
return `${subject}.${method}`; return `${subject}.${method}`;
} }
async goto(url: string, options: types.GotoOptions = {}): Promise<network.Response | null> { async goto(url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('goto')); return runNavigationTask(this, options, this._apiName('goto'), async progress => {
abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => {
progress.logger.info(`navigating to "${url}", waiting until "${options.waitUntil || 'load'}"`); progress.logger.info(`navigating to "${url}", waiting until "${options.waitUntil || 'load'}"`);
const headers = (this._page._state.extraHTTPHeaders || {}); const headers = (this._page._state.extraHTTPHeaders || {});
let referer = headers['referer'] || headers['Referer']; let referer = headers['referer'] || headers['Referer'];
@ -380,9 +373,7 @@ export class Frame {
} }
async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> { async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('waitForNavigation')); return runNavigationTask(this, options, this._apiName('waitForNavigation'), async progress => {
abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => {
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
progress.logger.info(`waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`); progress.logger.info(`waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`);
const frameTask = new FrameTask(this, progress); const frameTask = new FrameTask(this, progress);
@ -399,9 +390,7 @@ export class Frame {
} }
async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> { async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('waitForLoadState')); return runNavigationTask(this, options, this._apiName('waitForLoadState'), progress => this._waitForLoadState(progress, state));
abortProgressOnFrameDetach(progressController, this);
return progressController.run(progress => this._waitForLoadState(progress, state));
} }
async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> { async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
@ -468,7 +457,7 @@ export class Frame {
throw new Error(`Unsupported state option "${state}"`); throw new Error(`Unsupported state option "${state}"`);
const info = selectors._parseSelector(selector); const info = selectors._parseSelector(selector);
const task = selectors._waitForSelectorTask(info, state); const task = selectors._waitForSelectorTask(info, state);
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`); progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
const result = await this._scheduleRerunnableTask(progress, info.world, task); const result = await this._scheduleRerunnableTask(progress, info.world, task);
if (!result.asElement()) { if (!result.asElement()) {
@ -483,17 +472,17 @@ export class Frame {
return adopted; return adopted;
} }
return handle; return handle;
}, this._page._timeoutSettings.timeout(options), 'waitForSelector'); }, this._page._timeoutSettings.timeout(options), this._apiName('waitForSelector'));
} }
async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> { async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
const info = selectors._parseSelector(selector); const info = selectors._parseSelector(selector);
const task = selectors._dispatchEventTask(info, type, eventInit || {}); const task = selectors._dispatchEventTask(info, type, eventInit || {});
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
progress.logger.info(`Dispatching "${type}" event on selector "${selector}"...`); progress.logger.info(`Dispatching "${type}" event on selector "${selector}"...`);
const result = await this._scheduleRerunnableTask(progress, 'main', task); const result = await this._scheduleRerunnableTask(progress, 'main', task);
result.dispose(); result.dispose();
}, this._page._timeoutSettings.timeout(options), 'dispatchEvent'); }, this._page._timeoutSettings.timeout(options), this._apiName('dispatchEvent'));
} }
async $eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>; async $eval<R, Arg>(selector: string, pageFunction: js.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
@ -543,9 +532,7 @@ export class Frame {
} }
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> { async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('setContent')); return runNavigationTask(this, options, this._apiName('setContent'), async progress => {
abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil; const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.logger.info(`setting frame content, waiting until "${waitUntil}"`); progress.logger.info(`setting frame content, waiting until "${waitUntil}"`);
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`; const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
@ -733,7 +720,7 @@ export class Frame {
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>, action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>,
apiName: string): Promise<R> { apiName: string): Promise<R> {
const info = selectors._parseSelector(selector); const info = selectors._parseSelector(selector);
return this._runAbortableTask(async progress => { return this._page._runAbortableTask(async progress => {
while (progress.isRunning()) { while (progress.isRunning()) {
progress.logger.info(`waiting for selector "${selector}"`); progress.logger.info(`waiting for selector "${selector}"`);
const task = selectors._waitForSelectorTask(info, 'attached'); const task = selectors._waitForSelectorTask(info, 'attached');
@ -753,63 +740,63 @@ export class Frame {
} }
async click(selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async click(selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._click(progress, options), 'click'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._click(progress, options), this._apiName('click'));
} }
async dblclick(selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async dblclick(selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._dblclick(progress, options), 'dblclick'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._dblclick(progress, options), this._apiName('dblclick'));
} }
async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) { async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._fill(progress, value, options), 'fill'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._fill(progress, value, options), this._apiName('fill'));
} }
async focus(selector: string, options: types.TimeoutOptions = {}) { async focus(selector: string, options: types.TimeoutOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress), 'focus'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress), this._apiName('focus'));
} }
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<null|string> { async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<null|string> {
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.textContent(), 'textContent'); return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.textContent(), this._apiName('textContent'));
} }
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> { async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerText(), 'innerText'); return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerText(), this._apiName('innerText'));
} }
async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> { async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerHTML(), 'innerHTML'); return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerHTML(), this._apiName('innerHTML'));
} }
async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> { async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.getAttribute(name), 'getAttribute'); return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.getAttribute(name), this._apiName('getAttribute'));
} }
async hover(selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { async hover(selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._hover(progress, options), 'hover'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._hover(progress, options), this._apiName('hover'));
} }
async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> { async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
return this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._selectOption(progress, values, options), 'selectOption'); return this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._selectOption(progress, values, options), this._apiName('selectOption'));
} }
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, (progress, handle) => handle._setInputFiles(progress, files, options), 'setInputFiles'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setInputFiles(progress, files, options), this._apiName('setInputFiles'));
} }
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, (progress, handle) => handle._type(progress, text, options), 'type'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._type(progress, text, options), this._apiName('type'));
} }
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, (progress, handle) => handle._press(progress, key, options), 'press'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._press(progress, key, options), this._apiName('press'));
} }
async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, true, options), 'check'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, true, options), this._apiName('check'));
} }
async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, false, options), 'uncheck'); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, false, options), this._apiName('uncheck'));
} }
async waitForTimeout(timeout: number) { async waitForTimeout(timeout: number) {
@ -841,9 +828,9 @@ export class Frame {
return injectedScript.pollInterval(polling, (progress, continuePolling) => innerPredicate(arg) || continuePolling); return injectedScript.pollInterval(polling, (progress, continuePolling) => innerPredicate(arg) || continuePolling);
}, { injectedScript, predicateBody, polling, arg }); }, { injectedScript, predicateBody, polling, arg });
}; };
return this._runAbortableTask( return this._page._runAbortableTask(
progress => this._scheduleRerunnableTask(progress, 'main', task), progress => this._scheduleRerunnableTask(progress, 'main', task),
this._page._timeoutSettings.timeout(options), 'waitForFunction'); this._page._timeoutSettings.timeout(options), this._apiName('waitForFunction'));
} }
async title(): Promise<string> { async title(): Promise<string> {
@ -866,6 +853,8 @@ export class Frame {
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: SchedulableTask<T>): Promise<js.SmartHandle<T>> { private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: SchedulableTask<T>): Promise<js.SmartHandle<T>> {
const data = this._contextData.get(world)!; const data = this._contextData.get(world)!;
const rerunnableTask = new RerunnableTask(data, progress, task); const rerunnableTask = new RerunnableTask(data, progress, task);
if (this._detached)
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
if (data.context) if (data.context)
rerunnableTask.rerun(data.context); rerunnableTask.rerun(data.context);
return rerunnableTask.promise; return rerunnableTask.promise;
@ -1114,8 +1103,11 @@ class FrameTask {
} }
} }
function abortProgressOnFrameDetach(controller: ProgressController, frame: Frame) { async function runNavigationTask<T>(frame: Frame, options: types.TimeoutOptions, apiName: string, task: (progress: Progress) => Promise<T>): Promise<T> {
frame._page._disconnectedPromise.then(() => controller.abort(new Error('Navigation failed because page was closed!'))); const page = frame._page;
frame._page._crashedPromise.then(() => controller.abort(new Error('Navigation failed because page crashed!'))); const controller = new ProgressController(page._logger, page._timeoutSettings.navigationTimeout(options), apiName);
page._disconnectedPromise.then(() => controller.abort(new Error('Navigation failed because page was closed!')));
page._crashedPromise.then(() => controller.abort(new Error('Navigation failed because page crashed!')));
frame._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!'))); frame._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!')));
return controller.run(task);
} }

View File

@ -285,7 +285,7 @@ export class Response {
return this._statusText; return this._statusText;
} }
headers(): object { headers(): types.Headers {
return { ...this._headers }; return { ...this._headers };
} }

View File

@ -31,7 +31,7 @@ import * as accessibility from './accessibility';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
import { logError, Logger } from './logger'; import { logError, Logger } from './logger';
import { ProgressController, Progress } from './progress'; import { ProgressController, Progress, runAbortableTask } from './progress';
export interface PageDelegate { export interface PageDelegate {
readonly rawMouse: input.RawMouse; readonly rawMouse: input.RawMouse;
@ -68,12 +68,13 @@ export interface PageDelegate {
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>; getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>; getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'>; scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'>;
rafCountForStablePosition(): number;
getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>; getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
pdf?: (options?: types.PDFOptions) => Promise<Buffer>; pdf?: (options?: types.PDFOptions) => Promise<Buffer>;
coverage?: () => any; coverage?: () => any;
// Work around WebKit's raf issues on Windows.
rafCountForStablePosition(): number;
// Work around Chrome's non-associated input and protocol. // Work around Chrome's non-associated input and protocol.
inputActionEpilogue(): Promise<void>; inputActionEpilogue(): Promise<void>;
// Work around for asynchronously dispatched CSP errors in Firefox. // Work around for asynchronously dispatched CSP errors in Firefox.
@ -143,11 +144,6 @@ export class Page extends EventEmitter {
this.coverage = delegate.coverage ? delegate.coverage() : null; this.coverage = delegate.coverage ? delegate.coverage() : null;
} }
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
const controller = new ProgressController(this._logger, timeout, `page.${apiName}`);
return controller.run(task);
}
_didClose() { _didClose() {
assert(!this._closed, 'Page closed twice'); assert(!this._closed, 'Page closed twice');
this._closed = true; this._closed = true;
@ -166,6 +162,12 @@ export class Page extends EventEmitter {
this._disconnectedCallback(new Error('Page closed')); this._disconnectedCallback(new Error('Page closed'));
} }
async _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
return runAbortableTask(async progress => {
return task(progress);
}, this._logger, timeout, apiName);
}
async _onFileChooserOpened(handle: dom.ElementHandle) { async _onFileChooserOpened(handle: dom.ElementHandle) {
const multiple = await handle.evaluate(element => !!(element as HTMLInputElement).multiple); const multiple = await handle.evaluate(element => !!(element as HTMLInputElement).multiple);
if (!this.listenerCount(Events.Page.FileChooser)) { if (!this.listenerCount(Events.Page.FileChooser)) {
@ -448,8 +450,9 @@ export class Page extends EventEmitter {
} }
async screenshot(options: types.ScreenshotOptions = {}): Promise<Buffer> { async screenshot(options: types.ScreenshotOptions = {}): Promise<Buffer> {
const controller = new ProgressController(this._logger, this._timeoutSettings.timeout(options), 'page.screenshot'); return this._runAbortableTask(
return controller.run(progress => this._screenshotter.screenshotPage(progress, options)); progress => this._screenshotter.screenshotPage(progress, options),
this._timeoutSettings.timeout(options), 'page.screenshot');
} }
async title(): Promise<string> { async title(): Promise<string> {

View File

@ -431,9 +431,9 @@ describe('Frame.waitForSelector', function() {
}); });
it('should respect timeout', async({page, server}) => { it('should respect timeout', async({page, server}) => {
let error = null; let error = null;
await page.waitForSelector('div', { timeout: 10, state: 'attached' }).catch(e => error = e); await page.waitForSelector('div', { timeout: 3000, state: 'attached' }).catch(e => error = e);
expect(error).toBeTruthy(); expect(error).toBeTruthy();
expect(error.message).toContain('Timeout 10ms exceeded during page.waitForSelector'); expect(error.message).toContain('Timeout 3000ms exceeded during page.waitForSelector');
expect(error.message).toContain('waiting for selector "div"'); expect(error.message).toContain('waiting for selector "div"');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError); expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
}); });
@ -521,9 +521,9 @@ describe('Frame.waitForSelector xpath', function() {
}); });
it('should respect timeout', async({page}) => { it('should respect timeout', async({page}) => {
let error = null; let error = null;
await page.waitForSelector('//div', { state: 'attached', timeout: 10 }).catch(e => error = e); await page.waitForSelector('//div', { state: 'attached', timeout: 3000 }).catch(e => error = e);
expect(error).toBeTruthy(); expect(error).toBeTruthy();
expect(error.message).toContain('Timeout 10ms exceeded during page.waitForSelector'); expect(error.message).toContain('Timeout 3000ms exceeded during page.waitForSelector');
expect(error.message).toContain('waiting for selector "//div"'); expect(error.message).toContain('waiting for selector "//div"');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError); expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
}); });