chore: migrate navigations to Progress (#2463)

This commit is contained in:
Dmitry Gozman 2020-06-04 16:43:48 -07:00 committed by GitHub
parent 724d73c03b
commit 1d37a10558
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 344 additions and 297 deletions

View file

@ -28,8 +28,8 @@ import { Page } from './page';
import { selectors } from './selectors';
import * as types from './types';
import { NotConnectedError } from './errors';
import { Log, logError } from './logger';
import { Progress } from './progress';
import { logError, apiLog } from './logger';
import { Progress, runAbortableTask } from './progress';
export type PointerActionOptions = {
modifiers?: input.Modifier[];
@ -40,11 +40,6 @@ export type ClickOptions = PointerActionOptions & input.MouseClickOptions;
export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions;
export const inputLog: Log = {
name: 'input',
color: 'cyan'
};
export class FrameExecutionContext extends js.ExecutionContext {
readonly frame: frames.Frame;
private _injectedPromise?: Promise<js.JSHandle>;
@ -251,7 +246,7 @@ 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> {
while (!progress.isCanceled()) {
while (progress.isRunning()) {
const result = await this._performPointerAction(progress, action, options);
if (result === 'done')
return;
@ -263,27 +258,27 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (!force)
await this._waitForDisplayedAtStablePositionAndEnabled(progress);
progress.log(inputLog, 'scrolling into view if needed...');
progress.log(apiLog, 'scrolling into view if needed...');
const scrolled = await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined);
if (scrolled === 'invisible') {
if (force)
throw new Error('Element is not visible');
progress.log(inputLog, '...element is not visible, retrying input action');
progress.log(apiLog, '...element is not visible, retrying input action');
return 'retry';
}
progress.log(inputLog, '...done scrolling');
progress.log(apiLog, '...done scrolling');
const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
if (maybePoint === 'invisible') {
if (force)
throw new Error('Element is not visible');
progress.log(inputLog, 'element is not visibile, retrying input action');
progress.log(apiLog, 'element is not visibile, retrying input action');
return 'retry';
}
if (maybePoint === 'outsideviewport') {
if (force)
throw new Error('Element is outside of the viewport');
progress.log(inputLog, 'element is outside of the viewport, retrying input action');
progress.log(apiLog, 'element is outside of the viewport, retrying input action');
return 'retry';
}
const point = roundPoint(maybePoint);
@ -291,33 +286,35 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (!force) {
if ((options as any).__testHookBeforeHitTarget)
await (options as any).__testHookBeforeHitTarget();
progress.log(inputLog, `checking that element receives pointer events at (${point.x},${point.y})...`);
progress.log(apiLog, `checking that element receives pointer events at (${point.x},${point.y})...`);
const matchesHitTarget = await this._checkHitTargetAt(point);
if (!matchesHitTarget) {
progress.log(inputLog, '...element does not receive pointer events, retrying input action');
progress.log(apiLog, '...element does not receive pointer events, retrying input action');
return 'retry';
}
progress.log(inputLog, `...element does receive pointer events, continuing input action`);
progress.log(apiLog, `...element does receive pointer events, continuing input action`);
}
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
progress.log(inputLog, `performing ${progress.apiName} action...`);
progress.log(apiLog, `performing ${progress.apiName} action...`);
await action(point);
progress.log(inputLog, `...${progress.apiName} action done`);
progress.log(inputLog, 'waiting for scheduled navigations to finish...');
progress.log(apiLog, `...${progress.apiName} action done`);
progress.log(apiLog, 'waiting for scheduled navigations to finish...');
if ((options as any).__testHookAfterPointerAction)
await (options as any).__testHookAfterPointerAction();
if (restoreModifiers)
await this._page.keyboard._ensureModifiers(restoreModifiers);
}, 'input');
progress.log(inputLog, '...navigations have finished');
progress.log(apiLog, '...navigations have finished');
return 'done';
}
hover(options: PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> {
return Progress.runCancelableTask(progress => this._hover(progress, options), options, this._page, this._page._timeoutSettings);
return runAbortableTask(progress => this._hover(progress, options), options, this._page, this._page._timeoutSettings);
}
_hover(progress: Progress, options: PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
@ -325,7 +322,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
click(options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
return Progress.runCancelableTask(progress => this._click(progress, options), options, this._page, this._page._timeoutSettings);
return runAbortableTask(progress => this._click(progress, options), options, this._page, this._page._timeoutSettings);
}
_click(progress: Progress, options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
@ -333,7 +330,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
dblclick(options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
return Progress.runCancelableTask(progress => this._dblclick(progress, options), options, this._page, this._page._timeoutSettings);
return runAbortableTask(progress => this._dblclick(progress, options), options, this._page, this._page._timeoutSettings);
}
_dblclick(progress: Progress, options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<void> {
@ -341,11 +338,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
return Progress.runCancelableTask(progress => this._selectOption(progress, values, options), options, this._page, this._page._timeoutSettings);
return runAbortableTask(progress => this._selectOption(progress, values, options), options, this._page, this._page._timeoutSettings);
}
async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise<string[]> {
progress.log(inputLog, progress.apiName);
progress.log(apiLog, progress.apiName);
let vals: string[] | ElementHandle[] | types.SelectOption[];
if (!Array.isArray(values))
vals = [ values ] as (string[] | ElementHandle[] | types.SelectOption[]);
@ -369,11 +366,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
return Progress.runCancelableTask(progress => this._fill(progress, value, options), options, this._page, this._page._timeoutSettings);
return runAbortableTask(progress => this._fill(progress, value, options), options, this._page, this._page._timeoutSettings);
}
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<void> {
progress.log(inputLog, `elementHandle.fill("${value}")`);
progress.log(apiLog, `elementHandle.fill("${value}")`);
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
const poll = await this._evaluateHandleInUtility(([injected, node, value]) => {
@ -392,17 +389,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async selectText(): Promise<void> {
this._page._log(inputLog, `elementHandle.selectText()`);
this._page._log(apiLog, `elementHandle.selectText()`);
const injectedResult = await this._evaluateInUtility(([injected, node]) => injected.selectText(node), {});
handleInjectedResult(injectedResult);
}
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) {
return Progress.runCancelableTask(async progress => this._setInputFiles(progress, files, options), options, this._page, this._page._timeoutSettings);
return runAbortableTask(async progress => this._setInputFiles(progress, files, options), options, this._page, this._page._timeoutSettings);
}
async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions) {
progress.log(inputLog, progress.apiName);
progress.log(apiLog, progress.apiName);
const injectedResult = await this._evaluateInUtility(([injected, node]): types.InjectedScriptResult<boolean> => {
if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT')
return { status: 'error', error: 'Node is not an HTMLInputElement' };
@ -437,17 +434,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async focus() {
this._page._log(inputLog, `elementHandle.focus()`);
this._page._log(apiLog, `elementHandle.focus()`);
const injectedResult = await this._evaluateInUtility(([injected, node]) => injected.focusNode(node), {});
handleInjectedResult(injectedResult);
}
async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
return Progress.runCancelableTask(progress => this._type(progress, text, options), options, this._page, this._page._timeoutSettings);
return runAbortableTask(progress => this._type(progress, text, options), options, this._page, this._page._timeoutSettings);
}
async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions) {
progress.log(inputLog, `elementHandle.type("${text}")`);
progress.log(apiLog, `elementHandle.type("${text}")`);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
await this.focus();
await this._page.keyboard.type(text, options);
@ -455,11 +452,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
return Progress.runCancelableTask(progress => this._press(progress, key, options), options, this._page, this._page._timeoutSettings);
return runAbortableTask(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}")`);
progress.log(apiLog, `elementHandle.press("${key}")`);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
await this.focus();
await this._page.keyboard.press(key, options);
@ -467,15 +464,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
return Progress.runCancelableTask(async progress => {
progress.log(inputLog, `elementHandle.check()`);
return runAbortableTask(async progress => {
progress.log(apiLog, `elementHandle.check()`);
await this._setChecked(progress, true, options);
}, options, this._page, this._page._timeoutSettings);
}
async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
return Progress.runCancelableTask(async progress => {
progress.log(inputLog, `elementHandle.uncheck()`);
return runAbortableTask(async progress => {
progress.log(apiLog, `elementHandle.uncheck()`);
await this._setChecked(progress, false, options);
}, options, this._page, this._page._timeoutSettings);
}
@ -525,7 +522,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise<void> {
progress.log(inputLog, 'waiting for element to be displayed, enabled and not moving...');
progress.log(apiLog, 'waiting for element to be displayed, enabled and not moving...');
const rafCount = this._page._delegate.rafCountForStablePosition();
const poll = await this._evaluateHandleInUtility(([injected, node, rafCount]) => {
return injected.waitForDisplayedAtStablePositionAndEnabled(node, rafCount);
@ -533,7 +530,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
new InjectedScriptPollHandler(progress, poll);
const injectedResult = await poll.evaluate(poll => poll.result);
handleInjectedResult(injectedResult);
progress.log(inputLog, '...element is displayed and does not move');
progress.log(apiLog, '...element is displayed and does not move');
}
async _checkHitTargetAt(point: types.Point): Promise<boolean> {
@ -563,18 +560,18 @@ export class InjectedScriptPollHandler {
constructor(progress: Progress, poll: js.JSHandle<types.InjectedScriptPoll<any>>) {
this._progress = progress;
this._poll = poll;
this._progress.cleanupWhenCanceled(() => this.cancel());
this._progress.cleanupWhenAborted(() => this.cancel());
this._streamLogs(poll.evaluateHandle(poll => poll.logs));
}
private _streamLogs(logsPromise: Promise<js.JSHandle<types.InjectedScriptLogs>>) {
// We continuously get a chunk of logs, stream them to the progress and wait for the next chunk.
logsPromise.catch(e => null).then(logs => {
if (!logs || !this._poll || this._progress.isCanceled())
if (!logs || !this._poll || !this._progress.isRunning())
return;
logs.evaluate(logs => logs.current).catch(e => [] as string[]).then(messages => {
for (const message of messages)
this._progress.log(inputLog, message);
this._progress.log(apiLog, message);
});
this._streamLogs(logs.evaluateHandle(logs => logs.next));
});

View file

@ -19,7 +19,7 @@ import * as fs from 'fs';
import * as util from 'util';
import { ConsoleMessage } from './console';
import * as dom from './dom';
import { TimeoutError, NotConnectedError } from './errors';
import { NotConnectedError } from './errors';
import { Events } from './events';
import { assert, helper, RegisteredListener, assertMaxArguments, debugAssert } from './helper';
import * as js from './javascript';
@ -29,8 +29,8 @@ import { selectors } from './selectors';
import * as types from './types';
import { waitForTimeoutWasUsed } from './hints';
import { BrowserContext } from './browserContext';
import { rewriteErrorMessage } from './debug/stackTrace';
import { Progress } from './progress';
import { Progress, ProgressController, runAbortableTask } from './progress';
import { apiLog } from './logger';
type ContextType = 'main' | 'utility';
type ContextData = {
@ -111,7 +111,7 @@ export class FrameManager {
const barrier = new SignalBarrier(progress);
this._signalBarriers.add(barrier);
if (progress)
progress.cleanupWhenCanceled(() => this._signalBarriers.delete(barrier));
progress.cleanupWhenAborted(() => this._signalBarriers.delete(barrier));
const result = await action();
if (source === 'input')
await this._page._delegate.inputActionEpilogue();
@ -180,26 +180,18 @@ export class FrameManager {
}
frameStoppedLoading(frameId: string) {
const frame = this._frames.get(frameId);
if (!frame)
return;
const hasDOMContentLoaded = frame._firedLifecycleEvents.has('domcontentloaded');
const hasLoad = frame._firedLifecycleEvents.has('load');
frame._firedLifecycleEvents.add('domcontentloaded');
frame._firedLifecycleEvents.add('load');
this._notifyLifecycle(frame);
if (frame === this.mainFrame() && !hasDOMContentLoaded)
this._page.emit(Events.Page.DOMContentLoaded);
if (frame === this.mainFrame() && !hasLoad)
this._page.emit(Events.Page.Load);
this.frameLifecycleEvent(frameId, 'domcontentloaded');
this.frameLifecycleEvent(frameId, 'load');
}
frameLifecycleEvent(frameId: string, event: types.LifecycleEvent) {
const frame = this._frames.get(frameId);
if (!frame)
return;
if (frame._firedLifecycleEvents.has(event))
return;
frame._firedLifecycleEvents.add(event);
this._notifyLifecycle(frame);
this._notifyLifecycle(frame, event);
if (frame === this._mainFrame && event === 'load')
this._page.emit(Events.Page.Load);
if (frame === this._mainFrame && event === 'domcontentloaded')
@ -261,10 +253,10 @@ export class FrameManager {
task.onNewDocument(documentId, new Error(error));
}
private _notifyLifecycle(frame: Frame) {
private _notifyLifecycle(frame: Frame, lifecycleEvent: types.LifecycleEvent) {
for (let parent: Frame | null = frame; parent; parent = parent.parentFrame()) {
for (const frameTask of parent._frameTasks)
frameTask.onLifecycle();
frameTask.onLifecycle(frame, lifecycleEvent);
}
}
@ -350,55 +342,67 @@ export class Frame {
}
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
const headers = (this._page._state.extraHTTPHeaders || {});
let referer = headers['referer'] || headers['Referer'];
if (options.referer !== undefined) {
if (referer !== undefined && referer !== options.referer)
throw new Error('"referer" is already specified as extra HTTP header');
referer = options.referer;
}
url = helper.completeUserURL(url);
const progressController = new ProgressController(options, this._page, this._page._timeoutSettings.navigationTimeout());
abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => {
progress.log(apiLog, `${progress.apiName}("${url}"), waiting until "${options.waitUntil || 'load'}"`);
const headers = (this._page._state.extraHTTPHeaders || {});
let referer = headers['referer'] || headers['Referer'];
if (options.referer !== undefined) {
if (referer !== undefined && referer !== options.referer)
throw new Error('"referer" is already specified as extra HTTP header');
referer = options.referer;
}
url = helper.completeUserURL(url);
const frameTask = new FrameTask(this, options, url);
const sameDocumentPromise = frameTask.waitForSameDocumentNavigation();
const navigateResult = await frameTask.raceAgainstFailures(this._page._delegate.navigateFrame(this, url, referer)).catch(e => {
// Do not leave sameDocumentPromise unhandled.
sameDocumentPromise.catch(e => {});
throw e;
const frameTask = new FrameTask(this, progress);
const sameDocumentPromise = frameTask.waitForSameDocumentNavigation();
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer).catch(e => {
// Do not leave sameDocumentPromise unhandled.
sameDocumentPromise.catch(e => {});
throw e;
});
if (navigateResult.newDocumentId) {
// Do not leave sameDocumentPromise unhandled.
sameDocumentPromise.catch(e => {});
await frameTask.waitForSpecificDocument(navigateResult.newDocumentId);
} else {
await sameDocumentPromise;
}
const request = (navigateResult && navigateResult.newDocumentId) ? frameTask.request(navigateResult.newDocumentId) : null;
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil);
frameTask.done();
return request ? request._finalRequest().response() : null;
});
if (navigateResult.newDocumentId) {
// Do not leave sameDocumentPromise unhandled.
sameDocumentPromise.catch(e => {});
await frameTask.waitForSpecificDocument(navigateResult.newDocumentId);
} else {
await sameDocumentPromise;
}
const request = (navigateResult && navigateResult.newDocumentId) ? frameTask.request(navigateResult.newDocumentId) : null;
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil);
frameTask.done();
return request ? request._finalRequest().response() : null;
}
async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> {
return this._waitForNavigation(options);
}
async _waitForNavigation(options: types.ExtendedWaitForNavigationOptions = {}): Promise<network.Response | null> {
const frameTask = new FrameTask(this, options);
let documentId: string | undefined;
await Promise.race([
frameTask.waitForNewDocument(options.url).then(id => documentId = id),
frameTask.waitForSameDocumentNavigation(options.url),
]);
const request = documentId ? frameTask.request(documentId) : null;
if (options.waitUntil !== 'commit')
const progressController = new ProgressController(options, this._page, this._page._timeoutSettings.navigationTimeout());
abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => {
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
progress.log(apiLog, `waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`);
const frameTask = new FrameTask(this, progress);
let documentId: string | undefined;
await Promise.race([
frameTask.waitForNewDocument(options.url).then(id => documentId = id),
frameTask.waitForSameDocumentNavigation(options.url),
]);
const request = documentId ? frameTask.request(documentId) : null;
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil);
frameTask.done();
return request ? request._finalRequest().response() : null;
frameTask.done();
return request ? request._finalRequest().response() : null;
});
}
async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> {
const frameTask = new FrameTask(this, options);
const progressController = new ProgressController(options, this._page, this._page._timeoutSettings.navigationTimeout());
abortProgressOnFrameDetach(progressController, this);
return progressController.run(progress => this._waitForLoadState(progress, state));
}
async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
const frameTask = new FrameTask(this, progress);
await frameTask.waitForLifecycle(state);
frameTask.done();
}
@ -450,8 +454,8 @@ export class Frame {
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`Unsupported waitFor option "${state}"`);
const { world, task } = selectors._waitForSelectorTask(selector, state);
return Progress.runCancelableTask(async progress => {
progress.log(dom.inputLog, `Waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}...`);
return runAbortableTask(async progress => {
progress.log(apiLog, `Waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}...`);
const result = await this._scheduleRerunnableTask(progress, world, task);
if (!result.asElement()) {
result.dispose();
@ -470,8 +474,8 @@ export class Frame {
async dispatchEvent(selector: string, type: string, eventInit?: Object, options?: types.TimeoutOptions): Promise<void> {
const task = selectors._dispatchEventTask(selector, type, eventInit || {});
return Progress.runCancelableTask(async progress => {
progress.log(dom.inputLog, `Dispatching "${type}" event on selector "${selector}"...`);
return runAbortableTask(async progress => {
progress.log(apiLog, `Dispatching "${type}" event on selector "${selector}"...`);
const result = await this._scheduleRerunnableTask(progress, 'main', task);
result.dispose();
}, options || {}, this._page, this._page._timeoutSettings);
@ -515,24 +519,31 @@ export class Frame {
});
}
async setContent(html: string, options?: types.NavigateOptions): Promise<void> {
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const context = await this._utilityContext();
const lifecyclePromise = new Promise((resolve, reject) => {
this._page._frameManager._consoleMessageTags.set(tag, () => {
// Clear lifecycle right after document.open() - see 'tag' below.
this._page._frameManager.clearFrameLifecycle(this);
this.waitForLoadState(options ? options.waitUntil : 'load', options).then(resolve).catch(reject);
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
const progressController = new ProgressController(options, this._page, this._page._timeoutSettings.navigationTimeout());
abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.log(apiLog, `${progress.apiName}(), waiting until "${waitUntil}"`);
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const context = await this._utilityContext();
const lifecyclePromise = new Promise((resolve, reject) => {
this._page._frameManager._consoleMessageTags.set(tag, () => {
progress.log(apiLog, 'content written');
// Clear lifecycle right after document.open() - see 'tag' below.
this._page._frameManager.clearFrameLifecycle(this);
this._waitForLoadState(progress, waitUntil).then(resolve).catch(reject);
});
});
const contentPromise = context.evaluateInternal(({ html, tag }) => {
window.stop();
document.open();
console.debug(tag); // eslint-disable-line no-console
document.write(html);
document.close();
}, { html, tag });
await Promise.all([contentPromise, lifecyclePromise]);
});
const contentPromise = context.evaluateInternal(({ html, tag }) => {
window.stop();
document.open();
console.debug(tag); // eslint-disable-line no-console
document.write(html);
document.close();
}, { html, tag });
await Promise.all([contentPromise, lifecyclePromise]);
}
name(): string {
@ -698,23 +709,23 @@ export class Frame {
private async _retryWithSelectorIfNotConnected<R>(
selector: string, options: types.TimeoutOptions,
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R>): Promise<R> {
return Progress.runCancelableTask(async progress => {
progress.log(dom.inputLog, `${progress.apiName}("${selector}")`);
while (!progress.isCanceled()) {
return runAbortableTask(async progress => {
progress.log(apiLog, `${progress.apiName}("${selector}")`);
while (progress.isRunning()) {
try {
const { world, task } = selectors._waitForSelectorTask(selector, 'attached');
progress.log(dom.inputLog, `waiting for the selector "${selector}"`);
progress.log(apiLog, `waiting for the selector "${selector}"`);
const handle = await this._scheduleRerunnableTask(progress, world, task);
progress.log(dom.inputLog, `...got element for the selector`);
progress.log(apiLog, `...got element for the selector`);
const element = handle.asElement() as dom.ElementHandle<Element>;
progress.cleanupWhenCanceled(() => element.dispose());
progress.cleanupWhenAborted(() => element.dispose());
const result = await action(progress, element);
element.dispose();
return result;
} catch (e) {
if (!(e instanceof NotConnectedError))
throw e;
progress.log(dom.inputLog, 'element was detached from the DOM, retrying');
progress.log(apiLog, 'element was detached from the DOM, retrying');
}
}
return undefined as any;
@ -804,7 +815,7 @@ export class Frame {
return injectedScript.poll(polling, () => innerPredicate(arg));
}, { injectedScript, predicateBody, polling, arg });
};
return Progress.runCancelableTask(
return runAbortableTask(
progress => this._scheduleRerunnableTask(progress, 'main', task),
options, this._page, this._page._timeoutSettings);
}
@ -892,11 +903,11 @@ class RerunnableTask<T> {
this._task = task;
this._progress = progress;
data.rerunnableTasks.add(this);
this.promise = progress.race(new Promise<types.SmartHandle<T>>((resolve, reject) => {
this.promise = new Promise<types.SmartHandle<T>>((resolve, reject) => {
// The task is either resolved with a value, or rejected with a meaningful evaluation error.
this._resolve = resolve;
this._reject = reject;
}));
});
}
terminate(error: Error) {
@ -948,8 +959,14 @@ export class SignalBarrier {
async addFrameNavigation(frame: Frame) {
this.retain();
const timeout = this._progress ? helper.timeUntilDeadline(this._progress.deadline) : undefined;
await frame._waitForNavigation({timeout, waitUntil: 'commit'}).catch(e => {});
const frameTask = new FrameTask(frame, this._progress);
await Promise.race([
frame._page._disconnectedPromise,
frame._detachedPromise,
frameTask.waitForNewDocument(),
frameTask.waitForSameDocumentNavigation(),
]).catch(e => {});
frameTask.done();
this.release();
}
@ -970,36 +987,32 @@ export class SignalBarrier {
export class FrameTask {
private _frame: Frame;
private _failurePromise: Promise<Error>;
private _requestMap = new Map<string, network.Request>();
private _timer?: NodeJS.Timer;
private _url: string | undefined;
private readonly _progress: Progress | null = null;
onNewDocument: (documentId: string, error?: Error) => void = () => {};
onSameDocument = () => {};
onLifecycle = () => {};
onNewDocument: (documentId: string, error?: Error) => void;
onSameDocument: (documentId?: string, error?: Error) => void;
onLifecycle: (frame: Frame, lifecycleEvent?: types.LifecycleEvent) => void;
constructor(frame: Frame, options: types.TimeoutOptions, url?: string) {
constructor(frame: Frame, progress: Progress | null) {
this._frame = frame;
this._url = url;
// Process timeouts
let timeoutPromise = new Promise<TimeoutError>(() => {});
const { timeout = frame._page._timeoutSettings.navigationTimeout() } = options;
if (timeout) {
const errorMessage = 'Navigation timeout exceeded';
timeoutPromise = new Promise(fulfill => this._timer = setTimeout(fulfill, timeout))
.then(() => { throw new TimeoutError(errorMessage); });
}
// Process detached frames
this._failurePromise = Promise.race([
timeoutPromise,
this._frame._page._disconnectedPromise.then(() => { throw new Error('Navigation failed because browser has disconnected!'); }),
this._frame._detachedPromise.then(() => { throw new Error('Navigating frame was detached!'); }),
]);
frame._frameTasks.add(this);
this._progress = progress;
this.onSameDocument = this._logUrl.bind(this);
this.onNewDocument = this._logUrl.bind(this);
this.onLifecycle = this._logLifecycle.bind(this);
if (progress)
progress.cleanupWhenAborted(() => this.done());
}
private _logUrl(documentId?: string, error?: Error) {
if (this._progress && !error)
this._progress.log(apiLog, `navigated to "${this._frame._url}"`);
}
private _logLifecycle(frame: Frame, lifecycleEvent?: types.LifecycleEvent) {
if (this._progress && frame === this._frame && lifecycleEvent && frame._url !== 'about:blank')
this._progress.log(apiLog, `"${lifecycleEvent}" event fired`);
}
onRequest(request: network.Request) {
@ -1008,38 +1021,24 @@ export class FrameTask {
this._requestMap.set(request._documentId, request);
}
async raceAgainstFailures<T>(promise: Promise<T>): Promise<T> {
let result: T;
let error: Error | undefined;
await Promise.race([
this._failurePromise.catch(e => error = e),
promise.then(r => result = r).catch(e => error = e)
]);
if (!error)
return result!;
this.done();
if (this._url)
rewriteErrorMessage(error, error.message + ` while navigating to ${this._url}`);
throw error;
}
request(documentId: string): network.Request | undefined {
return this._requestMap.get(documentId);
}
waitForSameDocumentNavigation(url?: types.URLMatch): Promise<void> {
return this.raceAgainstFailures(new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
this.onSameDocument = () => {
this._logUrl();
if (helper.urlMatches(this._frame.url(), url))
resolve();
};
}));
});
}
waitForSpecificDocument(expectedDocumentId: string): Promise<void> {
return this.raceAgainstFailures(new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
this.onNewDocument = (documentId: string, error?: Error) => {
this._logUrl(documentId, error);
if (documentId === expectedDocumentId) {
if (!error)
resolve();
@ -1049,12 +1048,13 @@ export class FrameTask {
reject(new Error('Navigation interrupted by another one'));
}
};
}));
});
}
waitForNewDocument(url?: types.URLMatch): Promise<string> {
return this.raceAgainstFailures(new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
this.onNewDocument = (documentId: string, error?: Error) => {
this._logUrl(documentId, error);
if (!error && !helper.urlMatches(this._frame.url(), url))
return;
if (error)
@ -1062,7 +1062,7 @@ export class FrameTask {
else
resolve(documentId);
};
}));
});
}
waitForLifecycle(waitUntil: types.LifecycleEvent): Promise<void> {
@ -1070,14 +1070,15 @@ export class FrameTask {
waitUntil = 'networkidle';
if (!types.kLifecycleEvents.has(waitUntil))
throw new Error(`Unsupported waitUntil option ${String(waitUntil)}`);
return this.raceAgainstFailures(new Promise((resolve, reject) => {
this.onLifecycle = () => {
return new Promise((resolve, reject) => {
this.onLifecycle = (frame: Frame, lifecycleEvent?: types.LifecycleEvent) => {
this._logLifecycle(frame, lifecycleEvent);
if (!checkLifecycleRecursively(this._frame))
return;
resolve();
};
this.onLifecycle();
}));
this.onLifecycle(this._frame);
});
function checkLifecycleRecursively(frame: Frame): boolean {
if (!frame._firedLifecycleEvents.has(waitUntil))
@ -1092,8 +1093,10 @@ export class FrameTask {
done() {
this._frame._frameTasks.delete(this);
if (this._timer)
clearTimeout(this._timer);
this._failurePromise.catch(e => {});
}
}
function abortProgressOnFrameDetach(controller: ProgressController, frame: Frame) {
frame._page._disconnectedPromise.then(() => controller.abort(new Error('Navigation failed because browser has disconnected!')));
frame._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!')));
}

View file

@ -35,6 +35,7 @@ export interface InnerLogger {
}
export const errorLog: Log = { name: 'generic', severity: 'error' };
export const apiLog: Log = { name: 'api', color: 'cyan' };
export function logError(logger: InnerLogger): (error: Error) => void {
return error => logger._log(errorLog, error, []);

View file

@ -16,97 +16,109 @@
import { InnerLogger, Log } from './logger';
import { TimeoutError } from './errors';
import { helper } from './helper';
import { helper, assert } from './helper';
import * as types from './types';
import { DEFAULT_TIMEOUT, TimeoutSettings } from './timeoutSettings';
import { getCurrentApiCall, rewriteErrorMessage } from './debug/stackTrace';
class AbortError extends Error {}
export interface Progress {
readonly apiName: string;
readonly deadline: number; // To be removed?
readonly aborted: Promise<void>;
isRunning(): boolean;
cleanupWhenAborted(cleanup: () => any): void;
log(log: Log, message: string | Error): void;
}
export class Progress {
static async runCancelableTask<T>(task: (progress: Progress) => Promise<T>, timeoutOptions: types.TimeoutOptions, logger: InnerLogger, timeoutSettings?: TimeoutSettings, apiName?: string): Promise<T> {
apiName = apiName || getCurrentApiCall();
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeoutOptions: types.TimeoutOptions, logger: InnerLogger, timeoutSettingsOrDefaultTimeout?: TimeoutSettings | number, apiName?: string): Promise<T> {
const controller = new ProgressController(timeoutOptions, logger, timeoutSettingsOrDefaultTimeout, apiName);
return controller.run(task);
}
const defaultTimeout = timeoutSettings ? timeoutSettings.timeout() : DEFAULT_TIMEOUT;
export class ProgressController {
// Promise and callback that forcefully abort the progress.
// This promise always rejects.
private _forceAbort: (error: Error) => void = () => {};
private _forceAbortPromise: Promise<any>;
// Promise and callback that resolve once the progress is aborted.
// This includes the force abort and also rejection of the task itself (failure).
private _aborted = () => {};
private _abortedPromise: Promise<void>;
// Cleanups to be run only in the case of abort.
private _cleanups: (() => any)[] = [];
private _logger: InnerLogger;
private _logRecording: string[] = [];
private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before';
private _apiName: string;
private _deadline: number;
private _timeout: number;
constructor(timeoutOptions: types.TimeoutOptions, logger: InnerLogger, timeoutSettingsOrDefaultTimeout?: TimeoutSettings | number, apiName?: string) {
this._apiName = apiName || getCurrentApiCall();
this._logger = logger;
// TODO: figure out nice timeout parameters.
let defaultTimeout = DEFAULT_TIMEOUT;
if (typeof timeoutSettingsOrDefaultTimeout === 'number')
defaultTimeout = timeoutSettingsOrDefaultTimeout;
if (timeoutSettingsOrDefaultTimeout instanceof TimeoutSettings)
defaultTimeout = timeoutSettingsOrDefaultTimeout.timeout();
const { timeout = defaultTimeout } = timeoutOptions;
const deadline = TimeoutSettings.computeDeadline(timeout);
this._timeout = timeout;
this._deadline = TimeoutSettings.computeDeadline(timeout);
let rejectCancelPromise: (error: Error) => void = () => {};
const cancelPromise = new Promise<T>((resolve, x) => rejectCancelPromise = x);
const timeoutError = new TimeoutError(`Timeout ${timeout}ms exceeded during ${apiName}.`);
const timer = setTimeout(() => rejectCancelPromise(timeoutError), helper.timeUntilDeadline(deadline));
this._forceAbortPromise = new Promise((resolve, reject) => this._forceAbort = reject);
this._forceAbortPromise.catch(e => null); // Prevent unhandle promsie rejection.
this._abortedPromise = new Promise(resolve => this._aborted = resolve);
}
let resolveCancelation = () => {};
const progress = new Progress(deadline, logger, new Promise(resolve => resolveCancelation = resolve), rejectCancelPromise, apiName);
async run<T>(task: (progress: Progress) => Promise<T>): Promise<T> {
assert(this._state === 'before');
this._state = 'running';
const progress: Progress = {
apiName: this._apiName,
deadline: this._deadline,
aborted: this._abortedPromise,
isRunning: () => this._state === 'running',
cleanupWhenAborted: (cleanup: () => any) => {
if (this._state === 'running')
this._cleanups.push(cleanup);
else
runCleanup(cleanup);
},
log: (log: Log, message: string | Error) => {
if (this._state === 'running')
this._logRecording.push(message.toString());
this._logger._log(log, message);
},
};
const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded during ${this._apiName}.`);
const timer = setTimeout(() => this._forceAbort(timeoutError), helper.timeUntilDeadline(this._deadline));
try {
const promise = task(progress);
const result = await Promise.race([promise, cancelPromise]);
const result = await Promise.race([promise, this._forceAbortPromise]);
clearTimeout(timer);
progress._running = false;
progress._logRecording = [];
this._state = 'finished';
this._logRecording = [];
return result;
} catch (e) {
resolveCancelation();
rewriteErrorMessage(e, e.message + formatLogRecording(progress._logRecording, apiName));
this._aborted();
rewriteErrorMessage(e, e.message + formatLogRecording(this._logRecording, this._apiName));
clearTimeout(timer);
progress._running = false;
progress._logRecording = [];
await Promise.all(progress._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
this._state = 'aborted';
this._logRecording = [];
await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
throw e;
}
}
readonly apiName: string;
readonly deadline: number; // To be removed?
readonly cancel: (error: Error) => void;
readonly _canceled: Promise<any>;
private _logger: InnerLogger;
private _logRecording: string[] = [];
private _cleanups: (() => any)[] = [];
private _running = true;
constructor(deadline: number, logger: InnerLogger, canceled: Promise<any>, cancel: (error: Error) => void, apiName: string) {
this.deadline = deadline;
this.apiName = apiName;
this.cancel = cancel;
this._canceled = canceled;
this._logger = logger;
}
isCanceled(): boolean {
return !this._running;
}
cleanupWhenCanceled(cleanup: () => any) {
if (this._running)
this._cleanups.push(cleanup);
else
runCleanup(cleanup);
}
throwIfCanceled() {
if (!this._running)
throw new AbortError();
}
race<T>(promise: Promise<T>, cleanup?: () => any): Promise<T> {
const canceled = this._canceled.then(async error => {
if (cleanup)
await runCleanup(cleanup);
throw error;
});
const success = promise.then(result => {
cleanup = undefined;
return result;
});
return Promise.race<T>([success, canceled]);
}
log(log: Log, message: string | Error): void {
if (this._running)
this._logRecording.push(message.toString());
this._logger._log(log, message);
abort(error: Error) {
this._forceAbort(error);
}
}

View file

@ -28,7 +28,7 @@ import { assert } from '../helper';
import { launchProcess, Env, waitForLine } from './processLauncher';
import { Events } from '../events';
import { PipeTransport } from './pipeTransport';
import { Progress } from '../progress';
import { Progress, runAbortableTask } from '../progress';
export type BrowserArgOptions = {
headless?: boolean,
@ -107,7 +107,7 @@ export abstract class BrowserTypeBase implements BrowserType {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
const logger = new RootLogger(options.logger);
const browser = await Progress.runCancelableTask(progress => this._innerLaunch(progress, options, logger, undefined), options, logger);
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, logger, undefined), options, logger);
return browser;
}
@ -115,7 +115,7 @@ export abstract class BrowserTypeBase implements BrowserType {
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
const persistent = validatePersistentContextOptions(options);
const logger = new RootLogger(options.logger);
const browser = await Progress.runCancelableTask(progress => this._innerLaunch(progress, options, logger, persistent, userDataDir), options, logger);
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, logger, persistent, userDataDir), options, logger);
return browser._defaultContext!;
}
@ -144,7 +144,7 @@ export abstract class BrowserTypeBase implements BrowserType {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launchServer`. Use `browserType.launchPersistentContext` instead');
const { port = 0 } = options;
const logger = new RootLogger(options.logger);
return Progress.runCancelableTask(async progress => {
return runAbortableTask(async progress => {
const { browserServer, transport } = await this._launchServer(progress, options, false, logger);
browserServer._webSocketWrapper = this._wrapTransportWithWebSocket(transport, logger, port);
return browserServer;
@ -153,9 +153,9 @@ export abstract class BrowserTypeBase implements BrowserType {
async connect(options: ConnectOptions): Promise<Browser> {
const logger = new RootLogger(options.logger);
return Progress.runCancelableTask(async progress => {
return runAbortableTask(async progress => {
const transport = await WebSocketTransport.connect(progress, options.wsEndpoint);
progress.cleanupWhenCanceled(() => transport.closeAndWait());
progress.cleanupWhenAborted(() => transport.closeAndWait());
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await this._connectToTransport(transport, { slowMo: options.slowMo, logger });
@ -221,7 +221,7 @@ export abstract class BrowserTypeBase implements BrowserType {
},
});
browserServer = new BrowserServer(launchedProcess, gracefullyClose, kill);
progress.cleanupWhenCanceled(() => browserServer && browserServer._closeOrKill(progress.deadline));
progress.cleanupWhenAborted(() => browserServer && browserServer._closeOrKill(progress.deadline));
if (this._webSocketNotPipe) {
const match = await waitForLine(progress, launchedProcess, this._webSocketNotPipe.stream === 'stdout' ? launchedProcess.stdout : launchedProcess.stderr, this._webSocketNotPipe.webSocketRegex);

View file

@ -30,7 +30,7 @@ import { BrowserServer } from './browserServer';
import { launchProcess, waitForLine } from './processLauncher';
import { BrowserContext } from '../browserContext';
import type {BrowserWindow} from 'electron';
import { Progress } from '../progress';
import { runAbortableTask } from '../progress';
type ElectronLaunchOptions = {
args?: string[],
@ -168,7 +168,7 @@ export class Electron {
handleSIGHUP = true,
} = options;
const logger = new RootLogger(options.logger);
return Progress.runCancelableTask(async progress => {
return runAbortableTask(async progress => {
let app: ElectronApplication | undefined = undefined;
const electronArguments = ['--inspect=0', '--remote-debugging-port=0', '--require', path.join(__dirname, 'electronLoader.js'), ...args];
const { launchedProcess, gracefullyClose, kill } = await launchProcess({

View file

@ -194,7 +194,7 @@ export function waitForLine(progress: Progress, process: childProcess.ChildProce
helper.addEventListener(process, 'error', reject)
];
progress.cleanupWhenCanceled(cleanup);
progress.cleanupWhenAborted(cleanup);
function onLine(line: string) {
const match = line.match(regex);

View file

@ -127,10 +127,15 @@ export class WebSocketTransport implements ConnectionTransport {
onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void;
static connect(progress: Progress, url: string): Promise<WebSocketTransport> {
static async connect(progress: Progress, url: string): Promise<WebSocketTransport> {
progress.log(browserLog, `<ws connecting> ${url}`);
const transport = new WebSocketTransport(progress, url);
const promise = new Promise<WebSocketTransport>((fulfill, reject) => {
let success = false;
progress.aborted.then(() => {
if (!success)
transport.closeAndWait().catch(e => null);
});
await new Promise<WebSocketTransport>((fulfill, reject) => {
transport._ws.addEventListener('open', async () => {
progress.log(browserLog, `<ws connected> ${url}`);
fulfill(transport);
@ -141,7 +146,8 @@ export class WebSocketTransport implements ConnectionTransport {
transport._ws.close();
});
});
return progress.race(promise, () => transport.closeAndWait());
success = true;
return transport;
}
constructor(progress: Progress, url: string) {

View file

@ -65,11 +65,6 @@ export type WaitForNavigationOptions = TimeoutOptions & {
url?: URLMatch
};
export type ExtendedWaitForNavigationOptions = TimeoutOptions & {
waitUntil?: LifecycleEvent | 'commit',
url?: URLMatch
};
export type ElementScreenshotOptions = {
type?: 'png' | 'jpeg',
path?: string,

View file

@ -0,0 +1 @@
<iframe src='./redirect-my-parent.html'></iframe>

View file

@ -0,0 +1,3 @@
<script>
window.parent.location = './one-frame.html';
</script>

View file

@ -187,6 +187,14 @@ describe('Auto waiting', () => {
await page.click('input[type=submit]');
await page.goto(server.EMPTY_PAGE);
});
it('should report navigation in the log when clicking anchor', async({page, server}) => {
await page.setContent(`<a href="${server.PREFIX + '/frames/one-frame.html'}">click me</a>`);
const __testHookAfterPointerAction = () => new Promise(f => setTimeout(f, 6000));
const error = await page.click('a', { timeout: 5000, __testHookAfterPointerAction }).catch(e => e);
expect(error.message).toContain('Timeout 5000ms exceeded during page.click.');
expect(error.message).toContain('waiting for scheduled navigations to finish...');
expect(error.message).toContain(`navigated to "${server.PREFIX + '/frames/one-frame.html'}"`);
});
});
describe('Auto waiting should not hang when', () => {

View file

@ -198,8 +198,8 @@ describe('Page.goto', function() {
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 exceeded';
expect(error.message).toContain(message);
expect(error.message).toContain('Timeout 1ms exceeded during page.goto.');
expect(error.message).toContain(server.PREFIX + '/empty.html');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should fail when exceeding default maximum navigation timeout', async({page, server}) => {
@ -209,8 +209,8 @@ describe('Page.goto', function() {
page.context().setDefaultNavigationTimeout(2);
page.setDefaultNavigationTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
const message = 'Navigation timeout exceeded';
expect(error.message).toContain(message);
expect(error.message).toContain('Timeout 1ms exceeded during page.goto.');
expect(error.message).toContain(server.PREFIX + '/empty.html');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should fail when exceeding browser context navigation timeout', async({page, server}) => {
@ -219,8 +219,8 @@ describe('Page.goto', function() {
let error = null;
page.context().setDefaultNavigationTimeout(2);
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
const message = 'Navigation timeout exceeded';
expect(error.message).toContain(message);
expect(error.message).toContain('Timeout 2ms exceeded during page.goto.');
expect(error.message).toContain(server.PREFIX + '/empty.html');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should fail when exceeding default maximum timeout', async({page, server}) => {
@ -230,8 +230,8 @@ describe('Page.goto', function() {
page.context().setDefaultTimeout(2);
page.setDefaultTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
const message = 'Navigation timeout exceeded';
expect(error.message).toContain(message);
expect(error.message).toContain('Timeout 1ms exceeded during page.goto.');
expect(error.message).toContain(server.PREFIX + '/empty.html');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should fail when exceeding browser context timeout', async({page, server}) => {
@ -240,8 +240,8 @@ describe('Page.goto', function() {
let error = null;
page.context().setDefaultTimeout(2);
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
const message = 'Navigation timeout exceeded';
expect(error.message).toContain(message);
expect(error.message).toContain('Timeout 2ms exceeded during page.goto.');
expect(error.message).toContain(server.PREFIX + '/empty.html');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should prioritize default navigation timeout over default timeout', async({page, server}) => {
@ -251,8 +251,8 @@ describe('Page.goto', function() {
page.setDefaultTimeout(0);
page.setDefaultNavigationTimeout(1);
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
const message = 'Navigation timeout exceeded';
expect(error.message).toContain(message);
expect(error.message).toContain('Timeout 1ms exceeded during page.goto.');
expect(error.message).toContain(server.PREFIX + '/empty.html');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should disable timeout when its set to 0', async({page, server}) => {
@ -365,7 +365,8 @@ describe('Page.goto', function() {
await page.goto(server.PREFIX + '/grid.html', {
referer: 'http://google.com/',
}).catch(e => error = e);
expect(error.message).toBe('"referer" is already specified as extra HTTP header');
expect(error.message).toContain('"referer" is already specified as extra HTTP header');
expect(error.message).toContain(server.PREFIX + '/grid.html');
});
it('should override referrer-policy', async({page, server}) => {
server.setRoute('/grid.html', (req, res) => {
@ -545,6 +546,15 @@ describe('Page.waitForNavigation', function() {
expect(response.ok()).toBe(true);
expect(response.url()).toContain('grid.html');
});
it('should respect timeout', async({page, server}) => {
const promise = page.waitForNavigation({ url: '**/frame.html', timeout: 5000 });
await page.goto(server.EMPTY_PAGE);
const error = await promise.catch(e => e);
expect(error.message).toContain('Timeout 5000ms exceeded during page.waitForNavigation.');
expect(error.message).toContain('waiting for navigation to "**/frame.html" until "load"');
expect(error.message).toContain(`navigated to "${server.EMPTY_PAGE}"`);
expect(error.message).toContain(`"load" event fired`);
});
it('should work with both domcontentloaded and load', async({page, server}) => {
let response = null;
server.setRoute('/one-style.css', (req, res) => response = res);
@ -737,7 +747,7 @@ describe('Page.waitForLoadState', () => {
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 exceeded');
expect(error.message).toContain('Timeout 1ms exceeded during page.waitForLoadState.');
});
it('should resolve immediately if loaded', async({page, server}) => {
await page.goto(server.PREFIX + '/one-style.html');
@ -904,6 +914,16 @@ describe('Frame.goto', function() {
const error = await navigationPromise;
expect(error.message).toContain('frame was detached');
});
it('should continue after client redirect', async({page, server}) => {
server.setRoute('/frames/script.js', () => {});
const url = server.PREFIX + '/frames/child-redirect.html';
const error = await page.goto(url, { timeout: 5000, waitUntil: 'networkidle' }).catch(e => e);
expect(error.message).toContain('Timeout 5000ms exceeded during page.goto.');
expect(error.message).toContain(`page.goto("${url}"), waiting until "networkidle"`);
expect(error.message).toContain(`navigated to "${url}"`);
expect(error.message).toContain(`navigated to "${server.PREFIX + '/frames/one-frame.html'}"`);
expect(error.message).toContain(`"domcontentloaded" event fired`);
});
it('should return matching responses', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
// Attach three frames.
@ -955,11 +975,12 @@ describe('Frame.waitForNavigation', function() {
frame.evaluate('window.location = "/empty.html"'),
page.evaluate('setTimeout(() => document.querySelector("iframe").remove())'),
]).catch(e => error = e);
expect(error.message).toContain('waiting for navigation until "load"');
expect(error.message).toContain('frame was detached');
});
});
describe('Frame._waitForLodState', function() {
describe('Frame.waitForLoadState', function() {
it('should work', async({page, server}) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
const frame = page.frames()[1];

View file

@ -631,8 +631,8 @@ describe('Page.setContent', function() {
const imgPath = '/img.png';
// stall for image
server.setRoute(imgPath, (req, res) => {});
let error = null;
await page.setContent(`<img src="${server.PREFIX + imgPath}"></img>`).catch(e => error = e);
const error = await page.setContent(`<img src="${server.PREFIX + imgPath}"></img>`).catch(e => e);
expect(error.message).toContain('Timeout 1ms exceeded during page.setContent.');
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
});
it('should await resources to load', async({page, server}) => {

View file

@ -220,7 +220,7 @@ class Reporter {
const lineNumber = location.lineNumber();
if (lineNumber < lines.length) {
const lineNumberLength = (lineNumber + 1 + '').length;
const FROM = Math.max(test.location().lineNumber() - 1, lineNumber - 5);
const FROM = Math.max(0, lineNumber - 5);
const snippet = lines.slice(FROM, lineNumber).map((line, index) => ` ${(FROM + index + 1 + '').padStart(lineNumberLength, ' ')} | ${line}`).join('\n');
const pointer = ` ` + ' '.repeat(lineNumberLength) + ' ' + '~'.repeat(location.columnNumber() - 1) + '^';
console.log('\n' + snippet + '\n' + colors.grey(pointer) + '\n');