diff --git a/docs/api.md b/docs/api.md index 2910204010..899baa71e4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -745,7 +745,7 @@ Emitted when attachment is downloaded. User can access basic file operations on - `element` <[ElementHandle]> handle to the input element that was clicked - `multiple` <[boolean]> Whether file chooser allow for [multiple](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-multiple) file selection. -Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using [`elementHandle.setInputFiles`](#elementhandlesetinputfilesfiles) which can be uploaded in the end. +Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using [`elementHandle.setInputFiles`](#elementhandlesetinputfilesfiles-options) which can be uploaded in the end. ```js page.on('filechooser', async ({element, multiple}) => { @@ -2498,7 +2498,7 @@ ElementHandle instances can be used as an argument in [`page.$eval()`](#pageeval - [elementHandle.screenshot([options])](#elementhandlescreenshotoptions) - [elementHandle.scrollIntoViewIfNeeded()](#elementhandlescrollintoviewifneeded) - [elementHandle.selectOption(values[, options])](#elementhandleselectoptionvalues-options) -- [elementHandle.setInputFiles(files)](#elementhandlesetinputfilesfiles) +- [elementHandle.setInputFiles(files[, options])](#elementhandlesetinputfilesfiles-options) - [elementHandle.toString()](#elementhandletostring) - [elementHandle.type(text[, options])](#elementhandletypetext-options) - [elementHandle.uncheck([options])](#elementhandleuncheckoptions) @@ -2759,11 +2759,19 @@ handle.selectOption('red', 'green', 'blue'); handle.selectOption({ value: 'blue' }, { index: 2 }, 'red'); ``` -#### elementHandle.setInputFiles(files) +#### elementHandle.setInputFiles(files[, options]) - `files` <[string]|[Array]<[string]>|[Object]|[Array]<[Object]>> - `name` <[string]> [File] name **required** - `type` <[string]> [File] type **required** - `data` <[string]> Base64-encoded data **required** +- `options` <[Object]> + - `waitUntil` <"load"|"domcontentloaded"|"networkidle0"|"networkidle2"|"nowait"> Actions that cause navigations are waiting for those navigations to fire `domcontentloaded` by default. This behavior can be changed to either wait for another load phase or to omit the waiting altogether using `nowait`: + - `'domcontentloaded'` - consider navigation to be finished when the `DOMContentLoaded` event is fired. + - `'load'` - consider navigation to be finished when the `load` event is fired. + - `'networkidle0'` - consider navigation to be finished when there are no more than 0 network connections for at least `500` ms. + - `'networkidle2'` - consider navigation to be finished when there are no more than 2 network connections for at least `500` ms. + - `'nowait'` - do not wait. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - returns: <[Promise]> This method expects `elementHandle` to point to an [input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). diff --git a/src/browserContext.ts b/src/browserContext.ts index 42750411cc..1d00f40263 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -86,8 +86,8 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements return event === Events.BrowserContext.Close ? super._abortPromiseForEvent(event) : this._closePromise; } - protected _timeoutForEvent(event: string): number { - return this._timeoutSettings.timeout(); + protected _computeDeadline(options?: types.TimeoutOptions): number { + return this._timeoutSettings.computeDeadline(options); } _browserClosed() { diff --git a/src/dom.ts b/src/dom.ts index 25f9100d5d..36ef5f1e15 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -57,7 +57,7 @@ export class FrameExecutionContext extends js.ExecutionContext { async _doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise { return await this.frame._page._frameManager.waitForNavigationsCreatedBy(async () => { return this._delegate.evaluate(this, returnByValue, pageFunction, ...args); - }, waitForNavigations ? undefined : { waitUntil: 'nowait' }); + }, Number.MAX_SAFE_INTEGER, waitForNavigations ? undefined : { waitUntil: 'nowait' }); } _createHandle(remoteObject: any): js.JSHandle { @@ -181,19 +181,21 @@ export class ElementHandle extends js.JSHandle { return point; } - async _performPointerAction(action: (point: types.Point) => Promise, options?: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise { + async _performPointerAction(action: (point: types.Point) => Promise, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + const deadline = this._page._timeoutSettings.computeDeadline(options); const { force = false } = (options || {}); if (!force) - await this._waitForDisplayedAtStablePosition(options); + await this._waitForDisplayedAtStablePosition(deadline); const position = options ? options.position : undefined; await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined); const point = position ? await this._offsetPoint(position) : await this._clickablePoint(); - if (!force) - await this._waitForHitTargetAt(point, options); point.x = (point.x * 100 | 0) / 100; point.y = (point.y * 100 | 0) / 100; + if (!force) + await this._waitForHitTargetAt(point, deadline); + await this._page._frameManager.waitForNavigationsCreatedBy(async () => { let restoreModifiers: input.Modifier[] | undefined; if (options && options.modifiers) @@ -203,7 +205,7 @@ export class ElementHandle extends js.JSHandle { debugInput('...done'); if (restoreModifiers) await this._page.keyboard._ensureModifiers(restoreModifiers); - }, options, true); + }, deadline, options, true); } hover(options?: PointerActionOptions & types.PointerActionWaitOptions): Promise { @@ -219,6 +221,7 @@ export class ElementHandle extends js.JSHandle { } async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options?: types.NavigatingActionWaitOptions): Promise { + const deadline = this._page._timeoutSettings.computeDeadline(options); let vals: string[] | ElementHandle[] | types.SelectOption[]; if (!Array.isArray(values)) vals = [ values ] as (string[] | ElementHandle[] | types.SelectOption[]); @@ -237,11 +240,12 @@ export class ElementHandle extends js.JSHandle { } return await this._page._frameManager.waitForNavigationsCreatedBy(async () => { return this._evaluateInUtility(({ injected, node }, selectOptions) => injected.selectOptions(node, selectOptions), selectOptions); - }, options); + }, deadline, options); } async fill(value: string, options?: types.NavigatingActionWaitOptions): Promise { assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"'); + const deadline = this._page._timeoutSettings.computeDeadline(options); await this._page._frameManager.waitForNavigationsCreatedBy(async () => { const errorOrNeedsInput = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value); if (typeof errorOrNeedsInput === 'string') @@ -252,10 +256,11 @@ export class ElementHandle extends js.JSHandle { else await this._page.keyboard.press('Delete'); } - }, options, true); + }, deadline, options, true); } - async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[]) { + async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions) { + const deadline = this._page._timeoutSettings.computeDeadline(options); const multiple = await this._evaluateInUtility(({ node }) => { if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT') throw new Error('Node is not an HTMLInputElement'); @@ -283,7 +288,7 @@ export class ElementHandle extends js.JSHandle { } await this._page._frameManager.waitForNavigationsCreatedBy(async () => { await this._page._delegate.setInputFiles(this as any as ElementHandle, filePayloads); - }); + }, deadline, options); } async focus() { @@ -298,17 +303,19 @@ export class ElementHandle extends js.JSHandle { } async type(text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { + const deadline = this._page._timeoutSettings.computeDeadline(options); await this._page._frameManager.waitForNavigationsCreatedBy(async () => { await this.focus(); await this._page.keyboard.type(text, options); - }, options, true); + }, deadline, options, true); } async press(key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { + const deadline = this._page._timeoutSettings.computeDeadline(options); await this._page._frameManager.waitForNavigationsCreatedBy(async () => { await this.focus(); await this._page.keyboard.press(key, options); - }, options, true); + }, deadline, options, true); } async check(options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { @@ -367,16 +374,16 @@ export class ElementHandle extends js.JSHandle { return result; } - async _waitForDisplayedAtStablePosition(options: types.TimeoutOptions = {}): Promise { + async _waitForDisplayedAtStablePosition(deadline: number): Promise { debugInput('waiting for element to be displayed and not moving...'); const stablePromise = this._evaluateInUtility(({ injected, node }, timeout) => { return injected.waitForDisplayedAtStablePosition(node, timeout); - }, options.timeout || 0); - await helper.waitWithTimeout(stablePromise, 'element to be displayed and not moving', options.timeout || 0); + }, helper.timeUntilDeadline(deadline)); + await helper.waitWithDeadline(stablePromise, 'element to be displayed and not moving', deadline); debugInput('...done'); } - async _waitForHitTargetAt(point: types.Point, options: types.TimeoutOptions = {}): Promise { + async _waitForHitTargetAt(point: types.Point, deadline: number): Promise { debugInput(`waiting for element to receive pointer events at (${point.x},${point.y}) ...`); const frame = await this.ownerFrame(); if (frame && frame.parentFrame()) { @@ -389,8 +396,8 @@ export class ElementHandle extends js.JSHandle { } const hitTargetPromise = this._evaluateInUtility(({ injected, node }, { timeout, point }) => { return injected.waitForHitTargetAt(node, timeout, point); - }, { timeout: options.timeout || 0, point }); - await helper.waitWithTimeout(hitTargetPromise, 'element to receive pointer events', options.timeout || 0); + }, { timeout: helper.timeUntilDeadline(deadline), point }); + await helper.waitWithDeadline(hitTargetPromise, 'element to receive pointer events', deadline); debugInput('...done'); } } diff --git a/src/extendedEventEmitter.ts b/src/extendedEventEmitter.ts index b6ffb00361..db4addd849 100644 --- a/src/extendedEventEmitter.ts +++ b/src/extendedEventEmitter.ts @@ -16,21 +16,22 @@ import { EventEmitter } from 'events'; import { helper } from './helper'; +import { TimeoutOptions } from './types'; export class ExtendedEventEmitter extends EventEmitter { protected _abortPromiseForEvent(event: string) { return new Promise(() => void 0); } - protected _timeoutForEvent(event: string): number { + protected _computeDeadline(options?: TimeoutOptions): number { throw new Error('unimplemented'); } async waitForEvent(event: string, optionsOrPredicate: Function|{ predicate?: Function, timeout?: number } = {}): Promise { + const deadline = this._computeDeadline(typeof optionsOrPredicate === 'function' ? undefined : optionsOrPredicate); const { predicate = () => true, - timeout = this._timeoutForEvent(event) } = typeof optionsOrPredicate === 'function' ? {predicate: optionsOrPredicate} : optionsOrPredicate; - return helper.waitForEvent(this, event, predicate, timeout, this._abortPromiseForEvent(event)); + return helper.waitForEvent(this, event, predicate, deadline, this._abortPromiseForEvent(event)); } } diff --git a/src/frames.ts b/src/frames.ts index b440d8b635..4cab0384bc 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -99,10 +99,10 @@ export class FrameManager { } } - async waitForNavigationsCreatedBy(action: () => Promise, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise { + async waitForNavigationsCreatedBy(action: () => Promise, deadline: number, options: types.NavigatingActionWaitOptions = {}, input?: boolean): Promise { if (options.waitUntil === 'nowait') return action(); - const barrier = new PendingNavigationBarrier({ waitUntil: 'domcontentloaded', ...options }); + const barrier = new PendingNavigationBarrier({ waitUntil: 'domcontentloaded', ...options }, deadline); this._pendingNavigationBarriers.add(barrier); try { const result = await action(); @@ -438,12 +438,13 @@ export class Frame { async waitForSelector(selector: string, options?: types.WaitForElementOptions): Promise | null> { if (options && (options as any).visibility) throw new Error('options.visibility is not supported, did you mean options.waitFor?'); - const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'attached' } = (options || {}); + const { waitFor = 'attached' } = (options || {}); if (!['attached', 'detached', 'visible', 'hidden'].includes(waitFor)) throw new Error(`Unsupported waitFor option "${waitFor}"`); - const { world, task } = selectors._waitForSelectorTask(selector, waitFor, timeout); - const result = await this._scheduleRerunnableTask(task, world, timeout, `selector "${selectorToString(selector, waitFor)}"`); + const deadline = this._page._timeoutSettings.computeDeadline(options); + const { world, task } = selectors._waitForSelectorTask(selector, waitFor, deadline); + const result = await this._scheduleRerunnableTask(task, world, deadline, `selector "${selectorToString(selector, waitFor)}"`); if (!result.asElement()) { result.dispose(); return null; @@ -667,64 +668,64 @@ export class Frame { return result!; } - async click(selector: string, options?: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - await handle.click(options); + async click(selector: string, options: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + await handle.click(helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); } - async dblclick(selector: string, options?: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - await handle.dblclick(options); + async dblclick(selector: string, options: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + await handle.dblclick(helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); } - async fill(selector: string, value: string, options?: types.NavigatingActionWaitOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - await handle.fill(value, options); + async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) { + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + await handle.fill(value, helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); } async focus(selector: string, options?: types.TimeoutOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); + const { handle } = await this._waitForSelectorInUtilityContext(selector, options); await handle.focus(); handle.dispose(); } async hover(selector: string, options?: dom.PointerActionOptions & types.PointerActionWaitOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - await handle.hover(options); + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + await handle.hover(helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); } async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[], options?: types.NavigatingActionWaitOptions): Promise { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - const result = await handle.selectOption(values, options); + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + const result = await handle.selectOption(values, helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); return result; } async type(selector: string, text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - await handle.type(text, options); + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + await handle.type(text, helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); } async press(selector: string, key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - await handle.press(key, options); + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + await handle.press(key, helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); } async check(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - await handle.check(options); + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + await handle.check(helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); } async uncheck(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - await handle.uncheck(options); + const { handle, deadline } = await this._waitForSelectorInUtilityContext(selector, options); + await handle.uncheck(helper.optionsWithUpdatedTimeout(options, deadline)); handle.dispose(); } @@ -738,17 +739,19 @@ export class Frame { return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout))); } - private async _waitForSelectorInUtilityContext(selector: string, options?: types.WaitForElementOptions): Promise> { - const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'attached' } = (options || {}); - const { world, task } = selectors._waitForSelectorTask(selector, waitFor, timeout); - const result = await this._scheduleRerunnableTask(task, world, timeout, `selector "${selectorToString(selector, waitFor)}"`); - return result.asElement() as dom.ElementHandle; + private async _waitForSelectorInUtilityContext(selector: string, options?: types.WaitForElementOptions): Promise<{ handle: dom.ElementHandle, deadline: number }> { + const { waitFor = 'attached' } = options || {}; + const deadline = this._page._timeoutSettings.computeDeadline(options); + const { world, task } = selectors._waitForSelectorTask(selector, waitFor, deadline); + const result = await this._scheduleRerunnableTask(task, world, deadline, `selector "${selectorToString(selector, waitFor)}"`); + return { handle: result.asElement() as dom.ElementHandle, deadline }; } async waitForFunction(pageFunction: types.Func1, arg: Arg, options?: types.WaitForFunctionOptions): Promise>; async waitForFunction(pageFunction: types.Func1, arg?: any, options?: types.WaitForFunctionOptions): Promise>; async waitForFunction(pageFunction: types.Func1, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise> { - const { polling = 'raf', timeout = this._page._timeoutSettings.timeout() } = options; + const { polling = 'raf' } = options; + const deadline = this._page._timeoutSettings.computeDeadline(options); if (helper.isString(polling)) assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling); else if (helper.isNumber(polling)) @@ -760,8 +763,8 @@ export class Frame { const task = async (context: dom.FrameExecutionContext) => context.evaluateHandleInternal(({ injected, predicateBody, polling, timeout, arg }) => { const innerPredicate = new Function('arg', predicateBody); return injected.poll(polling, timeout, () => innerPredicate(arg)); - }, { injected: await context._injected(), predicateBody, polling, timeout, arg }); - return this._scheduleRerunnableTask(task, 'main', timeout) as any as types.SmartHandle; + }, { injected: await context._injected(), predicateBody, polling, timeout: helper.timeUntilDeadline(deadline), arg }); + return this._scheduleRerunnableTask(task, 'main', deadline) as any as types.SmartHandle; } async title(): Promise { @@ -781,9 +784,9 @@ export class Frame { this._parentFrame = null; } - private _scheduleRerunnableTask(task: Task, contextType: ContextType, timeout?: number, title?: string): Promise { + private _scheduleRerunnableTask(task: Task, contextType: ContextType, deadline: number, title?: string): Promise { const data = this._contextData.get(contextType)!; - const rerunnableTask = new RerunnableTask(data, task, timeout, title); + const rerunnableTask = new RerunnableTask(data, task, deadline, title); data.rerunnableTasks.add(rerunnableTask); if (data.context) rerunnableTask.rerun(data.context); @@ -834,7 +837,7 @@ class RerunnableTask { private _timeoutTimer?: NodeJS.Timer; private _terminated = false; - constructor(data: ContextData, task: Task, timeout?: number, title?: string) { + constructor(data: ContextData, task: Task, deadline: number, title?: string) { this._contextData = data; this._task = task; this._runCount = 0; @@ -844,10 +847,8 @@ class RerunnableTask { }); // Since page navigation requires us to re-install the pageScript, we should track // timeout on our end. - if (timeout) { - const timeoutError = new TimeoutError(`waiting for ${title || 'function'} failed: timeout ${timeout}ms exceeded`); - this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), timeout); - } + const timeoutError = new TimeoutError(`waiting for ${title || 'function'} failed: timeout exceeded`); + this._timeoutTimer = setTimeout(() => this.terminate(timeoutError), helper.timeUntilDeadline(deadline)); } terminate(error: Error) { @@ -918,13 +919,15 @@ function selectorToString(selector: string, waitFor: 'attached' | 'detached' | ' class PendingNavigationBarrier { private _frameIds = new Map(); - private _options: types.NavigatingActionWaitOptions | undefined; + private _options: types.NavigatingActionWaitOptions; private _protectCount = 0; private _promise: Promise; private _promiseCallback = () => {}; + private _deadline: number; - constructor(options: types.NavigatingActionWaitOptions | undefined) { + constructor(options: types.NavigatingActionWaitOptions, deadline: number) { this._options = options; + this._deadline = deadline; this._promise = new Promise(f => this._promiseCallback = f); this.retain(); } @@ -936,7 +939,9 @@ class PendingNavigationBarrier { async addFrame(frame: Frame) { this.retain(); - await frame.waitForNavigation(this._options as types.NavigateOptions).catch(e => {}); + const timeout = helper.timeUntilDeadline(this._deadline); + const options = { ...this._options, timeout } as types.NavigateOptions; + await frame.waitForNavigation(options).catch(e => {}); this.release(); } @@ -974,7 +979,7 @@ export class FrameTask { let timeoutPromise = new Promise(() => {}); const { timeout = frame._page._timeoutSettings.navigationTimeout() } = options; if (timeout) { - const errorMessage = 'Navigation timeout of ' + timeout + ' ms exceeded'; + const errorMessage = 'Navigation timeout exceeded'; timeoutPromise = new Promise(fulfill => this._timer = setTimeout(fulfill, timeout)) .then(() => { throw new TimeoutError(errorMessage); }); } diff --git a/src/helper.ts b/src/helper.ts index faa42b4e6a..d5c2e8b84a 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -149,9 +149,8 @@ class Helper { emitter: EventEmitter, eventName: (string | symbol), predicate: Function, - timeout: number, + deadline: number, abortPromise: Promise): Promise { - let eventTimeout: NodeJS.Timer; let resolveCallback: (event: any) => void = () => {}; let rejectCallback: (error: any) => void = () => {}; const promise = new Promise((resolve, reject) => { @@ -167,11 +166,9 @@ class Helper { rejectCallback(e); } }); - if (timeout) { - eventTimeout = setTimeout(() => { - rejectCallback(new TimeoutError(`Timeout exceeded while waiting for ${String(eventName)}`)); - }, timeout); - } + const eventTimeout = setTimeout(() => { + rejectCallback(new TimeoutError(`Timeout exceeded while waiting for ${String(eventName)}`)); + }, helper.timeUntilDeadline(deadline)); function cleanup() { Helper.removeEventListeners([listener]); clearTimeout(eventTimeout); @@ -189,12 +186,14 @@ class Helper { } static async waitWithTimeout(promise: Promise, taskName: string, timeout: number): Promise { + return this.waitWithDeadline(promise, taskName, helper.monotonicTime() + timeout); + } + + static async waitWithDeadline(promise: Promise, taskName: string, deadline: number): Promise { let reject: (error: Error) => void; - const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout ${timeout}ms exceeded`); + const timeoutError = new TimeoutError(`waiting for ${taskName} failed: timeout exceeded`); const timeoutPromise = new Promise((resolve, x) => reject = x); - let timeoutTimer = null; - if (timeout) - timeoutTimer = setTimeout(() => reject(timeoutError), timeout); + const timeoutTimer = setTimeout(() => reject(timeoutError), helper.timeUntilDeadline(deadline)); try { return await Promise.race([promise, timeoutPromise]); } finally { @@ -341,6 +340,19 @@ class Helper { static guid(): string { return crypto.randomBytes(16).toString('hex'); } + + static monotonicTime(): number { + const [seconds, nanoseconds] = process.hrtime(); + return seconds * 1000 + (nanoseconds / 1000000 | 0); + } + + static timeUntilDeadline(deadline: number): number { + return Math.min(deadline - this.monotonicTime(), 2147483647); // 2^31-1 safe setTimeout in Node. + } + + static optionsWithUpdatedTimeout(options: T | undefined, deadline: number): T { + return { ...(options || {}) as T, timeout: this.timeUntilDeadline(deadline) }; + } } export function assert(value: any, message?: string): asserts value { diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 04249652b1..2fd46cd985 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -257,7 +257,7 @@ class Injected { return isDisplayedAndStable; }); if (!result) - throw new Error(`waiting for element to be displayed and not moving failed: timeout ${timeout}ms exceeded`); + throw new Error(`waiting for element to be displayed and not moving failed: timeout exceeded`); } async waitForHitTargetAt(node: Node, timeout: number, point: types.Point) { @@ -273,7 +273,7 @@ class Injected { return hitElement === element; }); if (!result) - throw new Error(`waiting for element to receive mouse events failed: timeout ${timeout}ms exceeded`); + throw new Error(`waiting for element to receive mouse events failed: timeout exceeded`); } private _parentElementOrShadowHost(element: Element): Element | undefined { diff --git a/src/page.ts b/src/page.ts index 75a7f9a054..28626d6e5b 100644 --- a/src/page.ts +++ b/src/page.ts @@ -147,8 +147,8 @@ export class Page extends ExtendedEventEmitter { return this._disconnectedPromise; } - protected _timeoutForEvent(event: string): number { - return this._timeoutSettings.timeout(); + protected _computeDeadline(options?: types.TimeoutOptions): number { + return this._timeoutSettings.computeDeadline(options); } _didClose() { @@ -313,21 +313,21 @@ export class Page extends ExtendedEventEmitter { } async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean), options: types.TimeoutOptions = {}): Promise { - const { timeout = this._timeoutSettings.timeout() } = options; + const deadline = this._timeoutSettings.computeDeadline(options); return helper.waitForEvent(this, Events.Page.Request, (request: network.Request) => { if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) return helper.urlMatches(request.url(), urlOrPredicate); return urlOrPredicate(request); - }, timeout, this._disconnectedPromise); + }, deadline, this._disconnectedPromise); } async waitForResponse(urlOrPredicate: string | RegExp | ((r: network.Response) => boolean), options: types.TimeoutOptions = {}): Promise { - const { timeout = this._timeoutSettings.timeout() } = options; + const deadline = this._timeoutSettings.computeDeadline(options); return helper.waitForEvent(this, Events.Page.Response, (response: network.Response) => { if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) return helper.urlMatches(response.url(), urlOrPredicate); return urlOrPredicate(response); - }, timeout, this._disconnectedPromise); + }, deadline, this._disconnectedPromise); } async goBack(options?: types.NavigateOptions): Promise { diff --git a/src/selectors.ts b/src/selectors.ts index b2a35f7f3d..4c84e9aa3b 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -134,7 +134,7 @@ export class Selectors { return result; } - _waitForSelectorTask(selector: string, waitFor: 'attached' | 'detached' | 'visible' | 'hidden', timeout: number): { world: 'main' | 'utility', task: (context: dom.FrameExecutionContext) => Promise } { + _waitForSelectorTask(selector: string, waitFor: 'attached' | 'detached' | 'visible' | 'hidden', deadline: number): { world: 'main' | 'utility', task: (context: dom.FrameExecutionContext) => Promise } { const parsed = this._parseSelector(selector); const task = async (context: dom.FrameExecutionContext) => context.evaluateHandleInternal(({ evaluator, parsed, waitFor, timeout }) => { const polling = (waitFor === 'attached' || waitFor === 'detached') ? 'mutation' : 'raf'; @@ -151,7 +151,7 @@ export class Selectors { return !element || !evaluator.injected.isVisible(element); } }); - }, { evaluator: await this._prepareEvaluator(context), parsed, waitFor, timeout }); + }, { evaluator: await this._prepareEvaluator(context), parsed, waitFor, timeout: helper.timeUntilDeadline(deadline) }); return { world: this._needsMainContext(parsed) ? 'main' : 'utility', task }; } diff --git a/src/timeoutSettings.ts b/src/timeoutSettings.ts index 4669248239..f3ae69c8c3 100644 --- a/src/timeoutSettings.ts +++ b/src/timeoutSettings.ts @@ -15,6 +15,9 @@ * limitations under the License. */ +import { TimeoutOptions } from './types'; +import { helper } from './helper'; + const DEFAULT_TIMEOUT = 30000; export class TimeoutSettings { @@ -44,11 +47,20 @@ export class TimeoutSettings { return DEFAULT_TIMEOUT; } - timeout(): number { + private _timeout(): number { if (this._defaultTimeout !== null) return this._defaultTimeout; if (this._parent) - return this._parent.timeout(); + return this._parent._timeout(); return DEFAULT_TIMEOUT; } + + computeDeadline(options?: TimeoutOptions) { + const { timeout } = options || {}; + if (timeout === 0) + return Number.MAX_SAFE_INTEGER; + else if (typeof timeout === 'number') + return helper.monotonicTime() + timeout; + return helper.monotonicTime() + this._timeout(); + } } diff --git a/test/click.spec.js b/test/click.spec.js index af4e8d9cd6..88d138be99 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -169,7 +169,7 @@ const utils = require('./utils'); await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', b => b.style.display = 'none'); const error = await page.click('button', { timeout: 100 }).catch(e => e); - expect(error.message).toContain('timeout 100ms exceeded'); + expect(error.message).toContain('timeout exceeded'); }); it('should waitFor visible when parent is hidden', async({page, server}) => { let done = false; @@ -413,7 +413,7 @@ const utils = require('./utils'); button.style.marginLeft = '200px'; }); const error = await button.click({ timeout: 100 }).catch(e => e); - expect(error.message).toContain('timeout 100ms exceeded'); + expect(error.message).toContain('timeout exceeded'); }); it('should wait for becoming hit target', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -461,7 +461,7 @@ const utils = require('./utils'); document.body.appendChild(blocker); }); const error = await button.click({ timeout: 100 }).catch(e => e); - expect(error.message).toContain('timeout 100ms exceeded'); + expect(error.message).toContain('timeout exceeded'); }); it('should fail when obscured and not waiting for hit target', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); @@ -553,10 +553,10 @@ const utils = require('./utils'); // guarantee timeout. const error1 = await page.click('button', { timeout: 250 }).catch(e => e); if (error1) - expect(error1.message).toContain('timeout 250ms exceeded'); + expect(error1.message).toContain('timeout exceeded'); const error2 = await page.click('button', { timeout: 250 }).catch(e => e); if (error2) - expect(error2.message).toContain('timeout 250ms exceeded'); + expect(error2.message).toContain('timeout exceeded'); }); }); diff --git a/test/navigation.spec.js b/test/navigation.spec.js index 61592685d1..ebccac53f9 100644 --- a/test/navigation.spec.js +++ b/test/navigation.spec.js @@ -198,7 +198,7 @@ module.exports.describe = function({playwright, MAC, WIN, FFOX, CHROMIUM, WEBKIT server.setRoute('/empty.html', (req, res) => { }); let error = null; await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(e => error = e); - const message = 'Navigation timeout of 1 ms exceeded'; + const message = 'Navigation timeout exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -209,7 +209,7 @@ module.exports.describe = function({playwright, MAC, WIN, FFOX, CHROMIUM, WEBKIT page.context().setDefaultNavigationTimeout(2); page.setDefaultNavigationTimeout(1); await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); - const message = 'Navigation timeout of 1 ms exceeded'; + const message = 'Navigation timeout exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -219,7 +219,7 @@ module.exports.describe = function({playwright, MAC, WIN, FFOX, CHROMIUM, WEBKIT let error = null; page.context().setDefaultNavigationTimeout(2); await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); - const message = 'Navigation timeout of 2 ms exceeded'; + const message = 'Navigation timeout exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -230,7 +230,7 @@ module.exports.describe = function({playwright, MAC, WIN, FFOX, CHROMIUM, WEBKIT page.context().setDefaultTimeout(2); page.setDefaultTimeout(1); await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); - const message = 'Navigation timeout of 1 ms exceeded'; + const message = 'Navigation timeout exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -240,7 +240,7 @@ module.exports.describe = function({playwright, MAC, WIN, FFOX, CHROMIUM, WEBKIT let error = null; page.context().setDefaultTimeout(2); await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); - const message = 'Navigation timeout of 2 ms exceeded'; + const message = 'Navigation timeout exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -251,7 +251,7 @@ module.exports.describe = function({playwright, MAC, WIN, FFOX, CHROMIUM, WEBKIT page.setDefaultTimeout(0); page.setDefaultNavigationTimeout(1); await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); - const message = 'Navigation timeout of 1 ms exceeded'; + const message = 'Navigation timeout exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -818,7 +818,7 @@ module.exports.describe = function({playwright, MAC, WIN, FFOX, CHROMIUM, WEBKIT server.setRoute('/one-style.css', (req, res) => response = res); await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'}); const error = await page.waitForLoadState('load', { timeout: 1 }).catch(e => e); - expect(error.message).toBe('Navigation timeout of 1 ms exceeded'); + expect(error.message).toBe('Navigation timeout exceeded'); }); it('should resolve immediately if loaded', async({page, server}) => { await page.goto(server.PREFIX + '/one-style.html');