chore(logs): rework logs for simplicity (#2592)

This commit is contained in:
Pavel Feldman 2020-06-16 17:11:19 -07:00 committed by GitHub
parent 4b2efd6e3e
commit c220fc7f46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 386 additions and 365 deletions

View file

@ -22,7 +22,7 @@ export { Dialog } from './dialog';
export { Download } from './download'; export { Download } from './download';
export { ElementHandle } from './dom'; export { ElementHandle } from './dom';
export { FileChooser } from './fileChooser'; export { FileChooser } from './fileChooser';
export { Logger } from './logger'; export { Logger } from './types';
export { TimeoutError } from './errors'; export { TimeoutError } from './errors';
export { Frame } from './frames'; export { Frame } from './frames';
export { Keyboard, Mouse } from './input'; export { Keyboard, Mouse } from './input';

View file

@ -20,11 +20,11 @@ import { EventEmitter } from 'events';
import { Download } from './download'; import { Download } from './download';
import type { BrowserServer } from './server/browserServer'; import type { BrowserServer } from './server/browserServer';
import { Events } from './events'; import { Events } from './events';
import { InnerLogger } from './logger'; import { Loggers } from './logger';
import { ProxySettings } from './types'; import { ProxySettings } from './types';
export type BrowserOptions = { export type BrowserOptions = {
logger: InnerLogger, loggers: Loggers,
downloadsPath?: string, downloadsPath?: string,
headful?: boolean, headful?: boolean,
persistent?: PersistentContextOptions, // Undefined means no persistent context. persistent?: PersistentContextOptions, // Undefined means no persistent context.

View file

@ -25,7 +25,7 @@ import * as types from './types';
import { Events } from './events'; import { Events } from './events';
import { Download } from './download'; import { Download } from './download';
import { BrowserBase } from './browser'; import { BrowserBase } from './browser';
import { InnerLogger, Logger } from './logger'; import { Loggers, Logger } from './logger';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { ProgressController } from './progress'; import { ProgressController } from './progress';
import { DebugController } from './debug/debugController'; import { DebugController } from './debug/debugController';
@ -52,7 +52,7 @@ type CommonContextOptions = {
export type PersistentContextOptions = CommonContextOptions; export type PersistentContextOptions = CommonContextOptions;
export type BrowserContextOptions = CommonContextOptions & { export type BrowserContextOptions = CommonContextOptions & {
logger?: Logger, logger?: types.Logger,
}; };
export interface BrowserContext { export interface BrowserContext {
@ -89,14 +89,15 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
readonly _permissions = new Map<string, string[]>(); readonly _permissions = new Map<string, string[]>();
readonly _downloads = new Set<Download>(); readonly _downloads = new Set<Download>();
readonly _browserBase: BrowserBase; readonly _browserBase: BrowserBase;
readonly _logger: InnerLogger; readonly _apiLogger: Logger;
private _debugController: DebugController | undefined; private _debugController: DebugController | undefined;
constructor(browserBase: BrowserBase, options: BrowserContextOptions) { constructor(browserBase: BrowserBase, options: BrowserContextOptions) {
super(); super();
this._browserBase = browserBase; this._browserBase = browserBase;
this._options = options; this._options = options;
this._logger = options.logger ? new InnerLogger(options.logger) : browserBase._options.logger; const loggers = options.logger ? new Loggers(options.logger) : browserBase._options.loggers;
this._apiLogger = loggers.api;
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill); this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
} }
@ -115,7 +116,7 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate; const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options)); const progressController = new ProgressController(this._apiLogger, this._timeoutSettings.timeout(options), 'browserContext.waitForEvent');
if (event !== Events.BrowserContext.Close) if (event !== Events.BrowserContext.Close)
this._closePromise.then(error => progressController.abort(error)); this._closePromise.then(error => progressController.abort(error));
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate)); return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate));

View file

@ -46,7 +46,7 @@ export class CRBrowser extends BrowserBase {
private _tracingClient: CRSession | undefined; private _tracingClient: CRSession | undefined;
static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> { static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> {
const connection = new CRConnection(SlowMoTransport.wrap(transport, options.slowMo), options.logger); const connection = new CRConnection(SlowMoTransport.wrap(transport, options.slowMo), options.loggers);
const browser = new CRBrowser(connection, options); const browser = new CRBrowser(connection, options);
browser._devtools = devtools; browser._devtools = devtools;
const session = connection.rootSession; const session = connection.rootSession;

View file

@ -16,10 +16,10 @@
*/ */
import { assert } from '../helper'; import { assert } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { InnerLogger, errorLog } from '../logger'; import { Loggers, Logger } from '../logger';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
export const ConnectionEvents = { export const ConnectionEvents = {
@ -36,12 +36,12 @@ export class CRConnection extends EventEmitter {
private readonly _sessions = new Map<string, CRSession>(); private readonly _sessions = new Map<string, CRSession>();
readonly rootSession: CRSession; readonly rootSession: CRSession;
_closed = false; _closed = false;
readonly _logger: InnerLogger; readonly _logger: Logger;
constructor(transport: ConnectionTransport, logger: InnerLogger) { constructor(transport: ConnectionTransport, loggers: Loggers) {
super(); super();
this._transport = transport; this._transport = transport;
this._logger = logger; this._logger = loggers.protocol;
this._transport.onmessage = this._onMessage.bind(this); this._transport.onmessage = this._onMessage.bind(this);
this._transport.onclose = this._onClose.bind(this); this._transport.onclose = this._onClose.bind(this);
this.rootSession = new CRSession(this, '', 'browser', ''); this.rootSession = new CRSession(this, '', 'browser', '');
@ -62,15 +62,15 @@ export class CRConnection extends EventEmitter {
const message: ProtocolRequest = { id, method, params }; const message: ProtocolRequest = { id, method, params };
if (sessionId) if (sessionId)
message.sessionId = sessionId; message.sessionId = sessionId;
if (this._logger.isLogEnabled(protocolLog)) if (this._logger.isEnabled())
this._logger.log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); this._logger.info('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
this._transport.send(message); this._transport.send(message);
return id; return id;
} }
async _onMessage(message: ProtocolResponse) { async _onMessage(message: ProtocolResponse) {
if (this._logger.isLogEnabled(protocolLog)) if (this._logger.isEnabled())
this._logger.log(protocolLog, '◀ RECV ' + JSON.stringify(message)); this._logger.info('◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.method === 'Target.attachedToTarget') { if (message.method === 'Target.attachedToTarget') {
@ -166,9 +166,9 @@ export class CRSession extends EventEmitter {
} }
_sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> { _sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
return this.send(method, params).catch(error => { return this.send(method, params).catch((error: Error) => {
if (this._connection) if (this._connection)
this._connection._logger.log(errorLog, error, []); this._connection._logger.error(error);
}); });
} }

View file

@ -28,8 +28,7 @@ import * as js from './javascript';
import { Page } from './page'; import { Page } from './page';
import { selectors } from './selectors'; import { selectors } from './selectors';
import * as types from './types'; import * as types from './types';
import { apiLog } from './logger'; import { Progress, ProgressController } from './progress';
import { Progress, runAbortableTask } from './progress';
import DebugScript from './debug/injected/debugScript'; import DebugScript from './debug/injected/debugScript';
export type PointerActionOptions = { export type PointerActionOptions = {
@ -120,6 +119,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
this._initializePreview().catch(e => {}); this._initializePreview().catch(e => {});
} }
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
const controller = new ProgressController(this._page._logger, timeout, `elementHandle.${apiName}`);
return controller.run(task);
}
async _initializePreview() { async _initializePreview() {
const utility = await this._context.injectedScript(); const utility = await this._context.injectedScript();
this._preview = await utility.evaluate((injected, e) => 'JSHandle@' + injected.previewNode(e), this); this._preview = await utility.evaluate((injected, e) => 'JSHandle@' + injected.previewNode(e), this);
@ -268,25 +272,25 @@ 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<'notconnected' | 'done'> { async _retryPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
let first = true; let first = true;
while (progress.isRunning()) { while (progress.isRunning()) {
progress.log(apiLog, `${first ? 'attempting' : 'retrying'} ${progress.apiName} action`); progress.logger.info(`${first ? 'attempting' : 'retrying'} ${progress.apiName} action`);
const result = await this._performPointerAction(progress, action, options); const result = await this._performPointerAction(progress, action, options);
first = false; first = false;
if (result === 'notvisible') { if (result === 'notvisible') {
if (options.force) if (options.force)
throw new Error('Element is not visible'); throw new Error('Element is not visible');
progress.log(apiLog, ' element is not visible'); progress.logger.info(' element is not visible');
continue; continue;
} }
if (result === 'notinviewport') { if (result === 'notinviewport') {
if (options.force) if (options.force)
throw new Error('Element is outside of the viewport'); throw new Error('Element is outside of the viewport');
progress.log(apiLog, ' element is outside of the viewport'); progress.logger.info(' element is outside of the viewport');
continue; continue;
} }
if (result === 'nothittarget') { if (result === 'nothittarget') {
if (options.force) if (options.force)
throw new Error('Element does not receive pointer events'); throw new Error('Element does not receive pointer events');
progress.log(apiLog, ' element does not receive pointer events'); progress.logger.info(' element does not receive pointer events');
continue; continue;
} }
if (result === 'notconnected') if (result === 'notconnected')
@ -306,14 +310,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if ((options as any).__testHookAfterStable) if ((options as any).__testHookAfterStable)
await (options as any).__testHookAfterStable(); await (options as any).__testHookAfterStable();
progress.log(apiLog, ' scrolling into view if needed'); progress.logger.info(' scrolling into view if needed');
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
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 === 'notvisible') if (scrolled === 'notvisible')
return 'notvisible'; return 'notvisible';
if (scrolled === 'notconnected') if (scrolled === 'notconnected')
return 'notconnected'; return 'notconnected';
progress.log(apiLog, ' done scrolling'); progress.logger.info(' done scrolling');
const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint(); const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
if (maybePoint === 'notvisible') if (maybePoint === 'notvisible')
@ -325,13 +329,13 @@ 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(apiLog, ` checking that element receives pointer events at (${point.x},${point.y})`); progress.logger.info(` checking that element receives pointer events at (${point.x},${point.y})`);
const hitTargetResult = await this._checkHitTargetAt(point); const hitTargetResult = await this._checkHitTargetAt(point);
if (hitTargetResult === 'notconnected') if (hitTargetResult === 'notconnected')
return 'notconnected'; return 'notconnected';
if (hitTargetResult === 'nothittarget') if (hitTargetResult === 'nothittarget')
return 'nothittarget'; return 'nothittarget';
progress.log(apiLog, ` element does receive pointer events`); progress.logger.info(` element does receive pointer events`);
} }
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
@ -341,24 +345,24 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
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(apiLog, ` performing ${progress.apiName} action`); progress.logger.info(` performing ${progress.apiName} action`);
await action(point); await action(point);
progress.log(apiLog, ` ${progress.apiName} action done`); progress.logger.info(` ${progress.apiName} action done`);
progress.log(apiLog, ' waiting for scheduled navigations to finish'); progress.logger.info(' waiting for scheduled navigations to finish');
if ((options as any).__testHookAfterPointerAction) if ((options as any).__testHookAfterPointerAction)
await (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(apiLog, ' navigations have finished'); progress.logger.info(' navigations have finished');
return 'done'; return 'done';
} }
hover(options: PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> { hover(options: PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> {
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
throwIfNotConnected(await this._hover(progress, options)); throwIfNotConnected(await this._hover(progress, options));
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'hover');
} }
_hover(progress: Progress, options: PointerActionOptions & types.PointerActionWaitOptions): Promise<'notconnected' | 'done'> { _hover(progress: Progress, options: PointerActionOptions & types.PointerActionWaitOptions): Promise<'notconnected' | 'done'> {
@ -366,9 +370,9 @@ 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 runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
throwIfNotConnected(await this._click(progress, options)); throwIfNotConnected(await this._click(progress, options));
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'click');
} }
_click(progress: Progress, options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> { _click(progress: Progress, options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
@ -376,9 +380,9 @@ 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 runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
throwIfNotConnected(await this._dblclick(progress, options)); throwIfNotConnected(await this._dblclick(progress, options));
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'dblclick');
} }
_dblclick(progress: Progress, options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> { _dblclick(progress: Progress, options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
@ -386,9 +390,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> { async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
return throwIfNotConnected(await this._selectOption(progress, values, options)); return throwIfNotConnected(await this._selectOption(progress, values, options));
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'selectOption');
} }
async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions): Promise<string[] | 'notconnected'> { async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions): Promise<string[] | 'notconnected'> {
@ -419,16 +423,16 @@ 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 runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
throwIfNotConnected(await this._fill(progress, value, options)); throwIfNotConnected(await this._fill(progress, value, options));
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'fill');
} }
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> { async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
progress.log(apiLog, `elementHandle.fill("${value}")`); progress.logger.info(`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) + '"');
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.log(apiLog, ' waiting for element to be visible, enabled and editable'); progress.logger.info(' waiting for element to be visible, enabled and editable');
const poll = await this._evaluateHandleInUtility(([injected, node, value]) => { const poll = await this._evaluateHandleInUtility(([injected, node, value]) => {
return injected.waitForEnabledAndFill(node, value); return injected.waitForEnabledAndFill(node, value);
}, value); }, value);
@ -437,7 +441,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
if (filled === 'notconnected') if (filled === 'notconnected')
return 'notconnected'; return 'notconnected';
progress.log(apiLog, ' element is visible, enabled and editable'); progress.logger.info(' element is visible, enabled and editable');
if (filled === 'needsinput') { if (filled === 'needsinput') {
if (value) if (value)
await this._page.keyboard.insertText(value); await this._page.keyboard.insertText(value);
@ -449,17 +453,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async selectText(): Promise<void> { async selectText(): Promise<void> {
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
const selected = throwIfError(await this._evaluateInUtility(([injected, node]) => injected.selectText(node), {})); const selected = throwIfError(await this._evaluateInUtility(([injected, node]) => injected.selectText(node), {}));
throwIfNotConnected(selected); throwIfNotConnected(selected);
}, this._page._logger, 0); }, 0, 'selectText');
} }
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) { async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) {
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
throwIfNotConnected(await this._setInputFiles(progress, files, options)); throwIfNotConnected(await this._setInputFiles(progress, files, options));
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'setInputFiles');
} }
async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> { async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
@ -500,9 +504,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async focus(): Promise<void> { async focus(): Promise<void> {
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
throwIfNotConnected(await this._focus(progress)); throwIfNotConnected(await this._focus(progress));
}, this._page._logger, 0); }, 0, 'focus');
} }
async _focus(progress: Progress): Promise<'notconnected' | 'done'> { async _focus(progress: Progress): Promise<'notconnected' | 'done'> {
@ -511,13 +515,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> { async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> {
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
throwIfNotConnected(await this._type(progress, text, options)); throwIfNotConnected(await this._type(progress, text, options));
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'type');
} }
async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> { async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
progress.log(apiLog, `elementHandle.type("${text}")`); progress.logger.info(`elementHandle.type("${text}")`);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
const focused = await this._focus(progress); const focused = await this._focus(progress);
if (focused === 'notconnected') if (focused === 'notconnected')
@ -529,13 +533,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> { async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> {
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
throwIfNotConnected(await this._press(progress, key, options)); throwIfNotConnected(await this._press(progress, key, options));
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'press');
} }
async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> { async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
progress.log(apiLog, `elementHandle.press("${key}")`); progress.logger.info(`elementHandle.press("${key}")`);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
const focused = await this._focus(progress); const focused = await this._focus(progress);
if (focused === 'notconnected') if (focused === 'notconnected')
@ -547,11 +551,11 @@ 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 runAbortableTask(progress => this._setChecked(progress, true, options), this._page._logger, this._page._timeoutSettings.timeout(options)); return this._runAbortableTask(progress => this._setChecked(progress, true, options), this._page._timeoutSettings.timeout(options), 'check');
} }
async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
return runAbortableTask(progress => this._setChecked(progress, false, options), this._page._logger, this._page._timeoutSettings.timeout(options)); return this._runAbortableTask(progress => this._setChecked(progress, false, options), this._page._timeoutSettings.timeout(options), 'uncheck');
} }
async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
@ -599,14 +603,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise<'notconnected' | 'done'> { async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise<'notconnected' | 'done'> {
progress.log(apiLog, ' waiting for element to be visible, enabled and not moving'); progress.logger.info(' waiting for element to be visible, enabled and not moving');
const rafCount = this._page._delegate.rafCountForStablePosition(); const rafCount = this._page._delegate.rafCountForStablePosition();
const poll = this._evaluateHandleInUtility(([injected, node, rafCount]) => { const poll = this._evaluateHandleInUtility(([injected, node, rafCount]) => {
return injected.waitForDisplayedAtStablePositionAndEnabled(node, rafCount); return injected.waitForDisplayedAtStablePositionAndEnabled(node, rafCount);
}, rafCount); }, rafCount);
const pollHandler = new InjectedScriptPollHandler(progress, await poll); const pollHandler = new InjectedScriptPollHandler(progress, await poll);
const result = throwIfError(await pollHandler.finish()); const result = throwIfError(await pollHandler.finish());
progress.log(apiLog, ' element is visible, enabled and does not move'); progress.logger.info(' element is visible, enabled and does not move');
return result; return result;
} }
@ -651,7 +655,7 @@ export class InjectedScriptPollHandler<T> {
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(apiLog, message); this._progress.logger.info(message);
}); });
this._streamLogs(logs.evaluateHandle(logs => logs.next)); this._streamLogs(logs.evaluateHandle(logs => logs.next));
}); });
@ -683,7 +687,7 @@ export class InjectedScriptPollHandler<T> {
// Retrieve all the logs before continuing. // Retrieve all the logs before continuing.
const messages = await this._poll.evaluate(poll => poll.takeLastLogs()).catch(e => [] as string[]); const messages = await this._poll.evaluate(poll => poll.takeLastLogs()).catch(e => [] as string[]);
for (const message of messages) for (const message of messages)
this._progress.log(apiLog, message); this._progress.logger.info(message);
} }
async cancel() { async cancel() {

View file

@ -35,7 +35,7 @@ export class FFBrowser extends BrowserBase {
private _eventListeners: RegisteredListener[]; private _eventListeners: RegisteredListener[];
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> { static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
const connection = new FFConnection(SlowMoTransport.wrap(transport, options.slowMo), options.logger); const connection = new FFConnection(SlowMoTransport.wrap(transport, options.slowMo), options.loggers);
const browser = new FFBrowser(connection, options); const browser = new FFBrowser(connection, options);
const promises: Promise<any>[] = [ const promises: Promise<any>[] = [
connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent }), connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent }),

View file

@ -17,9 +17,9 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { assert } from '../helper'; import { assert } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { InnerLogger, errorLog } from '../logger'; import { Loggers, Logger } from '../logger';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
export const ConnectionEvents = { export const ConnectionEvents = {
@ -34,7 +34,7 @@ export class FFConnection extends EventEmitter {
private _lastId: number; private _lastId: number;
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>; private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
private _transport: ConnectionTransport; private _transport: ConnectionTransport;
readonly _logger: InnerLogger; readonly _logger: Logger;
readonly _sessions: Map<string, FFSession>; readonly _sessions: Map<string, FFSession>;
_closed: boolean; _closed: boolean;
@ -44,10 +44,10 @@ export class FFConnection extends EventEmitter {
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
constructor(transport: ConnectionTransport, logger: InnerLogger) { constructor(transport: ConnectionTransport, loggers: Loggers) {
super(); super();
this._transport = transport; this._transport = transport;
this._logger = logger; this._logger = loggers.protocol;
this._lastId = 0; this._lastId = 0;
this._callbacks = new Map(); this._callbacks = new Map();
@ -79,14 +79,14 @@ export class FFConnection extends EventEmitter {
} }
_rawSend(message: ProtocolRequest) { _rawSend(message: ProtocolRequest) {
if (this._logger.isLogEnabled(protocolLog)) if (this._logger.isEnabled())
this._logger.log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); this._logger.info('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
this._transport.send(message); this._transport.send(message);
} }
async _onMessage(message: ProtocolResponse) { async _onMessage(message: ProtocolResponse) {
if (this._logger.isLogEnabled(protocolLog)) if (this._logger.isEnabled())
this._logger.log(protocolLog, '◀ RECV ' + JSON.stringify(message)); this._logger.info('◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.sessionId) { if (message.sessionId) {
@ -187,7 +187,7 @@ export class FFSession extends EventEmitter {
sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> { sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
return this.send(method, params).catch(error => { return this.send(method, params).catch(error => {
this._connection._logger.log(errorLog, error, []); this._connection._logger.error(error);
}); });
} }

View file

@ -26,10 +26,8 @@ import * as network from './network';
import { Page } from './page'; import { Page } from './page';
import { selectors } from './selectors'; import { selectors } from './selectors';
import * as types from './types'; import * as types from './types';
import { waitForTimeoutWasUsed } from './hints';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
import { Progress, ProgressController, runAbortableTask } from './progress'; import { Progress, ProgressController } from './progress';
import { apiLog } from './logger';
type ContextType = 'main' | 'utility'; type ContextType = 'main' | 'utility';
type ContextData = { type ContextData = {
@ -340,11 +338,21 @@ export class Frame {
this._parentFrame._childFrames.add(this); this._parentFrame._childFrames.add(this);
} }
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
const controller = new ProgressController(this._page._logger, timeout, this._apiName(apiName));
return controller.run(task);
}
private _apiName(method: string) {
const subject = this._page._callingPageAPI ? 'page' : 'frame';
return `${subject}.${method}`;
}
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> { async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options)); const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('goto'));
abortProgressOnFrameDetach(progressController, this); abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => { return progressController.run(async progress => {
progress.log(apiLog, `navigating to "${url}", waiting until "${options.waitUntil || 'load'}"`); progress.logger.info(`navigating to "${url}", waiting until "${options.waitUntil || 'load'}"`);
const headers = (this._page._state.extraHTTPHeaders || {}); const headers = (this._page._state.extraHTTPHeaders || {});
let referer = headers['referer'] || headers['Referer']; let referer = headers['referer'] || headers['Referer'];
if (options.referer !== undefined) { if (options.referer !== undefined) {
@ -376,11 +384,11 @@ export class Frame {
} }
async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> { async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options)); const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('waitForNavigation'));
abortProgressOnFrameDetach(progressController, this); abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => { return progressController.run(async progress => {
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
progress.log(apiLog, `waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`); progress.logger.info(`waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`);
const frameTask = new FrameTask(this, progress); const frameTask = new FrameTask(this, progress);
let documentId: string | undefined; let documentId: string | undefined;
await Promise.race([ await Promise.race([
@ -395,7 +403,7 @@ export class Frame {
} }
async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> { async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options)); const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('waitForLoadState'));
abortProgressOnFrameDetach(progressController, this); abortProgressOnFrameDetach(progressController, this);
return progressController.run(progress => this._waitForLoadState(progress, state)); return progressController.run(progress => this._waitForLoadState(progress, state));
} }
@ -453,8 +461,8 @@ export class Frame {
if (!['attached', 'detached', 'visible', 'hidden'].includes(state)) if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`Unsupported state option "${state}"`); throw new Error(`Unsupported state option "${state}"`);
const { world, task } = selectors._waitForSelectorTask(selector, state); const { world, task } = selectors._waitForSelectorTask(selector, state);
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
progress.log(apiLog, `waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`); progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
const result = await this._scheduleRerunnableTask(progress, world, task); const result = await this._scheduleRerunnableTask(progress, world, task);
if (!result.asElement()) { if (!result.asElement()) {
result.dispose(); result.dispose();
@ -468,16 +476,16 @@ export class Frame {
return adopted; return adopted;
} }
return handle; return handle;
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'waitForSelector');
} }
async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> { async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
const task = selectors._dispatchEventTask(selector, type, eventInit || {}); const task = selectors._dispatchEventTask(selector, type, eventInit || {});
return runAbortableTask(async progress => { return this._runAbortableTask(async progress => {
progress.log(apiLog, `Dispatching "${type}" event on selector "${selector}"...`); progress.logger.info(`Dispatching "${type}" event on selector "${selector}"...`);
const result = await this._scheduleRerunnableTask(progress, 'main', task); const result = await this._scheduleRerunnableTask(progress, 'main', task);
result.dispose(); result.dispose();
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), 'dispatchEvent');
} }
async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>; async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
@ -519,11 +527,11 @@ export class Frame {
} }
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> { async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options)); const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('setContent'));
abortProgressOnFrameDetach(progressController, this); abortProgressOnFrameDetach(progressController, this);
return progressController.run(async progress => { return progressController.run(async progress => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil; const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.log(apiLog, `setting frame content, waiting until "${waitUntil}"`); progress.logger.info(`setting frame content, waiting until "${waitUntil}"`);
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`; const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const context = await this._utilityContext(); const context = await this._utilityContext();
const lifecyclePromise = new Promise((resolve, reject) => { const lifecyclePromise = new Promise((resolve, reject) => {
@ -706,10 +714,11 @@ 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 | 'notconnected'>): Promise<R> { action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'notconnected'>,
return runAbortableTask(async progress => { apiName: string): Promise<R> {
return this._runAbortableTask(async progress => {
while (progress.isRunning()) { while (progress.isRunning()) {
progress.log(apiLog, `waiting for selector "${selector}"`); progress.logger.info(`waiting for selector "${selector}"`);
const { world, task } = selectors._waitForSelectorTask(selector, 'attached'); const { world, task } = selectors._waitForSelectorTask(selector, 'attached');
const handle = await this._scheduleRerunnableTask(progress, world, task); const handle = await this._scheduleRerunnableTask(progress, world, task);
const element = handle.asElement() as dom.ElementHandle<Element>; const element = handle.asElement() as dom.ElementHandle<Element>;
@ -717,77 +726,76 @@ export class Frame {
const result = await action(progress, element); const result = await action(progress, element);
element.dispose(); element.dispose();
if (result === 'notconnected') { if (result === 'notconnected') {
progress.log(apiLog, 'element was detached from the DOM, retrying'); progress.logger.info('element was detached from the DOM, retrying');
continue; continue;
} }
return result; return result;
} }
return undefined as any; return undefined as any;
}, this._page._logger, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options), apiName);
} }
async click(selector: string, options: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async click(selector: string, options: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._click(progress, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._click(progress, options), 'click');
} }
async dblclick(selector: string, options: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async dblclick(selector: string, options: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._dblclick(progress, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._dblclick(progress, options), 'dblclick');
} }
async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) { async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._fill(progress, value, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._fill(progress, value, options), 'fill');
} }
async focus(selector: string, options: types.TimeoutOptions = {}) { async focus(selector: string, options: types.TimeoutOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress), 'focus');
} }
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<null|string> { async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<null|string> {
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.textContent()); return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.textContent(), 'textContent');
} }
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> { async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerText()); return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerText(), 'innerText');
} }
async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> { async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerHTML()); return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerHTML(), 'innerHTML');
} }
async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> { async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.getAttribute(name)); return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.getAttribute(name), 'getAttribute');
} }
async hover(selector: string, options: dom.PointerActionOptions & types.PointerActionWaitOptions = {}) { async hover(selector: string, options: dom.PointerActionOptions & types.PointerActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._hover(progress, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._hover(progress, options), 'hover');
} }
async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> { async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
return this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._selectOption(progress, values, options)); return this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._selectOption(progress, values, options), 'selectOption');
} }
async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> { async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setInputFiles(progress, files, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setInputFiles(progress, files, options), 'setInputFiles');
} }
async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._type(progress, text, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._type(progress, text, options), 'type');
} }
async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._press(progress, key, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._press(progress, key, options), 'press');
} }
async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, true, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, true, options), 'check');
} }
async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, false, options)); await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, false, options), 'uncheck');
} }
async waitForTimeout(timeout: number) { async waitForTimeout(timeout: number) {
waitForTimeoutWasUsed(this._page);
await new Promise(fulfill => setTimeout(fulfill, timeout)); await new Promise(fulfill => setTimeout(fulfill, timeout));
} }
@ -809,9 +817,9 @@ export class Frame {
return injectedScript.poll(polling, () => innerPredicate(arg)); return injectedScript.poll(polling, () => innerPredicate(arg));
}, { injectedScript, predicateBody, polling, arg }); }, { injectedScript, predicateBody, polling, arg });
}; };
return runAbortableTask( return this._runAbortableTask(
progress => this._scheduleRerunnableTask(progress, 'main', task), progress => this._scheduleRerunnableTask(progress, 'main', task),
this._page._logger, this._page._timeoutSettings.timeout(options)); this._page._timeoutSettings.timeout(options), 'waitForFunction');
} }
async title(): Promise<string> { async title(): Promise<string> {
@ -999,14 +1007,14 @@ class FrameTask {
onSameDocument() { onSameDocument() {
if (this._progress) if (this._progress)
this._progress.log(apiLog, `navigated to "${this._frame._url}"`); this._progress.logger.info(`navigated to "${this._frame._url}"`);
if (this._onSameDocument && helper.urlMatches(this._frame.url(), this._onSameDocument.url)) if (this._onSameDocument && helper.urlMatches(this._frame.url(), this._onSameDocument.url))
this._onSameDocument.resolve(); this._onSameDocument.resolve();
} }
onNewDocument(documentId: string, error?: Error) { onNewDocument(documentId: string, error?: Error) {
if (this._progress && !error) if (this._progress && !error)
this._progress.log(apiLog, `navigated to "${this._frame._url}"`); this._progress.logger.info(`navigated to "${this._frame._url}"`);
if (this._onSpecificDocument) { if (this._onSpecificDocument) {
if (documentId === this._onSpecificDocument.expectedDocumentId) { if (documentId === this._onSpecificDocument.expectedDocumentId) {
if (error) if (error)
@ -1027,7 +1035,7 @@ class FrameTask {
onLifecycle(frame: Frame, lifecycleEvent: types.LifecycleEvent) { onLifecycle(frame: Frame, lifecycleEvent: types.LifecycleEvent) {
if (this._progress && frame === this._frame && frame._url !== 'about:blank') if (this._progress && frame === this._frame && frame._url !== 'about:blank')
this._progress.log(apiLog, `"${lifecycleEvent}" event fired`); this._progress.logger.info(`"${lifecycleEvent}" event fired`);
if (this._onLifecycle && this._checkLifecycleRecursively(this._frame, this._onLifecycle.waitUntil)) if (this._onLifecycle && this._checkLifecycleRecursively(this._frame, this._onLifecycle.waitUntil))
this._onLifecycle.resolve(); this._onLifecycle.resolve();
} }

View file

@ -1,33 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Page } from './page';
import { Log } from './logger';
const hintsLog: Log = {
name: 'hint',
severity: 'warning'
};
let waitForTimeoutWasUsedReported = false;
export function waitForTimeoutWasUsed(page: Page) {
if (waitForTimeoutWasUsedReported)
return;
waitForTimeoutWasUsedReported = true;
page._logger.log(hintsLog, `WARNING: page.waitForTimeout(timeout) should only be used for debugging.
Tests using the timer in production are going to be flaky.
Use signals such as network events, selectors becoming visible, etc. instead.`);
}

View file

@ -15,49 +15,116 @@
*/ */
import * as debug from 'debug'; import * as debug from 'debug';
import { helper } from './helper';
import { Logger as LoggerSink, LoggerSeverity } from './types';
export type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error'; export function logError(logger: Logger): (error: Error) => void {
return error => logger.error(error);
export type Log = {
name: string;
severity?: LoggerSeverity;
color?: string | undefined;
};
export interface Logger {
isEnabled(name: string, severity: LoggerSeverity): boolean;
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }): void;
} }
export const errorLog: Log = { name: 'generic', severity: 'error' }; export class Logger {
export const apiLog: Log = { name: 'api', color: 'cyan' }; private _loggerSink: LoggerSink;
private _name: string;
private _hints: { color?: string; };
private _scopeName: string | undefined;
private _recording: string[] | undefined;
export function logError(logger: InnerLogger): (error: Error) => void { constructor(loggerSink: LoggerSink, name: string, hints: { color?: string }, scopeName?: string, record?: boolean) {
return error => logger.log(errorLog, error, []); this._loggerSink = loggerSink;
this._name = name;
this._hints = hints;
this._scopeName = scopeName;
if (record)
this._recording = [];
}
isEnabled(severity?: LoggerSeverity): boolean {
return this._loggerSink.isEnabled(this._name, severity || 'info');
}
verbose(message: string, ...args: any[]) {
return this._innerLog('verbose', message, args);
}
info(message: string, ...args: any[]) {
return this._innerLog('info', message, args);
}
warn(message: string, ...args: any[]) {
return this._innerLog('warning', message, args);
}
error(message: string | Error, ...args: any[]) {
return this._innerLog('error', message, args);
}
createScope(scopeName: string, record?: boolean): Logger {
this._loggerSink.log(this._name, 'info', `=> ${scopeName} started`, [], this._hints);
return new Logger(this._loggerSink, this._name, this._hints, scopeName, record);
}
endScope(status: string) {
this._loggerSink.log(this._name, 'info', `<= ${this._scopeName} ${status}`, [], this._hints);
}
private _innerLog(severity: LoggerSeverity, message: string | Error, ...args: any[]) {
if (this._recording)
this._recording.push(`[${this._name}] ${message}`);
this._loggerSink.log(this._name, severity, message, args, this._hints);
}
recording(): string[] {
return this._recording ? this._recording.slice() : [];
}
} }
export class InnerLogger { export class Loggers {
private _userSink: Logger | undefined; readonly api: Logger;
private _debugSink: DebugLogger; readonly browser: Logger;
readonly protocol: Logger;
constructor(userSink: Logger | undefined) { constructor(userSink: LoggerSink | undefined) {
this._userSink = userSink; const loggerSink = new MultiplexingLoggerSink();
this._debugSink = new DebugLogger(); if (userSink)
loggerSink.add('user', userSink);
if (helper.isDebugMode())
loggerSink.add('pwdebug', new PwDebugLoggerSink());
loggerSink.add('debug', new DebugLoggerSink());
this.api = new Logger(loggerSink, 'api', { color: 'cyan' });
this.browser = new Logger(loggerSink, 'browser', {});
this.protocol = new Logger(loggerSink, 'protocol', { color: 'green' });
}
}
class MultiplexingLoggerSink implements LoggerSink {
private _loggers = new Map<string, LoggerSink>();
add(id: string, logger: LoggerSink) {
this._loggers.set(id, logger);
} }
isLogEnabled(log: Log): boolean { get(id: string): LoggerSink | undefined {
const severity = log.severity || 'info'; return this._loggers.get(id);
if (this._userSink && this._userSink.isEnabled(log.name, severity))
return true;
return this._debugSink.isEnabled(log.name, severity);
} }
log(log: Log, message: string | Error, ...args: any[]) { remove(id: string) {
const severity = log.severity || 'info'; this._loggers.delete(id);
const hints = log.color ? { color: log.color } : {}; }
if (this._userSink && this._userSink.isEnabled(log.name, severity))
this._userSink.log(log.name, severity, message, args, hints); isEnabled(name: string, severity: LoggerSeverity): boolean {
this._debugSink.log(log.name, severity, message, args, hints); for (const logger of this._loggers.values()) {
if (logger.isEnabled(name, severity))
return true;
}
return false;
}
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }) {
for (const logger of this._loggers.values()) {
if (logger.isEnabled(name, severity))
logger.log(name, severity, message, args, hints);
}
} }
} }
@ -71,7 +138,7 @@ const colorMap = new Map<string, number>([
['reset', 0], ['reset', 0],
]); ]);
class DebugLogger { class DebugLoggerSink {
private _debuggers = new Map<string, debug.IDebugger>(); private _debuggers = new Map<string, debug.IDebugger>();
isEnabled(name: string, severity: LoggerSeverity): boolean { isEnabled(name: string, severity: LoggerSeverity): boolean {
@ -96,3 +163,12 @@ class DebugLogger {
cachedDebugger(message, ...args); cachedDebugger(message, ...args);
} }
} }
class PwDebugLoggerSink {
isEnabled(name: string, severity: LoggerSeverity): boolean {
return false;
}
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }) {
}
}

View file

@ -30,8 +30,8 @@ import { ConsoleMessage, ConsoleMessageLocation } from './console';
import * as accessibility from './accessibility'; import * as accessibility from './accessibility';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
import { logError, InnerLogger } from './logger'; import { logError, Logger } from './logger';
import { ProgressController } from './progress'; import { ProgressController, Progress } from './progress';
export interface PageDelegate { export interface PageDelegate {
readonly rawMouse: input.RawMouse; readonly rawMouse: input.RawMouse;
@ -100,7 +100,7 @@ export class Page extends EventEmitter {
readonly mouse: input.Mouse; readonly mouse: input.Mouse;
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
readonly _delegate: PageDelegate; readonly _delegate: PageDelegate;
readonly _logger: InnerLogger; readonly _logger: Logger;
readonly _state: PageState; readonly _state: PageState;
readonly _pageBindings = new Map<string, PageBinding>(); readonly _pageBindings = new Map<string, PageBinding>();
readonly _evaluateOnNewDocumentSources: string[] = []; readonly _evaluateOnNewDocumentSources: string[] = [];
@ -112,11 +112,12 @@ export class Page extends EventEmitter {
readonly coverage: any; readonly coverage: any;
_routes: { url: types.URLMatch, handler: network.RouteHandler }[] = []; _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = [];
_ownedContext: BrowserContext | undefined; _ownedContext: BrowserContext | undefined;
_callingPageAPI = false;
constructor(delegate: PageDelegate, browserContext: BrowserContextBase) { constructor(delegate: PageDelegate, browserContext: BrowserContextBase) {
super(); super();
this._delegate = delegate; this._delegate = delegate;
this._logger = browserContext._logger; this._logger = browserContext._apiLogger;
this._closedCallback = () => {}; this._closedCallback = () => {};
this._closedPromise = new Promise(f => this._closedCallback = f); this._closedPromise = new Promise(f => this._closedCallback = f);
this._disconnectedCallback = () => {}; this._disconnectedCallback = () => {};
@ -139,6 +140,11 @@ export class Page extends EventEmitter {
this.coverage = delegate.coverage ? delegate.coverage() : null; this.coverage = delegate.coverage ? delegate.coverage() : null;
} }
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
const controller = new ProgressController(this._logger, timeout, `page.${apiName}`);
return controller.run(task);
}
_didClose() { _didClose() {
assert(!this._closed, 'Page closed twice'); assert(!this._closed, 'Page closed twice');
this._closed = true; this._closed = true;
@ -202,48 +208,48 @@ export class Page extends EventEmitter {
} }
async $(selector: string): Promise<dom.ElementHandle<Element> | null> { async $(selector: string): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().$(selector); return this._attributeToPage(() => this.mainFrame().$(selector));
} }
async waitForSelector(selector: string, options?: types.WaitForElementOptions): Promise<dom.ElementHandle<Element> | null> { async waitForSelector(selector: string, options?: types.WaitForElementOptions): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().waitForSelector(selector, options); return this._attributeToPage(() => this.mainFrame().waitForSelector(selector, options));
} }
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> {
return this.mainFrame().dispatchEvent(selector, type, eventInit, options); return this._attributeToPage(() => this.mainFrame().dispatchEvent(selector, type, eventInit, options));
} }
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>; async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
async evaluateHandle<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<types.SmartHandle<R>>; async evaluateHandle<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<types.SmartHandle<R>>;
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>> { async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>> {
assertMaxArguments(arguments.length, 2); assertMaxArguments(arguments.length, 2);
return this.mainFrame().evaluateHandle(pageFunction, arg); return this._attributeToPage(() => this.mainFrame().evaluateHandle(pageFunction, arg));
} }
async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>; async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
async $eval<R>(selector: string, pageFunction: types.FuncOn<Element, void, R>, arg?: any): Promise<R>; async $eval<R>(selector: string, pageFunction: types.FuncOn<Element, void, R>, arg?: any): Promise<R>;
async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R> { async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 3); assertMaxArguments(arguments.length, 3);
return this.mainFrame().$eval(selector, pageFunction, arg); return this._attributeToPage(() => this.mainFrame().$eval(selector, pageFunction, arg));
} }
async $$eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>; async $$eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>;
async $$eval<R>(selector: string, pageFunction: types.FuncOn<Element[], void, R>, arg?: any): Promise<R>; async $$eval<R>(selector: string, pageFunction: types.FuncOn<Element[], void, R>, arg?: any): Promise<R>;
async $$eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> { async $$eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 3); assertMaxArguments(arguments.length, 3);
return this.mainFrame().$$eval(selector, pageFunction, arg); return this._attributeToPage(() => this.mainFrame().$$eval(selector, pageFunction, arg));
} }
async $$(selector: string): Promise<dom.ElementHandle<Element>[]> { async $$(selector: string): Promise<dom.ElementHandle<Element>[]> {
return this.mainFrame().$$(selector); return this._attributeToPage(() => this.mainFrame().$$(selector));
} }
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<dom.ElementHandle> { async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<dom.ElementHandle> {
return this.mainFrame().addScriptTag(options); return this._attributeToPage(() => this.mainFrame().addScriptTag(options));
} }
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> { async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> {
return this.mainFrame().addStyleTag(options); return this._attributeToPage(() => this.mainFrame().addStyleTag(options));
} }
async exposeFunction(name: string, playwrightFunction: Function) { async exposeFunction(name: string, playwrightFunction: Function) {
@ -279,19 +285,19 @@ export class Page extends EventEmitter {
} }
url(): string { url(): string {
return this.mainFrame().url(); return this._attributeToPage(() => this.mainFrame().url());
} }
async content(): Promise<string> { async content(): Promise<string> {
return this.mainFrame().content(); return this._attributeToPage(() => this.mainFrame().content());
} }
async setContent(html: string, options?: types.NavigateOptions): Promise<void> { async setContent(html: string, options?: types.NavigateOptions): Promise<void> {
return this.mainFrame().setContent(html, options); return this._attributeToPage(() => this.mainFrame().setContent(html, options));
} }
async goto(url: string, options?: frames.GotoOptions): Promise<network.Response | null> { async goto(url: string, options?: frames.GotoOptions): Promise<network.Response | null> {
return this.mainFrame().goto(url, options); return this._attributeToPage(() => this.mainFrame().goto(url, options));
} }
async reload(options?: types.NavigateOptions): Promise<network.Response | null> { async reload(options?: types.NavigateOptions): Promise<network.Response | null> {
@ -301,11 +307,11 @@ export class Page extends EventEmitter {
} }
async waitForLoadState(state?: types.LifecycleEvent, options?: types.TimeoutOptions): Promise<void> { async waitForLoadState(state?: types.LifecycleEvent, options?: types.TimeoutOptions): Promise<void> {
return this.mainFrame().waitForLoadState(state, options); return this._attributeToPage(() => this.mainFrame().waitForLoadState(state, options));
} }
async waitForNavigation(options?: types.WaitForNavigationOptions): Promise<network.Response | null> { async waitForNavigation(options?: types.WaitForNavigationOptions): Promise<network.Response | null> {
return this.mainFrame().waitForNavigation(options); return this._attributeToPage(() => this.mainFrame().waitForNavigation(options));
} }
async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean), options: types.TimeoutOptions = {}): Promise<network.Request> { async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean), options: types.TimeoutOptions = {}): Promise<network.Request> {
@ -328,7 +334,7 @@ export class Page extends EventEmitter {
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate; const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options)); const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options), 'page.waitForEvent');
this._disconnectedPromise.then(error => progressController.abort(error)); this._disconnectedPromise.then(error => progressController.abort(error));
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate)); return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate));
} }
@ -376,7 +382,7 @@ export class Page extends EventEmitter {
async evaluate<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<R>; async evaluate<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<R>;
async evaluate<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<R> { async evaluate<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<R> {
assertMaxArguments(arguments.length, 2); assertMaxArguments(arguments.length, 2);
return this.mainFrame().evaluate(pageFunction, arg); return this._attributeToPage(() => this.mainFrame().evaluate(pageFunction, arg));
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
@ -424,7 +430,7 @@ export class Page extends EventEmitter {
} }
async title(): Promise<string> { async title(): Promise<string> {
return this.mainFrame().title(); return this._attributeToPage(() => this.mainFrame().title());
} }
async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) { async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) {
@ -443,64 +449,73 @@ export class Page extends EventEmitter {
return this._closed; return this._closed;
} }
private _attributeToPage<T>(func: () => T): T {
try {
this._callingPageAPI = true;
return func();
} finally {
this._callingPageAPI = false;
}
}
async click(selector: string, options?: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { async click(selector: string, options?: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
return this.mainFrame().click(selector, options); return this._attributeToPage(() => this.mainFrame().click(selector, options));
} }
async dblclick(selector: string, options?: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { async dblclick(selector: string, options?: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
return this.mainFrame().dblclick(selector, options); return this._attributeToPage(() => this.mainFrame().dblclick(selector, options));
} }
async fill(selector: string, value: string, options?: types.NavigatingActionWaitOptions) { async fill(selector: string, value: string, options?: types.NavigatingActionWaitOptions) {
return this.mainFrame().fill(selector, value, options); return this._attributeToPage(() => this.mainFrame().fill(selector, value, options));
} }
async focus(selector: string, options?: types.TimeoutOptions) { async focus(selector: string, options?: types.TimeoutOptions) {
return this.mainFrame().focus(selector, options); return this._attributeToPage(() => this.mainFrame().focus(selector, options));
} }
async textContent(selector: string, options?: types.TimeoutOptions): Promise<null|string> { async textContent(selector: string, options?: types.TimeoutOptions): Promise<null|string> {
return this.mainFrame().textContent(selector, options); return this._attributeToPage(() => this.mainFrame().textContent(selector, options));
} }
async innerText(selector: string, options?: types.TimeoutOptions): Promise<string> { async innerText(selector: string, options?: types.TimeoutOptions): Promise<string> {
return this.mainFrame().innerText(selector, options); return this._attributeToPage(() => this.mainFrame().innerText(selector, options));
} }
async innerHTML(selector: string, options?: types.TimeoutOptions): Promise<string> { async innerHTML(selector: string, options?: types.TimeoutOptions): Promise<string> {
return this.mainFrame().innerHTML(selector, options); return this._attributeToPage(() => this.mainFrame().innerHTML(selector, options));
} }
async getAttribute(selector: string, name: string, options?: types.TimeoutOptions): Promise<string | null> { async getAttribute(selector: string, name: string, options?: types.TimeoutOptions): Promise<string | null> {
return this.mainFrame().getAttribute(selector, name, options); return this._attributeToPage(() => this.mainFrame().getAttribute(selector, name, options));
} }
async hover(selector: string, options?: dom.PointerActionOptions & types.PointerActionWaitOptions) { async hover(selector: string, options?: dom.PointerActionOptions & types.PointerActionWaitOptions) {
return this.mainFrame().hover(selector, options); return this._attributeToPage(() => this.mainFrame().hover(selector, options));
} }
async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | null, options?: types.NavigatingActionWaitOptions): Promise<string[]> { async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | null, options?: types.NavigatingActionWaitOptions): Promise<string[]> {
return this.mainFrame().selectOption(selector, values, options); return this._attributeToPage(() => this.mainFrame().selectOption(selector, values, options));
} }
async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions): Promise<void> { async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions): Promise<void> {
return this.mainFrame().setInputFiles(selector, files, options); return this._attributeToPage(() => this.mainFrame().setInputFiles(selector, files, options));
} }
async type(selector: string, text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { async type(selector: string, text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
return this.mainFrame().type(selector, text, options); return this._attributeToPage(() => this.mainFrame().type(selector, text, options));
} }
async press(selector: string, key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { async press(selector: string, key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
return this.mainFrame().press(selector, key, options); return this._attributeToPage(() => this.mainFrame().press(selector, key, options));
} }
async check(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { async check(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
return this.mainFrame().check(selector, options); return this._attributeToPage(() => this.mainFrame().check(selector, options));
} }
async uncheck(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { async uncheck(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
return this.mainFrame().uncheck(selector, options); return this._attributeToPage(() => this.mainFrame().uncheck(selector, options));
} }
async waitForTimeout(timeout: number) { async waitForTimeout(timeout: number) {
@ -510,7 +525,7 @@ export class Page extends EventEmitter {
async waitForFunction<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>>; async waitForFunction<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>>;
async waitForFunction<R>(pageFunction: types.Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>>; async waitForFunction<R>(pageFunction: types.Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>>;
async waitForFunction<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>> { async waitForFunction<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>> {
return this.mainFrame().waitForFunction(pageFunction, arg, options); return this._attributeToPage(() => this.mainFrame().waitForFunction(pageFunction, arg, options));
} }
workers(): Worker[] { workers(): Worker[] {

View file

@ -14,18 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import { InnerLogger, Log, apiLog } from './logger'; import { Logger } from './logger';
import { TimeoutError } from './errors'; import { TimeoutError } from './errors';
import { assert } from './helper'; import { assert } from './helper';
import { getCurrentApiCall, rewriteErrorMessage } from './utils/stackTrace'; import { rewriteErrorMessage } from './utils/stackTrace';
export interface Progress { export interface Progress {
readonly apiName: string; readonly apiName: string;
readonly aborted: Promise<void>; readonly aborted: Promise<void>;
readonly logger: Logger;
timeUntilDeadline(): number; timeUntilDeadline(): number;
isRunning(): boolean; isRunning(): boolean;
cleanupWhenAborted(cleanup: () => any): void; cleanupWhenAborted(cleanup: () => any): void;
log(log: Log, message: string | Error): void;
throwIfAborted(): void; throwIfAborted(): void;
} }
@ -35,7 +35,7 @@ export function isRunningTask(): boolean {
return !!runningTaskCount; return !!runningTaskCount;
} }
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, logger: InnerLogger, timeout: number, apiName?: string): Promise<T> { export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, logger: Logger, timeout: number, apiName: string): Promise<T> {
const controller = new ProgressController(logger, timeout, apiName); const controller = new ProgressController(logger, timeout, apiName);
return controller.run(task); return controller.run(task);
} }
@ -54,15 +54,14 @@ export class ProgressController {
// Cleanups to be run only in the case of abort. // Cleanups to be run only in the case of abort.
private _cleanups: (() => any)[] = []; private _cleanups: (() => any)[] = [];
private _logger: InnerLogger; private _logger: Logger;
private _logRecording: string[] = [];
private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before'; private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before';
private _apiName: string; private _apiName: string;
private _deadline: number; private _deadline: number;
private _timeout: number; private _timeout: number;
constructor(logger: InnerLogger, timeout: number, apiName?: string) { constructor(logger: Logger, timeout: number, apiName: string) {
this._apiName = apiName || getCurrentApiCall(); this._apiName = apiName;
this._logger = logger; this._logger = logger;
this._timeout = timeout; this._timeout = timeout;
@ -78,9 +77,12 @@ export class ProgressController {
this._state = 'running'; this._state = 'running';
++runningTaskCount; ++runningTaskCount;
const loggerScope = this._logger.createScope(this._apiName, true);
const progress: Progress = { const progress: Progress = {
apiName: this._apiName, apiName: this._apiName,
aborted: this._abortedPromise, aborted: this._abortedPromise,
logger: loggerScope,
timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node. timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node.
isRunning: () => this._state === 'running', isRunning: () => this._state === 'running',
cleanupWhenAborted: (cleanup: () => any) => { cleanupWhenAborted: (cleanup: () => any) => {
@ -89,20 +91,11 @@ export class ProgressController {
else else
runCleanup(cleanup); runCleanup(cleanup);
}, },
log: (log: Log, message: string | Error) => {
if (this._state === 'running') {
this._logRecording.push(`[${log.name}] ${message.toString()}`);
this._logger.log(log, ' ' + message);
} else {
this._logger.log(log, message);
}
},
throwIfAborted: () => { throwIfAborted: () => {
if (this._state === 'aborted') if (this._state === 'aborted')
throw new AbortedError(); throw new AbortedError();
}, },
}; };
this._logger.log(apiLog, `=> ${this._apiName} started`);
const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded during ${this._apiName}.`); const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded during ${this._apiName}.`);
const timer = setTimeout(() => this._forceAbort(timeoutError), progress.timeUntilDeadline()); const timer = setTimeout(() => this._forceAbort(timeoutError), progress.timeUntilDeadline());
@ -111,18 +104,17 @@ export class ProgressController {
const result = await Promise.race([promise, this._forceAbortPromise]); const result = await Promise.race([promise, this._forceAbortPromise]);
clearTimeout(timer); clearTimeout(timer);
this._state = 'finished'; this._state = 'finished';
this._logger.log(apiLog, `<= ${this._apiName} succeeded`); loggerScope.endScope('succeeded');
return result; return result;
} catch (e) { } catch (e) {
this._aborted(); this._aborted();
rewriteErrorMessage(e, e.message + formatLogRecording(this._logRecording, this._apiName) + kLoggingNote); rewriteErrorMessage(e, e.message + formatLogRecording(loggerScope.recording(), this._apiName) + kLoggingNote);
clearTimeout(timer); clearTimeout(timer);
this._state = 'aborted'; this._state = 'aborted';
this._logger.log(apiLog, `<= ${this._apiName} failed`); loggerScope.endScope(`failed`);
await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup))); await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
throw e; throw e;
} finally { } finally {
this._logRecording = [];
--runningTaskCount; --runningTaskCount;
} }
} }

View file

@ -21,7 +21,7 @@ import * as util from 'util';
import { BrowserContext, PersistentContextOptions, verifyProxySettings, validateBrowserContextOptions } from '../browserContext'; import { BrowserContext, PersistentContextOptions, verifyProxySettings, validateBrowserContextOptions } from '../browserContext';
import { BrowserServer } from './browserServer'; import { BrowserServer } from './browserServer';
import * as browserPaths from '../install/browserPaths'; import * as browserPaths from '../install/browserPaths';
import { Logger, InnerLogger } from '../logger'; import { Loggers, Logger } from '../logger';
import { ConnectionTransport, WebSocketTransport } from '../transport'; import { ConnectionTransport, WebSocketTransport } from '../transport';
import { BrowserBase, BrowserOptions, Browser } from '../browser'; import { BrowserBase, BrowserOptions, Browser } from '../browser';
import { assert, helper } from '../helper'; import { assert, helper } from '../helper';
@ -29,7 +29,7 @@ import { launchProcess, Env, waitForLine } from './processLauncher';
import { Events } from '../events'; import { Events } from '../events';
import { PipeTransport } from './pipeTransport'; import { PipeTransport } from './pipeTransport';
import { Progress, runAbortableTask } from '../progress'; import { Progress, runAbortableTask } from '../progress';
import { ProxySettings } from '../types'; import * as types from '../types';
import { TimeoutSettings } from '../timeoutSettings'; import { TimeoutSettings } from '../timeoutSettings';
import { WebSocketServer } from './webSocketServer'; import { WebSocketServer } from './webSocketServer';
@ -45,18 +45,18 @@ export type LaunchOptionsBase = {
handleSIGTERM?: boolean, handleSIGTERM?: boolean,
handleSIGHUP?: boolean, handleSIGHUP?: boolean,
timeout?: number, timeout?: number,
logger?: Logger, logger?: types.Logger,
env?: Env, env?: Env,
headless?: boolean, headless?: boolean,
devtools?: boolean, devtools?: boolean,
proxy?: ProxySettings, proxy?: types.ProxySettings,
downloadsPath?: string, downloadsPath?: string,
}; };
type ConnectOptions = { type ConnectOptions = {
wsEndpoint: string, wsEndpoint: string,
slowMo?: number, slowMo?: number,
logger?: Logger, logger?: types.Logger,
timeout?: number, timeout?: number,
}; };
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number }; export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
@ -105,8 +105,8 @@ 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.');
options = validateLaunchOptions(options); options = validateLaunchOptions(options);
const logger = new InnerLogger(options.logger); const loggers = new Loggers(options.logger);
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, logger, undefined), logger, TimeoutSettings.timeout(options)); const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, loggers, undefined), loggers.browser, TimeoutSettings.timeout(options), `browserType.launch`);
return browser; return browser;
} }
@ -114,12 +114,12 @@ 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.');
options = validateLaunchOptions(options); options = validateLaunchOptions(options);
const persistent = validateBrowserContextOptions(options); const persistent = validateBrowserContextOptions(options);
const logger = new InnerLogger(options.logger); const loggers = new Loggers(options.logger);
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, logger, persistent, userDataDir), logger, TimeoutSettings.timeout(options)); const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, loggers, persistent, userDataDir), loggers.browser, TimeoutSettings.timeout(options), 'browserType.launchPersistentContext');
return browser._defaultContext!; return browser._defaultContext!;
} }
async _innerLaunch(progress: Progress, options: LaunchOptions, logger: InnerLogger, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> { async _innerLaunch(progress: Progress, options: LaunchOptions, logger: Loggers, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
options.proxy = options.proxy ? verifyProxySettings(options.proxy) : undefined; options.proxy = options.proxy ? verifyProxySettings(options.proxy) : undefined;
const { browserServer, downloadsPath, transport } = await this._launchServer(progress, options, !!persistent, logger, userDataDir); const { browserServer, downloadsPath, transport } = await this._launchServer(progress, options, !!persistent, logger, userDataDir);
if ((options as any).__testHookBeforeCreateBrowser) if ((options as any).__testHookBeforeCreateBrowser)
@ -128,7 +128,7 @@ export abstract class BrowserTypeBase implements BrowserType {
slowMo: options.slowMo, slowMo: options.slowMo,
persistent, persistent,
headful: !options.headless, headful: !options.headless,
logger, loggers: logger,
downloadsPath, downloadsPath,
ownedServer: browserServer, ownedServer: browserServer,
proxy: options.proxy, proxy: options.proxy,
@ -145,28 +145,28 @@ export abstract class BrowserTypeBase implements BrowserType {
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> { async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
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');
options = validateLaunchOptions(options); options = validateLaunchOptions(options);
const logger = new InnerLogger(options.logger); const loggers = new Loggers(options.logger);
const { port = 0 } = options; const { port = 0 } = options;
return runAbortableTask(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, loggers);
browserServer._webSocketServer = this._startWebSocketServer(transport, logger, port); browserServer._webSocketServer = this._startWebSocketServer(transport, loggers.browser, port);
return browserServer; return browserServer;
}, logger, TimeoutSettings.timeout(options)); }, loggers.browser, TimeoutSettings.timeout(options), 'browserType.launchServer');
} }
async connect(options: ConnectOptions): Promise<Browser> { async connect(options: ConnectOptions): Promise<Browser> {
const logger = new InnerLogger(options.logger); const loggers = new Loggers(options.logger);
return runAbortableTask(async progress => { return runAbortableTask(async progress => {
const transport = await WebSocketTransport.connect(progress, options.wsEndpoint); const transport = await WebSocketTransport.connect(progress, options.wsEndpoint);
progress.cleanupWhenAborted(() => 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, loggers });
return browser; return browser;
}, logger, TimeoutSettings.timeout(options)); }, loggers.browser, TimeoutSettings.timeout(options), 'browserType.connect');
} }
private async _launchServer(progress: Progress, options: LaunchServerOptions, isPersistent: boolean, logger: InnerLogger, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, transport: ConnectionTransport }> { private async _launchServer(progress: Progress, options: LaunchServerOptions, isPersistent: boolean, loggers: Loggers, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, transport: ConnectionTransport }> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
@ -240,14 +240,14 @@ export abstract class BrowserTypeBase implements BrowserType {
transport = await WebSocketTransport.connect(progress, innerEndpoint); transport = await WebSocketTransport.connect(progress, innerEndpoint);
} else { } else {
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream]; const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
transport = new PipeTransport(stdio[3], stdio[4], logger); transport = new PipeTransport(stdio[3], stdio[4], loggers.browser);
} }
return { browserServer, downloadsPath, transport }; return { browserServer, downloadsPath, transport };
} }
abstract _defaultArgs(options: LaunchOptionsBase, isPersistent: boolean, userDataDir: string): string[]; abstract _defaultArgs(options: LaunchOptionsBase, isPersistent: boolean, userDataDir: string): string[];
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>; abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
abstract _startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer; abstract _startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer;
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env; abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
abstract _attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void; abstract _attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void;
} }

View file

@ -23,7 +23,7 @@ import { Env } from './processLauncher';
import { kBrowserCloseMessageId } from '../chromium/crConnection'; import { kBrowserCloseMessageId } from '../chromium/crConnection';
import { LaunchOptionsBase, BrowserTypeBase } from './browserType'; import { LaunchOptionsBase, BrowserTypeBase } from './browserType';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { InnerLogger } from '../logger'; import { Logger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths'; import { BrowserDescriptor } from '../install/browserPaths';
import { CRDevTools } from '../chromium/crDevTools'; import { CRDevTools } from '../chromium/crDevTools';
import { BrowserOptions } from '../browser'; import { BrowserOptions } from '../browser';
@ -72,7 +72,7 @@ export class Chromium extends BrowserTypeBase {
transport.send(message); transport.send(message);
} }
_startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer { _startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
return startWebSocketServer(transport, logger, port); return startWebSocketServer(transport, logger, port);
} }
@ -130,7 +130,7 @@ type SessionData = {
parent?: string, parent?: string,
}; };
function startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer { function startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
const awaitingBrowserTarget = new Map<number, ws>(); const awaitingBrowserTarget = new Map<number, ws>();
const sessionToData = new Map<string, SessionData>(); const sessionToData = new Map<string, SessionData>();
const socketToBrowserSession = new Map<ws, { sessionId?: string, queue?: ProtocolRequest[] }>(); const socketToBrowserSession = new Map<ws, { sessionId?: string, queue?: ProtocolRequest[] }>();

View file

@ -20,7 +20,7 @@ import { CRConnection, CRSession } from '../chromium/crConnection';
import { CRExecutionContext } from '../chromium/crExecutionContext'; import { CRExecutionContext } from '../chromium/crExecutionContext';
import { Events } from '../events'; import { Events } from '../events';
import * as js from '../javascript'; import * as js from '../javascript';
import { InnerLogger, Logger } from '../logger'; import { Loggers, Logger } from '../logger';
import { Page } from '../page'; import { Page } from '../page';
import { TimeoutSettings } from '../timeoutSettings'; import { TimeoutSettings } from '../timeoutSettings';
import { WebSocketTransport } from '../transport'; import { WebSocketTransport } from '../transport';
@ -41,7 +41,7 @@ type ElectronLaunchOptions = {
handleSIGTERM?: boolean, handleSIGTERM?: boolean,
handleSIGHUP?: boolean, handleSIGHUP?: boolean,
timeout?: number, timeout?: number,
logger?: Logger, logger?: types.Logger,
}; };
export const ElectronEvents = { export const ElectronEvents = {
@ -57,7 +57,7 @@ interface ElectronPage extends Page {
} }
export class ElectronApplication extends EventEmitter { export class ElectronApplication extends EventEmitter {
private _logger: InnerLogger; private _apiLogger: Logger;
private _browserContext: CRBrowserContext; private _browserContext: CRBrowserContext;
private _nodeConnection: CRConnection; private _nodeConnection: CRConnection;
private _nodeSession: CRSession; private _nodeSession: CRSession;
@ -67,9 +67,9 @@ export class ElectronApplication extends EventEmitter {
private _lastWindowId = 0; private _lastWindowId = 0;
readonly _timeoutSettings = new TimeoutSettings(); readonly _timeoutSettings = new TimeoutSettings();
constructor(logger: InnerLogger, browser: CRBrowser, nodeConnection: CRConnection) { constructor(logger: Loggers, browser: CRBrowser, nodeConnection: CRConnection) {
super(); super();
this._logger = logger; this._apiLogger = logger.api;
this._browserContext = browser._defaultContext as CRBrowserContext; this._browserContext = browser._defaultContext as CRBrowserContext;
this._browserContext.on(Events.BrowserContext.Close, () => this.emit(ElectronEvents.ElectronApplication.Close)); this._browserContext.on(Events.BrowserContext.Close, () => this.emit(ElectronEvents.ElectronApplication.Close));
this._browserContext.on(Events.BrowserContext.Page, event => this._onPage(event)); this._browserContext.on(Events.BrowserContext.Page, event => this._onPage(event));
@ -131,7 +131,7 @@ export class ElectronApplication extends EventEmitter {
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate; const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options)); const progressController = new ProgressController(this._apiLogger, this._timeoutSettings.timeout(options), 'electron.waitForEvent');
if (event !== ElectronEvents.ElectronApplication.Close) if (event !== ElectronEvents.ElectronApplication.Close)
this._browserContext._closePromise.then(error => progressController.abort(error)); this._browserContext._closePromise.then(error => progressController.abort(error));
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate)); return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate));
@ -172,7 +172,7 @@ export class Electron {
handleSIGTERM = true, handleSIGTERM = true,
handleSIGHUP = true, handleSIGHUP = true,
} = options; } = options;
const logger = new InnerLogger(options.logger); const loggers = new Loggers(options.logger);
return runAbortableTask(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];
@ -196,15 +196,15 @@ export class Electron {
const nodeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/); const nodeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/);
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
const nodeConnection = new CRConnection(nodeTransport, logger); const nodeConnection = new CRConnection(nodeTransport, loggers);
const chromeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/); const chromeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/);
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
const browserServer = new BrowserServer(launchedProcess, gracefullyClose, kill); const browserServer = new BrowserServer(launchedProcess, gracefullyClose, kill);
const browser = await CRBrowser.connect(chromeTransport, { headful: true, logger, persistent: { viewport: null }, ownedServer: browserServer }); const browser = await CRBrowser.connect(chromeTransport, { headful: true, loggers, persistent: { viewport: null }, ownedServer: browserServer });
app = new ElectronApplication(logger, browser, nodeConnection); app = new ElectronApplication(loggers, browser, nodeConnection);
await app._init(); await app._init();
return app; return app;
}, logger, TimeoutSettings.timeout(options)); }, loggers.browser, TimeoutSettings.timeout(options), 'electron.launch');
} }
} }

View file

@ -24,7 +24,7 @@ import { kBrowserCloseMessageId } from '../firefox/ffConnection';
import { LaunchOptionsBase, BrowserTypeBase, FirefoxUserPrefsOptions } from './browserType'; import { LaunchOptionsBase, BrowserTypeBase, FirefoxUserPrefsOptions } from './browserType';
import { Env } from './processLauncher'; import { Env } from './processLauncher';
import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport'; import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport';
import { InnerLogger } from '../logger'; import { Logger } from '../logger';
import { BrowserOptions } from '../browser'; import { BrowserOptions } from '../browser';
import { BrowserDescriptor } from '../install/browserPaths'; import { BrowserDescriptor } from '../install/browserPaths';
import { WebSocketServer } from './webSocketServer'; import { WebSocketServer } from './webSocketServer';
@ -52,7 +52,7 @@ export class Firefox extends BrowserTypeBase {
transport.send(message); transport.send(message);
} }
_startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer { _startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
return startWebSocketServer(transport, logger, port); return startWebSocketServer(transport, logger, port);
} }
@ -110,7 +110,7 @@ type SessionData = {
socket: ws, socket: ws,
}; };
function startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer { function startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
const pendingBrowserContextCreations = new Set<number>(); const pendingBrowserContextCreations = new Set<number>();
const pendingBrowserContextDeletions = new Map<number, string>(); const pendingBrowserContextDeletions = new Map<number, string>();
const browserContextIds = new Map<string, ws>(); const browserContextIds = new Map<string, ws>();

View file

@ -17,7 +17,7 @@
import { helper, RegisteredListener } from '../helper'; import { helper, RegisteredListener } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { logError, InnerLogger } from '../logger'; import { logError, Logger } from '../logger';
export class PipeTransport implements ConnectionTransport { export class PipeTransport implements ConnectionTransport {
private _pipeWrite: NodeJS.WritableStream; private _pipeWrite: NodeJS.WritableStream;
@ -29,7 +29,7 @@ export class PipeTransport implements ConnectionTransport {
onmessage?: (message: ProtocolResponse) => void; onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void; onclose?: () => void;
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream, logger: InnerLogger) { constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream, logger: Logger) {
this._pipeWrite = pipeWrite; this._pipeWrite = pipeWrite;
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)), helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),

View file

@ -16,26 +16,12 @@
*/ */
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import { Log } from '../logger';
import * as readline from 'readline'; import * as readline from 'readline';
import * as removeFolder from 'rimraf'; import * as removeFolder from 'rimraf';
import * as stream from 'stream'; import * as stream from 'stream';
import { helper } from '../helper'; import { helper } from '../helper';
import { Progress } from '../progress'; import { Progress } from '../progress';
export const browserLog: Log = {
name: 'browser',
};
const browserStdOutLog: Log = {
name: 'browser:out',
};
const browserStdErrLog: Log = {
name: 'browser:err',
severity: 'warning'
};
export type Env = {[key: string]: string | number | boolean | undefined}; export type Env = {[key: string]: string | number | boolean | undefined};
export type LaunchProcessOptions = { export type LaunchProcessOptions = {
@ -68,7 +54,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
const progress = options.progress; const progress = options.progress;
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe']; const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
progress.log(browserLog, `<launching> ${options.executablePath} ${options.args.join(' ')}`); progress.logger.info(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
const spawnedProcess = childProcess.spawn( const spawnedProcess = childProcess.spawn(
options.executablePath, options.executablePath,
options.args, options.args,
@ -90,16 +76,16 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
}); });
return cleanup().then(() => failedPromise).then(e => Promise.reject(e)); return cleanup().then(() => failedPromise).then(e => Promise.reject(e));
} }
progress.log(browserLog, `<launched> pid=${spawnedProcess.pid}`); progress.logger.info(`<launched> pid=${spawnedProcess.pid}`);
const stdout = readline.createInterface({ input: spawnedProcess.stdout }); const stdout = readline.createInterface({ input: spawnedProcess.stdout });
stdout.on('line', (data: string) => { stdout.on('line', (data: string) => {
progress.log(browserStdOutLog, data); progress.logger.info(data);
}); });
const stderr = readline.createInterface({ input: spawnedProcess.stderr }); const stderr = readline.createInterface({ input: spawnedProcess.stderr });
stderr.on('line', (data: string) => { stderr.on('line', (data: string) => {
progress.log(browserStdErrLog, data); progress.logger.warn(data);
}); });
let processClosed = false; let processClosed = false;
@ -108,7 +94,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
let fulfillCleanup = () => {}; let fulfillCleanup = () => {};
const waitForCleanup = new Promise<void>(f => fulfillCleanup = f); const waitForCleanup = new Promise<void>(f => fulfillCleanup = f);
spawnedProcess.once('exit', (exitCode, signal) => { spawnedProcess.once('exit', (exitCode, signal) => {
progress.log(browserLog, `<process did exit: exitCode=${exitCode}, signal=${signal}>`); progress.logger.info(`<process did exit: exitCode=${exitCode}, signal=${signal}>`);
processClosed = true; processClosed = true;
helper.removeEventListeners(listeners); helper.removeEventListeners(listeners);
options.onExit(exitCode, signal); options.onExit(exitCode, signal);
@ -135,21 +121,21 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
// reentrancy to this function, for example user sends SIGINT second time. // reentrancy to this function, for example user sends SIGINT second time.
// In this case, let's forcefully kill the process. // In this case, let's forcefully kill the process.
if (gracefullyClosing) { if (gracefullyClosing) {
progress.log(browserLog, `<forecefully close>`); progress.logger.info(`<forecefully close>`);
killProcess(); killProcess();
await waitForClose; // Ensure the process is dead and we called options.onkill. await waitForClose; // Ensure the process is dead and we called options.onkill.
return; return;
} }
gracefullyClosing = true; gracefullyClosing = true;
progress.log(browserLog, `<gracefully close start>`); progress.logger.info(`<gracefully close start>`);
await options.attemptToGracefullyClose().catch(() => killProcess()); await options.attemptToGracefullyClose().catch(() => killProcess());
await waitForCleanup; // Ensure the process is dead and we have cleaned up. await waitForCleanup; // Ensure the process is dead and we have cleaned up.
progress.log(browserLog, `<gracefully close end>`); progress.logger.info(`<gracefully close end>`);
} }
// This method has to be sync to be used as 'exit' event handler. // This method has to be sync to be used as 'exit' event handler.
function killProcess() { function killProcess() {
progress.log(browserLog, `<kill>`); progress.logger.info(`<kill>`);
helper.removeEventListeners(listeners); helper.removeEventListeners(listeners);
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) { if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
// Force kill the browser. // Force kill the browser.

View file

@ -17,7 +17,7 @@
import { IncomingMessage } from 'http'; import { IncomingMessage } from 'http';
import * as ws from 'ws'; import * as ws from 'ws';
import { helper } from '../helper'; import { helper } from '../helper';
import { InnerLogger, logError } from '../logger'; import { logError, Logger } from '../logger';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
export interface WebSocketServerDelegate { export interface WebSocketServerDelegate {
@ -30,7 +30,7 @@ export interface WebSocketServerDelegate {
export class WebSocketServer { export class WebSocketServer {
private _transport: ConnectionTransport; private _transport: ConnectionTransport;
private _logger: InnerLogger; private _logger: Logger;
private _server: ws.Server; private _server: ws.Server;
private _guid: string; private _guid: string;
readonly wsEndpoint: string; readonly wsEndpoint: string;
@ -40,7 +40,7 @@ export class WebSocketServer {
private _sockets = new Set<ws>(); private _sockets = new Set<ws>();
private _pendingRequests = new Map<number, { message: ProtocolRequest, source: ws | null }>(); private _pendingRequests = new Map<number, { message: ProtocolRequest, source: ws | null }>();
constructor(transport: ConnectionTransport, logger: InnerLogger, port: number, delegate: WebSocketServerDelegate) { constructor(transport: ConnectionTransport, logger: Logger, port: number, delegate: WebSocketServerDelegate) {
this._guid = helper.guid(); this._guid = helper.guid();
this._transport = transport; this._transport = transport;
this._logger = logger; this._logger = logger;

View file

@ -22,7 +22,7 @@ import { kBrowserCloseMessageId } from '../webkit/wkConnection';
import { LaunchOptionsBase, BrowserTypeBase } from './browserType'; import { LaunchOptionsBase, BrowserTypeBase } from './browserType';
import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport'; import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport';
import * as ws from 'ws'; import * as ws from 'ws';
import { InnerLogger } from '../logger'; import { Logger } from '../logger';
import { BrowserOptions } from '../browser'; import { BrowserOptions } from '../browser';
import { BrowserDescriptor } from '../install/browserPaths'; import { BrowserDescriptor } from '../install/browserPaths';
import { WebSocketServer } from './webSocketServer'; import { WebSocketServer } from './webSocketServer';
@ -45,7 +45,7 @@ export class WebKit extends BrowserTypeBase {
transport.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId}); transport.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId});
} }
_startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer { _startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
return startWebSocketServer(transport, logger, port); return startWebSocketServer(transport, logger, port);
} }
@ -87,7 +87,7 @@ export class WebKit extends BrowserTypeBase {
} }
} }
function startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer { function startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
const pendingBrowserContextCreations = new Set<number>(); const pendingBrowserContextCreations = new Set<number>();
const pendingBrowserContextDeletions = new Map<number, string>(); const pendingBrowserContextDeletions = new Map<number, string>();
const browserContextIds = new Map<string, ws>(); const browserContextIds = new Map<string, ws>();

View file

@ -17,9 +17,7 @@
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { helper } from './helper'; import { helper } from './helper';
import { Log } from './logger';
import { Progress } from './progress'; import { Progress } from './progress';
import { browserLog } from './server/processLauncher';
export type ProtocolRequest = { export type ProtocolRequest = {
id: number; id: number;
@ -128,7 +126,7 @@ export class WebSocketTransport implements ConnectionTransport {
onclose?: () => void; onclose?: () => void;
static async connect(progress: Progress, url: string): Promise<WebSocketTransport> { static async connect(progress: Progress, url: string): Promise<WebSocketTransport> {
progress.log(browserLog, `<ws connecting> ${url}`); progress.logger.info(`<ws connecting> ${url}`);
const transport = new WebSocketTransport(progress, url); const transport = new WebSocketTransport(progress, url);
let success = false; let success = false;
progress.aborted.then(() => { progress.aborted.then(() => {
@ -137,11 +135,11 @@ export class WebSocketTransport implements ConnectionTransport {
}); });
await new Promise<WebSocketTransport>((fulfill, reject) => { await new Promise<WebSocketTransport>((fulfill, reject) => {
transport._ws.addEventListener('open', async () => { transport._ws.addEventListener('open', async () => {
progress.log(browserLog, `<ws connected> ${url}`); progress.logger.info(`<ws connected> ${url}`);
fulfill(transport); fulfill(transport);
}); });
transport._ws.addEventListener('error', event => { transport._ws.addEventListener('error', event => {
progress.log(browserLog, `<ws connect error> ${url} ${event.message}`); progress.logger.info(`<ws connect error> ${url} ${event.message}`);
reject(new Error('WebSocket error: ' + event.message)); reject(new Error('WebSocket error: ' + event.message));
transport._ws.close(); transport._ws.close();
}); });
@ -171,7 +169,7 @@ export class WebSocketTransport implements ConnectionTransport {
}); });
this._ws.addEventListener('close', event => { this._ws.addEventListener('close', event => {
this._progress && this._progress.log(browserLog, `<ws disconnected> ${url}`); this._progress && this._progress.logger.info(`<ws disconnected> ${url}`);
if (this.onclose) if (this.onclose)
this.onclose.call(null); this.onclose.call(null);
}); });
@ -184,7 +182,7 @@ export class WebSocketTransport implements ConnectionTransport {
} }
close() { close() {
this._progress && this._progress.log(browserLog, `<ws disconnecting> ${this._ws.url}`); this._progress && this._progress.logger.info(`<ws disconnecting> ${this._ws.url}`);
this._ws.close(); this._ws.close();
} }
@ -229,9 +227,3 @@ export class InterceptingTransport implements ConnectionTransport {
this._delegate.close(); this._delegate.close();
} }
} }
export const protocolLog: Log = {
name: 'protocol',
severity: 'verbose',
color: 'green'
};

View file

@ -177,4 +177,11 @@ export type ProxySettings = {
password?: string password?: string
}; };
export type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
export interface Logger {
isEnabled(name: string, severity: LoggerSeverity): boolean;
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }): void;
}
export type WaitForEventOptions = Function | { predicate?: Function, timeout?: number }; export type WaitForEventOptions = Function | { predicate?: Function, timeout?: number };

View file

@ -18,7 +18,6 @@ import * as path from 'path';
// NOTE: update this to point to playwright/lib when moving this file. // NOTE: update this to point to playwright/lib when moving this file.
const PLAYWRIGHT_LIB_PATH = path.normalize(path.join(__dirname, '..')); const PLAYWRIGHT_LIB_PATH = path.normalize(path.join(__dirname, '..'));
const APICOVERAGE = path.normalize(path.join(__dirname, '..', '..', 'test', 'apicoverage'));
type ParsedStackFrame = { filePath: string, functionName: string }; type ParsedStackFrame = { filePath: string, functionName: string };
@ -61,26 +60,6 @@ export function getCallerFilePath(ignorePrefix = PLAYWRIGHT_LIB_PATH): string |
return null; return null;
} }
export function getCurrentApiCall(prefix = PLAYWRIGHT_LIB_PATH): string {
const error = new Error();
const stackFrames = (error.stack || '').split('\n').slice(1);
// Find last stackframe that points to prefix - that should be the api call.
let apiName: string = '';
for (const frame of stackFrames) {
const parsed = parseStackFrame(frame);
if (!parsed || (!parsed.filePath.startsWith(prefix) && !parsed.filePath.startsWith(APICOVERAGE) && parsed.filePath !== __filename))
break;
apiName = parsed.functionName;
}
const parts = apiName.split('.');
if (parts.length && parts[0].length) {
parts[0] = parts[0][0].toLowerCase() + parts[0].substring(1);
if (parts[0] === 'webKit')
parts[0] = 'webkit';
}
return parts.join('.');
}
export function rewriteErrorMessage(e: Error, newMessage: string): Error { export function rewriteErrorMessage(e: Error, newMessage: string): Error {
if (e.stack) { if (e.stack) {
const index = e.stack.indexOf(e.message); const index = e.stack.indexOf(e.message);

View file

@ -51,7 +51,7 @@ export class WKBrowser extends BrowserBase {
constructor(transport: ConnectionTransport, options: BrowserOptions) { constructor(transport: ConnectionTransport, options: BrowserOptions) {
super(options); super(options);
this._connection = new WKConnection(transport, options.logger, this._onDisconnect.bind(this)); this._connection = new WKConnection(transport, options.loggers, this._onDisconnect.bind(this));
this._browserSession = this._connection.browserSession; this._browserSession = this._connection.browserSession;
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._browserSession, 'Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)), helper.addEventListener(this._browserSession, 'Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)),

View file

@ -17,9 +17,9 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { assert } from '../helper'; import { assert } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { InnerLogger, errorLog } from '../logger'; import { Loggers, Logger } from '../logger';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
// WKPlaywright uses this special id to issue Browser.close command which we // WKPlaywright uses this special id to issue Browser.close command which we
@ -37,11 +37,11 @@ export class WKConnection {
private _lastId = 0; private _lastId = 0;
private _closed = false; private _closed = false;
readonly browserSession: WKSession; readonly browserSession: WKSession;
readonly _logger: InnerLogger; readonly _logger: Logger;
constructor(transport: ConnectionTransport, logger: InnerLogger, onDisconnect: () => void) { constructor(transport: ConnectionTransport, loggers: Loggers, onDisconnect: () => void) {
this._transport = transport; this._transport = transport;
this._logger = logger; this._logger = loggers.protocol;
this._transport.onmessage = this._dispatchMessage.bind(this); this._transport.onmessage = this._dispatchMessage.bind(this);
this._transport.onclose = this._onClose.bind(this); this._transport.onclose = this._onClose.bind(this);
this._onDisconnect = onDisconnect; this._onDisconnect = onDisconnect;
@ -55,14 +55,14 @@ export class WKConnection {
} }
rawSend(message: ProtocolRequest) { rawSend(message: ProtocolRequest) {
if (this._logger.isLogEnabled(protocolLog)) if (this._logger.isEnabled())
this._logger.log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); this._logger.info('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
this._transport.send(message); this._transport.send(message);
} }
private _dispatchMessage(message: ProtocolResponse) { private _dispatchMessage(message: ProtocolResponse) {
if (this._logger.isLogEnabled(protocolLog)) if (this._logger.isEnabled())
this._logger.log(protocolLog, '◀ RECV ' + JSON.stringify(message)); this._logger.info('◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.pageProxyId) { if (message.pageProxyId) {
@ -139,7 +139,7 @@ export class WKSession extends EventEmitter {
sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> { sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
return this.send(method, params).catch(error => { return this.send(method, params).catch(error => {
this.connection._logger.log(errorLog, error, []); this.connection._logger.error(error);
}); });
} }

View file

@ -363,7 +363,7 @@ describe('launchPersistentContext()', function() {
const userDataDir = await makeUserDataDir(); const userDataDir = await makeUserDataDir();
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) }; const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e); const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error.message).toContain(`Timeout 5000ms exceeded during ${browserType.name()}.launchPersistentContext.`); expect(error.message).toContain(`Timeout 5000ms exceeded during browserType.launchPersistentContext.`);
await removeUserDataDir(userDataDir); await removeUserDataDir(userDataDir);
}); });
it('should handle exception', async({browserType, defaultBrowserOptions}) => { it('should handle exception', async({browserType, defaultBrowserOptions}) => {

View file

@ -187,10 +187,4 @@ describe('StackTrace', () => {
}); });
expect(filePath).toBe(__filename); expect(filePath).toBe(__filename);
}); });
it('api call', async state => {
const stackTrace = require(path.join(state.playwrightPath, 'lib', 'utils', 'stackTrace'));
const callme = require('./fixtures/callback');
const apiCall = callme(stackTrace.getCurrentApiCall.bind(stackTrace, path.join(__dirname, 'fixtures') + path.sep));
expect(apiCall).toBe('callme');
});
}); });

View file

@ -53,7 +53,7 @@ describe('Playwright', function() {
it('should handle timeout', async({browserType, defaultBrowserOptions}) => { it('should handle timeout', async({browserType, defaultBrowserOptions}) => {
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) }; const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launch(options).catch(e => e); const error = await browserType.launch(options).catch(e => e);
expect(error.message).toContain(`Timeout 5000ms exceeded during ${browserType.name()}.launch.`); expect(error.message).toContain(`Timeout 5000ms exceeded during browserType.launch.`);
expect(error.message).toContain(`[browser] <launching>`); expect(error.message).toContain(`[browser] <launching>`);
expect(error.message).toContain(`[browser] <launched> pid=`); expect(error.message).toContain(`[browser] <launched> pid=`);
}); });