chore: migrate navigations to Progress (#2463)
This commit is contained in:
parent
724d73c03b
commit
1d37a10558
85
src/dom.ts
85
src/dom.ts
|
|
@ -28,8 +28,8 @@ import { Page } from './page';
|
||||||
import { selectors } from './selectors';
|
import { selectors } from './selectors';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { NotConnectedError } from './errors';
|
import { NotConnectedError } from './errors';
|
||||||
import { Log, logError } from './logger';
|
import { logError, apiLog } from './logger';
|
||||||
import { Progress } from './progress';
|
import { Progress, runAbortableTask } from './progress';
|
||||||
|
|
||||||
export type PointerActionOptions = {
|
export type PointerActionOptions = {
|
||||||
modifiers?: input.Modifier[];
|
modifiers?: input.Modifier[];
|
||||||
|
|
@ -40,11 +40,6 @@ export type ClickOptions = PointerActionOptions & input.MouseClickOptions;
|
||||||
|
|
||||||
export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions;
|
export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions;
|
||||||
|
|
||||||
export const inputLog: Log = {
|
|
||||||
name: 'input',
|
|
||||||
color: 'cyan'
|
|
||||||
};
|
|
||||||
|
|
||||||
export class FrameExecutionContext extends js.ExecutionContext {
|
export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
readonly frame: frames.Frame;
|
readonly frame: frames.Frame;
|
||||||
private _injectedPromise?: Promise<js.JSHandle>;
|
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> {
|
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);
|
const result = await this._performPointerAction(progress, action, options);
|
||||||
if (result === 'done')
|
if (result === 'done')
|
||||||
return;
|
return;
|
||||||
|
|
@ -263,27 +258,27 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
if (!force)
|
if (!force)
|
||||||
await this._waitForDisplayedAtStablePositionAndEnabled(progress);
|
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);
|
const scrolled = await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined);
|
||||||
if (scrolled === 'invisible') {
|
if (scrolled === 'invisible') {
|
||||||
if (force)
|
if (force)
|
||||||
throw new Error('Element is not visible');
|
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';
|
return 'retry';
|
||||||
}
|
}
|
||||||
progress.log(inputLog, '...done scrolling');
|
progress.log(apiLog, '...done scrolling');
|
||||||
|
|
||||||
const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
|
const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
|
||||||
if (maybePoint === 'invisible') {
|
if (maybePoint === 'invisible') {
|
||||||
if (force)
|
if (force)
|
||||||
throw new Error('Element is not visible');
|
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';
|
return 'retry';
|
||||||
}
|
}
|
||||||
if (maybePoint === 'outsideviewport') {
|
if (maybePoint === 'outsideviewport') {
|
||||||
if (force)
|
if (force)
|
||||||
throw new Error('Element is outside of the viewport');
|
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';
|
return 'retry';
|
||||||
}
|
}
|
||||||
const point = roundPoint(maybePoint);
|
const point = roundPoint(maybePoint);
|
||||||
|
|
@ -291,33 +286,35 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
if (!force) {
|
if (!force) {
|
||||||
if ((options as any).__testHookBeforeHitTarget)
|
if ((options as any).__testHookBeforeHitTarget)
|
||||||
await (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);
|
const matchesHitTarget = await this._checkHitTargetAt(point);
|
||||||
if (!matchesHitTarget) {
|
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';
|
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 () => {
|
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||||
let restoreModifiers: input.Modifier[] | undefined;
|
let restoreModifiers: input.Modifier[] | undefined;
|
||||||
if (options && options.modifiers)
|
if (options && options.modifiers)
|
||||||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
||||||
progress.log(inputLog, `performing ${progress.apiName} action...`);
|
progress.log(apiLog, `performing ${progress.apiName} action...`);
|
||||||
await action(point);
|
await action(point);
|
||||||
progress.log(inputLog, `...${progress.apiName} action done`);
|
progress.log(apiLog, `...${progress.apiName} action done`);
|
||||||
progress.log(inputLog, 'waiting for scheduled navigations to finish...');
|
progress.log(apiLog, 'waiting for scheduled navigations to finish...');
|
||||||
|
if ((options as any).__testHookAfterPointerAction)
|
||||||
|
await (options as any).__testHookAfterPointerAction();
|
||||||
if (restoreModifiers)
|
if (restoreModifiers)
|
||||||
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
||||||
}, 'input');
|
}, 'input');
|
||||||
progress.log(inputLog, '...navigations have finished');
|
progress.log(apiLog, '...navigations have finished');
|
||||||
|
|
||||||
return 'done';
|
return 'done';
|
||||||
}
|
}
|
||||||
|
|
||||||
hover(options: PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> {
|
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> {
|
_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> {
|
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> {
|
_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> {
|
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> {
|
_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[]> {
|
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[]> {
|
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[];
|
let vals: string[] | ElementHandle[] | types.SelectOption[];
|
||||||
if (!Array.isArray(values))
|
if (!Array.isArray(values))
|
||||||
vals = [ values ] as (string[] | ElementHandle[] | types.SelectOption[]);
|
vals = [ values ] as (string[] | ElementHandle[] | types.SelectOption[]);
|
||||||
|
|
@ -369,11 +366,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
|
async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||||
return 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> {
|
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) + '"');
|
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||||
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||||
const poll = await this._evaluateHandleInUtility(([injected, node, value]) => {
|
const poll = await this._evaluateHandleInUtility(([injected, node, value]) => {
|
||||||
|
|
@ -392,17 +389,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectText(): Promise<void> {
|
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), {});
|
const injectedResult = await this._evaluateInUtility(([injected, node]) => injected.selectText(node), {});
|
||||||
handleInjectedResult(injectedResult);
|
handleInjectedResult(injectedResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) {
|
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) {
|
||||||
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) {
|
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> => {
|
const injectedResult = await this._evaluateInUtility(([injected, node]): types.InjectedScriptResult<boolean> => {
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT')
|
if (node.nodeType !== Node.ELEMENT_NODE || (node as Node as Element).tagName !== 'INPUT')
|
||||||
return { status: 'error', error: 'Node is not an HTMLInputElement' };
|
return { status: 'error', error: 'Node is not an HTMLInputElement' };
|
||||||
|
|
@ -437,17 +434,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async focus() {
|
async focus() {
|
||||||
this._page._log(inputLog, `elementHandle.focus()`);
|
this._page._log(apiLog, `elementHandle.focus()`);
|
||||||
const injectedResult = await this._evaluateInUtility(([injected, node]) => injected.focusNode(node), {});
|
const injectedResult = await this._evaluateInUtility(([injected, node]) => injected.focusNode(node), {});
|
||||||
handleInjectedResult(injectedResult);
|
handleInjectedResult(injectedResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
|
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) {
|
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 () => {
|
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||||
await this.focus();
|
await this.focus();
|
||||||
await this._page.keyboard.type(text, options);
|
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 = {}) {
|
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) {
|
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 () => {
|
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||||
await this.focus();
|
await this.focus();
|
||||||
await this._page.keyboard.press(key, options);
|
await this._page.keyboard.press(key, options);
|
||||||
|
|
@ -467,15 +464,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
||||||
return Progress.runCancelableTask(async progress => {
|
return runAbortableTask(async progress => {
|
||||||
progress.log(inputLog, `elementHandle.check()`);
|
progress.log(apiLog, `elementHandle.check()`);
|
||||||
await this._setChecked(progress, true, options);
|
await this._setChecked(progress, true, options);
|
||||||
}, options, this._page, this._page._timeoutSettings);
|
}, options, this._page, this._page._timeoutSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
||||||
return Progress.runCancelableTask(async progress => {
|
return runAbortableTask(async progress => {
|
||||||
progress.log(inputLog, `elementHandle.uncheck()`);
|
progress.log(apiLog, `elementHandle.uncheck()`);
|
||||||
await this._setChecked(progress, false, options);
|
await this._setChecked(progress, false, options);
|
||||||
}, options, this._page, this._page._timeoutSettings);
|
}, 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> {
|
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 rafCount = this._page._delegate.rafCountForStablePosition();
|
||||||
const poll = await this._evaluateHandleInUtility(([injected, node, rafCount]) => {
|
const poll = await this._evaluateHandleInUtility(([injected, node, rafCount]) => {
|
||||||
return injected.waitForDisplayedAtStablePositionAndEnabled(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);
|
new InjectedScriptPollHandler(progress, poll);
|
||||||
const injectedResult = await poll.evaluate(poll => poll.result);
|
const injectedResult = await poll.evaluate(poll => poll.result);
|
||||||
handleInjectedResult(injectedResult);
|
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> {
|
async _checkHitTargetAt(point: types.Point): Promise<boolean> {
|
||||||
|
|
@ -563,18 +560,18 @@ export class InjectedScriptPollHandler {
|
||||||
constructor(progress: Progress, poll: js.JSHandle<types.InjectedScriptPoll<any>>) {
|
constructor(progress: Progress, poll: js.JSHandle<types.InjectedScriptPoll<any>>) {
|
||||||
this._progress = progress;
|
this._progress = progress;
|
||||||
this._poll = poll;
|
this._poll = poll;
|
||||||
this._progress.cleanupWhenCanceled(() => this.cancel());
|
this._progress.cleanupWhenAborted(() => this.cancel());
|
||||||
this._streamLogs(poll.evaluateHandle(poll => poll.logs));
|
this._streamLogs(poll.evaluateHandle(poll => poll.logs));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _streamLogs(logsPromise: Promise<js.JSHandle<types.InjectedScriptLogs>>) {
|
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.
|
// We continuously get a chunk of logs, stream them to the progress and wait for the next chunk.
|
||||||
logsPromise.catch(e => null).then(logs => {
|
logsPromise.catch(e => null).then(logs => {
|
||||||
if (!logs || !this._poll || this._progress.isCanceled())
|
if (!logs || !this._poll || !this._progress.isRunning())
|
||||||
return;
|
return;
|
||||||
logs.evaluate(logs => logs.current).catch(e => [] as string[]).then(messages => {
|
logs.evaluate(logs => logs.current).catch(e => [] as string[]).then(messages => {
|
||||||
for (const message of messages)
|
for (const message of messages)
|
||||||
this._progress.log(inputLog, message);
|
this._progress.log(apiLog, message);
|
||||||
});
|
});
|
||||||
this._streamLogs(logs.evaluateHandle(logs => logs.next));
|
this._streamLogs(logs.evaluateHandle(logs => logs.next));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
293
src/frames.ts
293
src/frames.ts
|
|
@ -19,7 +19,7 @@ import * as fs from 'fs';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { ConsoleMessage } from './console';
|
import { ConsoleMessage } from './console';
|
||||||
import * as dom from './dom';
|
import * as dom from './dom';
|
||||||
import { TimeoutError, NotConnectedError } from './errors';
|
import { NotConnectedError } from './errors';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { assert, helper, RegisteredListener, assertMaxArguments, debugAssert } from './helper';
|
import { assert, helper, RegisteredListener, assertMaxArguments, debugAssert } from './helper';
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
|
|
@ -29,8 +29,8 @@ import { selectors } from './selectors';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { waitForTimeoutWasUsed } from './hints';
|
import { waitForTimeoutWasUsed } from './hints';
|
||||||
import { BrowserContext } from './browserContext';
|
import { BrowserContext } from './browserContext';
|
||||||
import { rewriteErrorMessage } from './debug/stackTrace';
|
import { Progress, ProgressController, runAbortableTask } from './progress';
|
||||||
import { Progress } from './progress';
|
import { apiLog } from './logger';
|
||||||
|
|
||||||
type ContextType = 'main' | 'utility';
|
type ContextType = 'main' | 'utility';
|
||||||
type ContextData = {
|
type ContextData = {
|
||||||
|
|
@ -111,7 +111,7 @@ export class FrameManager {
|
||||||
const barrier = new SignalBarrier(progress);
|
const barrier = new SignalBarrier(progress);
|
||||||
this._signalBarriers.add(barrier);
|
this._signalBarriers.add(barrier);
|
||||||
if (progress)
|
if (progress)
|
||||||
progress.cleanupWhenCanceled(() => this._signalBarriers.delete(barrier));
|
progress.cleanupWhenAborted(() => this._signalBarriers.delete(barrier));
|
||||||
const result = await action();
|
const result = await action();
|
||||||
if (source === 'input')
|
if (source === 'input')
|
||||||
await this._page._delegate.inputActionEpilogue();
|
await this._page._delegate.inputActionEpilogue();
|
||||||
|
|
@ -180,26 +180,18 @@ export class FrameManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
frameStoppedLoading(frameId: string) {
|
frameStoppedLoading(frameId: string) {
|
||||||
const frame = this._frames.get(frameId);
|
this.frameLifecycleEvent(frameId, 'domcontentloaded');
|
||||||
if (!frame)
|
this.frameLifecycleEvent(frameId, 'load');
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frameLifecycleEvent(frameId: string, event: types.LifecycleEvent) {
|
frameLifecycleEvent(frameId: string, event: types.LifecycleEvent) {
|
||||||
const frame = this._frames.get(frameId);
|
const frame = this._frames.get(frameId);
|
||||||
if (!frame)
|
if (!frame)
|
||||||
return;
|
return;
|
||||||
|
if (frame._firedLifecycleEvents.has(event))
|
||||||
|
return;
|
||||||
frame._firedLifecycleEvents.add(event);
|
frame._firedLifecycleEvents.add(event);
|
||||||
this._notifyLifecycle(frame);
|
this._notifyLifecycle(frame, event);
|
||||||
if (frame === this._mainFrame && event === 'load')
|
if (frame === this._mainFrame && event === 'load')
|
||||||
this._page.emit(Events.Page.Load);
|
this._page.emit(Events.Page.Load);
|
||||||
if (frame === this._mainFrame && event === 'domcontentloaded')
|
if (frame === this._mainFrame && event === 'domcontentloaded')
|
||||||
|
|
@ -261,10 +253,10 @@ export class FrameManager {
|
||||||
task.onNewDocument(documentId, new Error(error));
|
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 (let parent: Frame | null = frame; parent; parent = parent.parentFrame()) {
|
||||||
for (const frameTask of parent._frameTasks)
|
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> {
|
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
|
||||||
const headers = (this._page._state.extraHTTPHeaders || {});
|
const progressController = new ProgressController(options, this._page, this._page._timeoutSettings.navigationTimeout());
|
||||||
let referer = headers['referer'] || headers['Referer'];
|
abortProgressOnFrameDetach(progressController, this);
|
||||||
if (options.referer !== undefined) {
|
return progressController.run(async progress => {
|
||||||
if (referer !== undefined && referer !== options.referer)
|
progress.log(apiLog, `${progress.apiName}("${url}"), waiting until "${options.waitUntil || 'load'}"`);
|
||||||
throw new Error('"referer" is already specified as extra HTTP header');
|
const headers = (this._page._state.extraHTTPHeaders || {});
|
||||||
referer = options.referer;
|
let referer = headers['referer'] || headers['Referer'];
|
||||||
}
|
if (options.referer !== undefined) {
|
||||||
url = helper.completeUserURL(url);
|
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 frameTask = new FrameTask(this, progress);
|
||||||
const sameDocumentPromise = frameTask.waitForSameDocumentNavigation();
|
const sameDocumentPromise = frameTask.waitForSameDocumentNavigation();
|
||||||
const navigateResult = await frameTask.raceAgainstFailures(this._page._delegate.navigateFrame(this, url, referer)).catch(e => {
|
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer).catch(e => {
|
||||||
// Do not leave sameDocumentPromise unhandled.
|
// Do not leave sameDocumentPromise unhandled.
|
||||||
sameDocumentPromise.catch(e => {});
|
sameDocumentPromise.catch(e => {});
|
||||||
throw 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> {
|
async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> {
|
||||||
return this._waitForNavigation(options);
|
const progressController = new ProgressController(options, this._page, this._page._timeoutSettings.navigationTimeout());
|
||||||
}
|
abortProgressOnFrameDetach(progressController, this);
|
||||||
|
return progressController.run(async progress => {
|
||||||
async _waitForNavigation(options: types.ExtendedWaitForNavigationOptions = {}): Promise<network.Response | null> {
|
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
|
||||||
const frameTask = new FrameTask(this, options);
|
progress.log(apiLog, `waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`);
|
||||||
let documentId: string | undefined;
|
const frameTask = new FrameTask(this, progress);
|
||||||
await Promise.race([
|
let documentId: string | undefined;
|
||||||
frameTask.waitForNewDocument(options.url).then(id => documentId = id),
|
await Promise.race([
|
||||||
frameTask.waitForSameDocumentNavigation(options.url),
|
frameTask.waitForNewDocument(options.url).then(id => documentId = id),
|
||||||
]);
|
frameTask.waitForSameDocumentNavigation(options.url),
|
||||||
const request = documentId ? frameTask.request(documentId) : null;
|
]);
|
||||||
if (options.waitUntil !== 'commit')
|
const request = documentId ? frameTask.request(documentId) : null;
|
||||||
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil);
|
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||||
frameTask.done();
|
frameTask.done();
|
||||||
return request ? request._finalRequest().response() : null;
|
return request ? request._finalRequest().response() : null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> {
|
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);
|
await frameTask.waitForLifecycle(state);
|
||||||
frameTask.done();
|
frameTask.done();
|
||||||
}
|
}
|
||||||
|
|
@ -450,8 +454,8 @@ export class Frame {
|
||||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||||
throw new Error(`Unsupported waitFor option "${state}"`);
|
throw new Error(`Unsupported waitFor option "${state}"`);
|
||||||
const { world, task } = selectors._waitForSelectorTask(selector, state);
|
const { world, task } = selectors._waitForSelectorTask(selector, state);
|
||||||
return Progress.runCancelableTask(async progress => {
|
return runAbortableTask(async progress => {
|
||||||
progress.log(dom.inputLog, `Waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}...`);
|
progress.log(apiLog, `Waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}...`);
|
||||||
const result = await this._scheduleRerunnableTask(progress, world, task);
|
const result = await this._scheduleRerunnableTask(progress, world, task);
|
||||||
if (!result.asElement()) {
|
if (!result.asElement()) {
|
||||||
result.dispose();
|
result.dispose();
|
||||||
|
|
@ -470,8 +474,8 @@ export class Frame {
|
||||||
|
|
||||||
async dispatchEvent(selector: string, type: string, eventInit?: Object, options?: types.TimeoutOptions): Promise<void> {
|
async dispatchEvent(selector: string, type: string, eventInit?: Object, options?: types.TimeoutOptions): Promise<void> {
|
||||||
const task = selectors._dispatchEventTask(selector, type, eventInit || {});
|
const task = selectors._dispatchEventTask(selector, type, eventInit || {});
|
||||||
return Progress.runCancelableTask(async progress => {
|
return runAbortableTask(async progress => {
|
||||||
progress.log(dom.inputLog, `Dispatching "${type}" event on selector "${selector}"...`);
|
progress.log(apiLog, `Dispatching "${type}" event on selector "${selector}"...`);
|
||||||
const result = await this._scheduleRerunnableTask(progress, 'main', task);
|
const result = await this._scheduleRerunnableTask(progress, 'main', task);
|
||||||
result.dispose();
|
result.dispose();
|
||||||
}, options || {}, this._page, this._page._timeoutSettings);
|
}, options || {}, this._page, this._page._timeoutSettings);
|
||||||
|
|
@ -515,24 +519,31 @@ export class Frame {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setContent(html: string, options?: types.NavigateOptions): Promise<void> {
|
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
|
||||||
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
|
const progressController = new ProgressController(options, this._page, this._page._timeoutSettings.navigationTimeout());
|
||||||
const context = await this._utilityContext();
|
abortProgressOnFrameDetach(progressController, this);
|
||||||
const lifecyclePromise = new Promise((resolve, reject) => {
|
return progressController.run(async progress => {
|
||||||
this._page._frameManager._consoleMessageTags.set(tag, () => {
|
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
|
||||||
// Clear lifecycle right after document.open() - see 'tag' below.
|
progress.log(apiLog, `${progress.apiName}(), waiting until "${waitUntil}"`);
|
||||||
this._page._frameManager.clearFrameLifecycle(this);
|
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
|
||||||
this.waitForLoadState(options ? options.waitUntil : 'load', options).then(resolve).catch(reject);
|
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 {
|
name(): string {
|
||||||
|
|
@ -698,23 +709,23 @@ export class Frame {
|
||||||
private async _retryWithSelectorIfNotConnected<R>(
|
private async _retryWithSelectorIfNotConnected<R>(
|
||||||
selector: string, options: types.TimeoutOptions,
|
selector: string, options: types.TimeoutOptions,
|
||||||
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R>): Promise<R> {
|
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R>): Promise<R> {
|
||||||
return Progress.runCancelableTask(async progress => {
|
return runAbortableTask(async progress => {
|
||||||
progress.log(dom.inputLog, `${progress.apiName}("${selector}")`);
|
progress.log(apiLog, `${progress.apiName}("${selector}")`);
|
||||||
while (!progress.isCanceled()) {
|
while (progress.isRunning()) {
|
||||||
try {
|
try {
|
||||||
const { world, task } = selectors._waitForSelectorTask(selector, 'attached');
|
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);
|
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>;
|
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||||
progress.cleanupWhenCanceled(() => element.dispose());
|
progress.cleanupWhenAborted(() => element.dispose());
|
||||||
const result = await action(progress, element);
|
const result = await action(progress, element);
|
||||||
element.dispose();
|
element.dispose();
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof NotConnectedError))
|
if (!(e instanceof NotConnectedError))
|
||||||
throw e;
|
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;
|
return undefined as any;
|
||||||
|
|
@ -804,7 +815,7 @@ export class Frame {
|
||||||
return injectedScript.poll(polling, () => innerPredicate(arg));
|
return injectedScript.poll(polling, () => innerPredicate(arg));
|
||||||
}, { injectedScript, predicateBody, polling, arg });
|
}, { injectedScript, predicateBody, polling, arg });
|
||||||
};
|
};
|
||||||
return Progress.runCancelableTask(
|
return runAbortableTask(
|
||||||
progress => this._scheduleRerunnableTask(progress, 'main', task),
|
progress => this._scheduleRerunnableTask(progress, 'main', task),
|
||||||
options, this._page, this._page._timeoutSettings);
|
options, this._page, this._page._timeoutSettings);
|
||||||
}
|
}
|
||||||
|
|
@ -892,11 +903,11 @@ class RerunnableTask<T> {
|
||||||
this._task = task;
|
this._task = task;
|
||||||
this._progress = progress;
|
this._progress = progress;
|
||||||
data.rerunnableTasks.add(this);
|
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.
|
// The task is either resolved with a value, or rejected with a meaningful evaluation error.
|
||||||
this._resolve = resolve;
|
this._resolve = resolve;
|
||||||
this._reject = reject;
|
this._reject = reject;
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
terminate(error: Error) {
|
terminate(error: Error) {
|
||||||
|
|
@ -948,8 +959,14 @@ export class SignalBarrier {
|
||||||
|
|
||||||
async addFrameNavigation(frame: Frame) {
|
async addFrameNavigation(frame: Frame) {
|
||||||
this.retain();
|
this.retain();
|
||||||
const timeout = this._progress ? helper.timeUntilDeadline(this._progress.deadline) : undefined;
|
const frameTask = new FrameTask(frame, this._progress);
|
||||||
await frame._waitForNavigation({timeout, waitUntil: 'commit'}).catch(e => {});
|
await Promise.race([
|
||||||
|
frame._page._disconnectedPromise,
|
||||||
|
frame._detachedPromise,
|
||||||
|
frameTask.waitForNewDocument(),
|
||||||
|
frameTask.waitForSameDocumentNavigation(),
|
||||||
|
]).catch(e => {});
|
||||||
|
frameTask.done();
|
||||||
this.release();
|
this.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -970,36 +987,32 @@ export class SignalBarrier {
|
||||||
|
|
||||||
export class FrameTask {
|
export class FrameTask {
|
||||||
private _frame: Frame;
|
private _frame: Frame;
|
||||||
private _failurePromise: Promise<Error>;
|
|
||||||
private _requestMap = new Map<string, network.Request>();
|
private _requestMap = new Map<string, network.Request>();
|
||||||
private _timer?: NodeJS.Timer;
|
private readonly _progress: Progress | null = null;
|
||||||
private _url: string | undefined;
|
|
||||||
|
|
||||||
onNewDocument: (documentId: string, error?: Error) => void = () => {};
|
onNewDocument: (documentId: string, error?: Error) => void;
|
||||||
onSameDocument = () => {};
|
onSameDocument: (documentId?: string, error?: Error) => void;
|
||||||
onLifecycle = () => {};
|
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._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);
|
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) {
|
onRequest(request: network.Request) {
|
||||||
|
|
@ -1008,38 +1021,24 @@ export class FrameTask {
|
||||||
this._requestMap.set(request._documentId, request);
|
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 {
|
request(documentId: string): network.Request | undefined {
|
||||||
return this._requestMap.get(documentId);
|
return this._requestMap.get(documentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForSameDocumentNavigation(url?: types.URLMatch): Promise<void> {
|
waitForSameDocumentNavigation(url?: types.URLMatch): Promise<void> {
|
||||||
return this.raceAgainstFailures(new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.onSameDocument = () => {
|
this.onSameDocument = () => {
|
||||||
|
this._logUrl();
|
||||||
if (helper.urlMatches(this._frame.url(), url))
|
if (helper.urlMatches(this._frame.url(), url))
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForSpecificDocument(expectedDocumentId: string): Promise<void> {
|
waitForSpecificDocument(expectedDocumentId: string): Promise<void> {
|
||||||
return this.raceAgainstFailures(new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.onNewDocument = (documentId: string, error?: Error) => {
|
this.onNewDocument = (documentId: string, error?: Error) => {
|
||||||
|
this._logUrl(documentId, error);
|
||||||
if (documentId === expectedDocumentId) {
|
if (documentId === expectedDocumentId) {
|
||||||
if (!error)
|
if (!error)
|
||||||
resolve();
|
resolve();
|
||||||
|
|
@ -1049,12 +1048,13 @@ export class FrameTask {
|
||||||
reject(new Error('Navigation interrupted by another one'));
|
reject(new Error('Navigation interrupted by another one'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForNewDocument(url?: types.URLMatch): Promise<string> {
|
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.onNewDocument = (documentId: string, error?: Error) => {
|
||||||
|
this._logUrl(documentId, error);
|
||||||
if (!error && !helper.urlMatches(this._frame.url(), url))
|
if (!error && !helper.urlMatches(this._frame.url(), url))
|
||||||
return;
|
return;
|
||||||
if (error)
|
if (error)
|
||||||
|
|
@ -1062,7 +1062,7 @@ export class FrameTask {
|
||||||
else
|
else
|
||||||
resolve(documentId);
|
resolve(documentId);
|
||||||
};
|
};
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForLifecycle(waitUntil: types.LifecycleEvent): Promise<void> {
|
waitForLifecycle(waitUntil: types.LifecycleEvent): Promise<void> {
|
||||||
|
|
@ -1070,14 +1070,15 @@ export class FrameTask {
|
||||||
waitUntil = 'networkidle';
|
waitUntil = 'networkidle';
|
||||||
if (!types.kLifecycleEvents.has(waitUntil))
|
if (!types.kLifecycleEvents.has(waitUntil))
|
||||||
throw new Error(`Unsupported waitUntil option ${String(waitUntil)}`);
|
throw new Error(`Unsupported waitUntil option ${String(waitUntil)}`);
|
||||||
return this.raceAgainstFailures(new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.onLifecycle = () => {
|
this.onLifecycle = (frame: Frame, lifecycleEvent?: types.LifecycleEvent) => {
|
||||||
|
this._logLifecycle(frame, lifecycleEvent);
|
||||||
if (!checkLifecycleRecursively(this._frame))
|
if (!checkLifecycleRecursively(this._frame))
|
||||||
return;
|
return;
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
this.onLifecycle();
|
this.onLifecycle(this._frame);
|
||||||
}));
|
});
|
||||||
|
|
||||||
function checkLifecycleRecursively(frame: Frame): boolean {
|
function checkLifecycleRecursively(frame: Frame): boolean {
|
||||||
if (!frame._firedLifecycleEvents.has(waitUntil))
|
if (!frame._firedLifecycleEvents.has(waitUntil))
|
||||||
|
|
@ -1092,8 +1093,10 @@ export class FrameTask {
|
||||||
|
|
||||||
done() {
|
done() {
|
||||||
this._frame._frameTasks.delete(this);
|
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!')));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export interface InnerLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const errorLog: Log = { name: 'generic', severity: 'error' };
|
export const errorLog: Log = { name: 'generic', severity: 'error' };
|
||||||
|
export const apiLog: Log = { name: 'api', color: 'cyan' };
|
||||||
|
|
||||||
export function logError(logger: InnerLogger): (error: Error) => void {
|
export function logError(logger: InnerLogger): (error: Error) => void {
|
||||||
return error => logger._log(errorLog, error, []);
|
return error => logger._log(errorLog, error, []);
|
||||||
|
|
|
||||||
156
src/progress.ts
156
src/progress.ts
|
|
@ -16,97 +16,109 @@
|
||||||
|
|
||||||
import { InnerLogger, Log } from './logger';
|
import { InnerLogger, Log } from './logger';
|
||||||
import { TimeoutError } from './errors';
|
import { TimeoutError } from './errors';
|
||||||
import { helper } from './helper';
|
import { helper, assert } from './helper';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { DEFAULT_TIMEOUT, TimeoutSettings } from './timeoutSettings';
|
import { DEFAULT_TIMEOUT, TimeoutSettings } from './timeoutSettings';
|
||||||
import { getCurrentApiCall, rewriteErrorMessage } from './debug/stackTrace';
|
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 {
|
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeoutOptions: types.TimeoutOptions, logger: InnerLogger, timeoutSettingsOrDefaultTimeout?: TimeoutSettings | number, apiName?: string): Promise<T> {
|
||||||
static async runCancelableTask<T>(task: (progress: Progress) => Promise<T>, timeoutOptions: types.TimeoutOptions, logger: InnerLogger, timeoutSettings?: TimeoutSettings, apiName?: string): Promise<T> {
|
const controller = new ProgressController(timeoutOptions, logger, timeoutSettingsOrDefaultTimeout, apiName);
|
||||||
apiName = apiName || getCurrentApiCall();
|
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 { timeout = defaultTimeout } = timeoutOptions;
|
||||||
const deadline = TimeoutSettings.computeDeadline(timeout);
|
this._timeout = timeout;
|
||||||
|
this._deadline = TimeoutSettings.computeDeadline(timeout);
|
||||||
|
|
||||||
let rejectCancelPromise: (error: Error) => void = () => {};
|
this._forceAbortPromise = new Promise((resolve, reject) => this._forceAbort = reject);
|
||||||
const cancelPromise = new Promise<T>((resolve, x) => rejectCancelPromise = x);
|
this._forceAbortPromise.catch(e => null); // Prevent unhandle promsie rejection.
|
||||||
const timeoutError = new TimeoutError(`Timeout ${timeout}ms exceeded during ${apiName}.`);
|
this._abortedPromise = new Promise(resolve => this._aborted = resolve);
|
||||||
const timer = setTimeout(() => rejectCancelPromise(timeoutError), helper.timeUntilDeadline(deadline));
|
}
|
||||||
|
|
||||||
let resolveCancelation = () => {};
|
async run<T>(task: (progress: Progress) => Promise<T>): Promise<T> {
|
||||||
const progress = new Progress(deadline, logger, new Promise(resolve => resolveCancelation = resolve), rejectCancelPromise, apiName);
|
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 {
|
try {
|
||||||
const promise = task(progress);
|
const promise = task(progress);
|
||||||
const result = await Promise.race([promise, cancelPromise]);
|
const result = await Promise.race([promise, this._forceAbortPromise]);
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
progress._running = false;
|
this._state = 'finished';
|
||||||
progress._logRecording = [];
|
this._logRecording = [];
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
resolveCancelation();
|
this._aborted();
|
||||||
rewriteErrorMessage(e, e.message + formatLogRecording(progress._logRecording, apiName));
|
rewriteErrorMessage(e, e.message + formatLogRecording(this._logRecording, this._apiName));
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
progress._running = false;
|
this._state = 'aborted';
|
||||||
progress._logRecording = [];
|
this._logRecording = [];
|
||||||
await Promise.all(progress._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
|
await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly apiName: string;
|
abort(error: Error) {
|
||||||
readonly deadline: number; // To be removed?
|
this._forceAbort(error);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { assert } from '../helper';
|
||||||
import { launchProcess, Env, waitForLine } from './processLauncher';
|
import { launchProcess, Env, waitForLine } from './processLauncher';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import { PipeTransport } from './pipeTransport';
|
import { PipeTransport } from './pipeTransport';
|
||||||
import { Progress } from '../progress';
|
import { Progress, runAbortableTask } from '../progress';
|
||||||
|
|
||||||
export type BrowserArgOptions = {
|
export type BrowserArgOptions = {
|
||||||
headless?: boolean,
|
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).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.');
|
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||||
const logger = new RootLogger(options.logger);
|
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;
|
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.');
|
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||||
const persistent = validatePersistentContextOptions(options);
|
const persistent = validatePersistentContextOptions(options);
|
||||||
const logger = new RootLogger(options.logger);
|
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!;
|
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');
|
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launchServer`. Use `browserType.launchPersistentContext` instead');
|
||||||
const { port = 0 } = options;
|
const { port = 0 } = options;
|
||||||
const logger = new RootLogger(options.logger);
|
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);
|
const { browserServer, transport } = await this._launchServer(progress, options, false, logger);
|
||||||
browserServer._webSocketWrapper = this._wrapTransportWithWebSocket(transport, logger, port);
|
browserServer._webSocketWrapper = this._wrapTransportWithWebSocket(transport, logger, port);
|
||||||
return browserServer;
|
return browserServer;
|
||||||
|
|
@ -153,9 +153,9 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||||
|
|
||||||
async connect(options: ConnectOptions): Promise<Browser> {
|
async connect(options: ConnectOptions): Promise<Browser> {
|
||||||
const logger = new RootLogger(options.logger);
|
const logger = new RootLogger(options.logger);
|
||||||
return Progress.runCancelableTask(async progress => {
|
return runAbortableTask(async progress => {
|
||||||
const transport = await WebSocketTransport.connect(progress, options.wsEndpoint);
|
const transport = await WebSocketTransport.connect(progress, options.wsEndpoint);
|
||||||
progress.cleanupWhenCanceled(() => transport.closeAndWait());
|
progress.cleanupWhenAborted(() => transport.closeAndWait());
|
||||||
if ((options as any).__testHookBeforeCreateBrowser)
|
if ((options as any).__testHookBeforeCreateBrowser)
|
||||||
await (options as any).__testHookBeforeCreateBrowser();
|
await (options as any).__testHookBeforeCreateBrowser();
|
||||||
const browser = await this._connectToTransport(transport, { slowMo: options.slowMo, logger });
|
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);
|
browserServer = new BrowserServer(launchedProcess, gracefullyClose, kill);
|
||||||
progress.cleanupWhenCanceled(() => browserServer && browserServer._closeOrKill(progress.deadline));
|
progress.cleanupWhenAborted(() => browserServer && browserServer._closeOrKill(progress.deadline));
|
||||||
|
|
||||||
if (this._webSocketNotPipe) {
|
if (this._webSocketNotPipe) {
|
||||||
const match = await waitForLine(progress, launchedProcess, this._webSocketNotPipe.stream === 'stdout' ? launchedProcess.stdout : launchedProcess.stderr, this._webSocketNotPipe.webSocketRegex);
|
const match = await waitForLine(progress, launchedProcess, this._webSocketNotPipe.stream === 'stdout' ? launchedProcess.stdout : launchedProcess.stderr, this._webSocketNotPipe.webSocketRegex);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import { BrowserServer } from './browserServer';
|
||||||
import { launchProcess, waitForLine } from './processLauncher';
|
import { launchProcess, waitForLine } from './processLauncher';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import type {BrowserWindow} from 'electron';
|
import type {BrowserWindow} from 'electron';
|
||||||
import { Progress } from '../progress';
|
import { runAbortableTask } from '../progress';
|
||||||
|
|
||||||
type ElectronLaunchOptions = {
|
type ElectronLaunchOptions = {
|
||||||
args?: string[],
|
args?: string[],
|
||||||
|
|
@ -168,7 +168,7 @@ export class Electron {
|
||||||
handleSIGHUP = true,
|
handleSIGHUP = true,
|
||||||
} = options;
|
} = options;
|
||||||
const logger = new RootLogger(options.logger);
|
const logger = new RootLogger(options.logger);
|
||||||
return Progress.runCancelableTask(async progress => {
|
return runAbortableTask(async progress => {
|
||||||
let app: ElectronApplication | undefined = undefined;
|
let app: ElectronApplication | undefined = undefined;
|
||||||
const electronArguments = ['--inspect=0', '--remote-debugging-port=0', '--require', path.join(__dirname, 'electronLoader.js'), ...args];
|
const electronArguments = ['--inspect=0', '--remote-debugging-port=0', '--require', path.join(__dirname, 'electronLoader.js'), ...args];
|
||||||
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
|
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ export function waitForLine(progress: Progress, process: childProcess.ChildProce
|
||||||
helper.addEventListener(process, 'error', reject)
|
helper.addEventListener(process, 'error', reject)
|
||||||
];
|
];
|
||||||
|
|
||||||
progress.cleanupWhenCanceled(cleanup);
|
progress.cleanupWhenAborted(cleanup);
|
||||||
|
|
||||||
function onLine(line: string) {
|
function onLine(line: string) {
|
||||||
const match = line.match(regex);
|
const match = line.match(regex);
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,15 @@ export class WebSocketTransport implements ConnectionTransport {
|
||||||
onmessage?: (message: ProtocolResponse) => void;
|
onmessage?: (message: ProtocolResponse) => void;
|
||||||
onclose?: () => 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}`);
|
progress.log(browserLog, `<ws connecting> ${url}`);
|
||||||
const transport = new WebSocketTransport(progress, 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 () => {
|
transport._ws.addEventListener('open', async () => {
|
||||||
progress.log(browserLog, `<ws connected> ${url}`);
|
progress.log(browserLog, `<ws connected> ${url}`);
|
||||||
fulfill(transport);
|
fulfill(transport);
|
||||||
|
|
@ -141,7 +146,8 @@ export class WebSocketTransport implements ConnectionTransport {
|
||||||
transport._ws.close();
|
transport._ws.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return progress.race(promise, () => transport.closeAndWait());
|
success = true;
|
||||||
|
return transport;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(progress: Progress, url: string) {
|
constructor(progress: Progress, url: string) {
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,6 @@ export type WaitForNavigationOptions = TimeoutOptions & {
|
||||||
url?: URLMatch
|
url?: URLMatch
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExtendedWaitForNavigationOptions = TimeoutOptions & {
|
|
||||||
waitUntil?: LifecycleEvent | 'commit',
|
|
||||||
url?: URLMatch
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ElementScreenshotOptions = {
|
export type ElementScreenshotOptions = {
|
||||||
type?: 'png' | 'jpeg',
|
type?: 'png' | 'jpeg',
|
||||||
path?: string,
|
path?: string,
|
||||||
|
|
|
||||||
1
test/assets/frames/child-redirect.html
Normal file
1
test/assets/frames/child-redirect.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<iframe src='./redirect-my-parent.html'></iframe>
|
||||||
3
test/assets/frames/redirect-my-parent.html
Normal file
3
test/assets/frames/redirect-my-parent.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<script>
|
||||||
|
window.parent.location = './one-frame.html';
|
||||||
|
</script>
|
||||||
|
|
@ -187,6 +187,14 @@ describe('Auto waiting', () => {
|
||||||
await page.click('input[type=submit]');
|
await page.click('input[type=submit]');
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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', () => {
|
describe('Auto waiting should not hang when', () => {
|
||||||
|
|
|
||||||
|
|
@ -198,8 +198,8 @@ describe('Page.goto', function() {
|
||||||
server.setRoute('/empty.html', (req, res) => { });
|
server.setRoute('/empty.html', (req, res) => { });
|
||||||
let error = null;
|
let error = null;
|
||||||
await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(e => error = e);
|
await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(e => error = e);
|
||||||
const message = 'Navigation timeout exceeded';
|
expect(error.message).toContain('Timeout 1ms exceeded during page.goto.');
|
||||||
expect(error.message).toContain(message);
|
expect(error.message).toContain(server.PREFIX + '/empty.html');
|
||||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
it('should fail when exceeding default maximum navigation timeout', async({page, server}) => {
|
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.context().setDefaultNavigationTimeout(2);
|
||||||
page.setDefaultNavigationTimeout(1);
|
page.setDefaultNavigationTimeout(1);
|
||||||
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
||||||
const message = 'Navigation timeout exceeded';
|
expect(error.message).toContain('Timeout 1ms exceeded during page.goto.');
|
||||||
expect(error.message).toContain(message);
|
expect(error.message).toContain(server.PREFIX + '/empty.html');
|
||||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
it('should fail when exceeding browser context navigation timeout', async({page, server}) => {
|
it('should fail when exceeding browser context navigation timeout', async({page, server}) => {
|
||||||
|
|
@ -219,8 +219,8 @@ describe('Page.goto', function() {
|
||||||
let error = null;
|
let error = null;
|
||||||
page.context().setDefaultNavigationTimeout(2);
|
page.context().setDefaultNavigationTimeout(2);
|
||||||
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
||||||
const message = 'Navigation timeout exceeded';
|
expect(error.message).toContain('Timeout 2ms exceeded during page.goto.');
|
||||||
expect(error.message).toContain(message);
|
expect(error.message).toContain(server.PREFIX + '/empty.html');
|
||||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
it('should fail when exceeding default maximum timeout', async({page, server}) => {
|
it('should fail when exceeding default maximum timeout', async({page, server}) => {
|
||||||
|
|
@ -230,8 +230,8 @@ describe('Page.goto', function() {
|
||||||
page.context().setDefaultTimeout(2);
|
page.context().setDefaultTimeout(2);
|
||||||
page.setDefaultTimeout(1);
|
page.setDefaultTimeout(1);
|
||||||
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
||||||
const message = 'Navigation timeout exceeded';
|
expect(error.message).toContain('Timeout 1ms exceeded during page.goto.');
|
||||||
expect(error.message).toContain(message);
|
expect(error.message).toContain(server.PREFIX + '/empty.html');
|
||||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
it('should fail when exceeding browser context timeout', async({page, server}) => {
|
it('should fail when exceeding browser context timeout', async({page, server}) => {
|
||||||
|
|
@ -240,8 +240,8 @@ describe('Page.goto', function() {
|
||||||
let error = null;
|
let error = null;
|
||||||
page.context().setDefaultTimeout(2);
|
page.context().setDefaultTimeout(2);
|
||||||
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
||||||
const message = 'Navigation timeout exceeded';
|
expect(error.message).toContain('Timeout 2ms exceeded during page.goto.');
|
||||||
expect(error.message).toContain(message);
|
expect(error.message).toContain(server.PREFIX + '/empty.html');
|
||||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
it('should prioritize default navigation timeout over default timeout', async({page, server}) => {
|
it('should prioritize default navigation timeout over default timeout', async({page, server}) => {
|
||||||
|
|
@ -251,8 +251,8 @@ describe('Page.goto', function() {
|
||||||
page.setDefaultTimeout(0);
|
page.setDefaultTimeout(0);
|
||||||
page.setDefaultNavigationTimeout(1);
|
page.setDefaultNavigationTimeout(1);
|
||||||
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
await page.goto(server.PREFIX + '/empty.html').catch(e => error = e);
|
||||||
const message = 'Navigation timeout exceeded';
|
expect(error.message).toContain('Timeout 1ms exceeded during page.goto.');
|
||||||
expect(error.message).toContain(message);
|
expect(error.message).toContain(server.PREFIX + '/empty.html');
|
||||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
it('should disable timeout when its set to 0', async({page, server}) => {
|
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', {
|
await page.goto(server.PREFIX + '/grid.html', {
|
||||||
referer: 'http://google.com/',
|
referer: 'http://google.com/',
|
||||||
}).catch(e => error = e);
|
}).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}) => {
|
it('should override referrer-policy', async({page, server}) => {
|
||||||
server.setRoute('/grid.html', (req, res) => {
|
server.setRoute('/grid.html', (req, res) => {
|
||||||
|
|
@ -545,6 +546,15 @@ describe('Page.waitForNavigation', function() {
|
||||||
expect(response.ok()).toBe(true);
|
expect(response.ok()).toBe(true);
|
||||||
expect(response.url()).toContain('grid.html');
|
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}) => {
|
it('should work with both domcontentloaded and load', async({page, server}) => {
|
||||||
let response = null;
|
let response = null;
|
||||||
server.setRoute('/one-style.css', (req, res) => response = res);
|
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);
|
server.setRoute('/one-style.css', (req, res) => response = res);
|
||||||
await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'});
|
await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'});
|
||||||
const error = await page.waitForLoadState('load', { timeout: 1 }).catch(e => e);
|
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}) => {
|
it('should resolve immediately if loaded', async({page, server}) => {
|
||||||
await page.goto(server.PREFIX + '/one-style.html');
|
await page.goto(server.PREFIX + '/one-style.html');
|
||||||
|
|
@ -904,6 +914,16 @@ describe('Frame.goto', function() {
|
||||||
const error = await navigationPromise;
|
const error = await navigationPromise;
|
||||||
expect(error.message).toContain('frame was detached');
|
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}) => {
|
it('should return matching responses', async({page, server}) => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
// Attach three frames.
|
// Attach three frames.
|
||||||
|
|
@ -955,11 +975,12 @@ describe('Frame.waitForNavigation', function() {
|
||||||
frame.evaluate('window.location = "/empty.html"'),
|
frame.evaluate('window.location = "/empty.html"'),
|
||||||
page.evaluate('setTimeout(() => document.querySelector("iframe").remove())'),
|
page.evaluate('setTimeout(() => document.querySelector("iframe").remove())'),
|
||||||
]).catch(e => error = e);
|
]).catch(e => error = e);
|
||||||
|
expect(error.message).toContain('waiting for navigation until "load"');
|
||||||
expect(error.message).toContain('frame was detached');
|
expect(error.message).toContain('frame was detached');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Frame._waitForLodState', function() {
|
describe('Frame.waitForLoadState', function() {
|
||||||
it('should work', async({page, server}) => {
|
it('should work', async({page, server}) => {
|
||||||
await page.goto(server.PREFIX + '/frames/one-frame.html');
|
await page.goto(server.PREFIX + '/frames/one-frame.html');
|
||||||
const frame = page.frames()[1];
|
const frame = page.frames()[1];
|
||||||
|
|
|
||||||
|
|
@ -631,8 +631,8 @@ describe('Page.setContent', function() {
|
||||||
const imgPath = '/img.png';
|
const imgPath = '/img.png';
|
||||||
// stall for image
|
// stall for image
|
||||||
server.setRoute(imgPath, (req, res) => {});
|
server.setRoute(imgPath, (req, res) => {});
|
||||||
let error = null;
|
const error = await page.setContent(`<img src="${server.PREFIX + imgPath}"></img>`).catch(e => e);
|
||||||
await page.setContent(`<img src="${server.PREFIX + imgPath}"></img>`).catch(e => error = e);
|
expect(error.message).toContain('Timeout 1ms exceeded during page.setContent.');
|
||||||
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
|
||||||
});
|
});
|
||||||
it('should await resources to load', async({page, server}) => {
|
it('should await resources to load', async({page, server}) => {
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ class Reporter {
|
||||||
const lineNumber = location.lineNumber();
|
const lineNumber = location.lineNumber();
|
||||||
if (lineNumber < lines.length) {
|
if (lineNumber < lines.length) {
|
||||||
const lineNumberLength = (lineNumber + 1 + '').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 snippet = lines.slice(FROM, lineNumber).map((line, index) => ` ${(FROM + index + 1 + '').padStart(lineNumberLength, ' ')} | ${line}`).join('\n');
|
||||||
const pointer = ` ` + ' '.repeat(lineNumberLength) + ' ' + '~'.repeat(location.columnNumber() - 1) + '^';
|
const pointer = ` ` + ' '.repeat(lineNumberLength) + ' ' + '~'.repeat(location.columnNumber() - 1) + '^';
|
||||||
console.log('\n' + snippet + '\n' + colors.grey(pointer) + '\n');
|
console.log('\n' + snippet + '\n' + colors.grey(pointer) + '\n');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue