chore: remove logger infrastructure from server side (#3487)

We do not implement LoggerSink on the server, so we can
use a simple debugLogger.
This commit is contained in:
Dmitry Gozman 2020-08-17 14:12:31 -07:00 committed by GitHub
parent f3c2584dfc
commit 1e9c0eb705
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 188 additions and 408 deletions

View File

@ -20,9 +20,7 @@ import { Page } from './page';
import { EventEmitter } from 'events';
import { Download } from './download';
import { Events } from './events';
import { Loggers } from './logger';
import { ProxySettings } from './types';
import { LoggerSink } from './loggerSink';
import { ChildProcess } from 'child_process';
export interface BrowserProcess {
@ -34,7 +32,6 @@ export interface BrowserProcess {
export type BrowserOptions = {
name: string,
loggers: Loggers,
downloadsPath?: string,
headful?: boolean,
persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
@ -43,7 +40,7 @@ export type BrowserOptions = {
proxy?: ProxySettings,
};
export type BrowserContextOptions = types.BrowserContextOptions & { logger?: LoggerSink };
export type BrowserContextOptions = types.BrowserContextOptions;
export interface Browser extends EventEmitter {
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;

View File

@ -24,11 +24,9 @@ import * as types from './types';
import { Events } from './events';
import { Download } from './download';
import { BrowserBase } from './browser';
import { Loggers, Logger } from './logger';
import { EventEmitter } from 'events';
import { ProgressController } from './progress';
import { DebugController } from './debug/debugController';
import { LoggerSink } from './loggerSink';
export interface BrowserContext extends EventEmitter {
setDefaultNavigationTimeout(timeout: number): void;
@ -53,12 +51,10 @@ export interface BrowserContext extends EventEmitter {
close(): Promise<void>;
}
type BrowserContextOptions = types.BrowserContextOptions & { logger?: LoggerSink };
export abstract class BrowserContextBase extends EventEmitter implements BrowserContext {
readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>();
readonly _options: BrowserContextOptions;
readonly _options: types.BrowserContextOptions;
_routes: { url: types.URLMatch, handler: network.RouteHandler }[] = [];
private _isPersistentContext: boolean;
private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
@ -67,14 +63,11 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
readonly _permissions = new Map<string, string[]>();
readonly _downloads = new Set<Download>();
readonly _browserBase: BrowserBase;
readonly _apiLogger: Logger;
constructor(browserBase: BrowserBase, options: BrowserContextOptions, isPersistentContext: boolean) {
constructor(browserBase: BrowserBase, options: types.BrowserContextOptions, isPersistentContext: boolean) {
super();
this._browserBase = browserBase;
this._options = options;
const loggers = options.logger ? new Loggers(options.logger) : browserBase._options.loggers;
this._apiLogger = loggers.api;
this._isPersistentContext = isPersistentContext;
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
}
@ -86,7 +79,7 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
const progressController = new ProgressController(this._apiLogger, this._timeoutSettings.timeout(options));
const progressController = new ProgressController(this._timeoutSettings.timeout(options));
if (event !== Events.BrowserContext.Close)
this._closePromise.then(error => progressController.abort(error));
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate).promise);
@ -248,10 +241,10 @@ export function assertBrowserContextIsNotOwned(context: BrowserContextBase) {
}
}
export function validateBrowserContextOptions(options: BrowserContextOptions): BrowserContextOptions {
export function validateBrowserContextOptions(options: types.BrowserContextOptions): types.BrowserContextOptions {
// Copy all fields manually to strip any extra junk.
// Especially useful when we share context and launch options for launchPersistent.
const result: BrowserContextOptions = {
const result: types.BrowserContextOptions = {
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
bypassCSP: options.bypassCSP,
locale: options.locale,
@ -269,7 +262,6 @@ export function validateBrowserContextOptions(options: BrowserContextOptions): B
deviceScaleFactor: options.deviceScaleFactor,
isMobile: options.isMobile,
hasTouch: options.hasTouch,
logger: options.logger,
};
if (result.viewport === null && result.deviceScaleFactor !== undefined)
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);

View File

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

View File

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

View File

@ -664,7 +664,6 @@ class FrameSession {
_onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
this._page._logger,
event.type,
event.message,
async (accept: boolean, promptText?: string) => {

View File

@ -15,28 +15,25 @@
* limitations under the License.
*/
import { assert } from './helper';
import { Logger } from './logger';
import { assert, debugLogger } from './helper';
type OnHandle = (accept: boolean, promptText?: string) => Promise<void>;
export type DialogType = 'alert' | 'beforeunload' | 'confirm' | 'prompt';
export class Dialog {
private _logger: Logger;
private _type: string;
private _message: string;
private _onHandle: OnHandle;
private _handled = false;
private _defaultValue: string;
constructor(logger: Logger, type: string, message: string, onHandle: OnHandle, defaultValue?: string) {
this._logger = logger;
constructor(type: string, message: string, onHandle: OnHandle, defaultValue?: string) {
this._type = type;
this._message = message;
this._onHandle = onHandle;
this._defaultValue = defaultValue || '';
this._logger.info(` ${this._preview()} was shown`);
debugLogger.log('api', ` ${this._preview()} was shown`);
}
type(): string {
@ -54,14 +51,14 @@ export class Dialog {
async accept(promptText: string | undefined) {
assert(!this._handled, 'Cannot accept dialog which is already handled!');
this._handled = true;
this._logger.info(` ${this._preview()} was accepted`);
debugLogger.log('api', ` ${this._preview()} was accepted`);
await this._onHandle(true, promptText);
}
async dismiss() {
assert(!this._handled, 'Cannot dismiss dialog which is already handled!');
this._handled = true;
this._logger.info(` ${this._preview()} was dismissed`);
debugLogger.log('api', ` ${this._preview()} was dismissed`);
await this._onHandle(false);
}

View File

@ -291,25 +291,25 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
let first = true;
while (progress.isRunning()) {
progress.logger.info(`${first ? 'attempting' : 'retrying'} ${actionName} action`);
progress.log(`${first ? 'attempting' : 'retrying'} ${actionName} action`);
const result = await this._performPointerAction(progress, actionName, waitForEnabled, action, options);
first = false;
if (result === 'error:notvisible') {
if (options.force)
throw new Error('Element is not visible');
progress.logger.info(' element is not visible');
progress.log(' element is not visible');
continue;
}
if (result === 'error:notinviewport') {
if (options.force)
throw new Error('Element is outside of the viewport');
progress.logger.info(' element is outside of the viewport');
progress.log(' element is outside of the viewport');
continue;
}
if (typeof result === 'object' && 'hitTargetDescription' in result) {
if (options.force)
throw new Error(`Element does not receive pointer events, ${result.hitTargetDescription} intercepts them`);
progress.logger.info(` ${result.hitTargetDescription} intercepts pointer events`);
progress.log(` ${result.hitTargetDescription} intercepts pointer events`);
continue;
}
return result;
@ -329,12 +329,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if ((options as any).__testHookAfterStable)
await (options as any).__testHookAfterStable();
progress.logger.info(' scrolling into view if needed');
progress.log(' scrolling into view if needed');
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);
if (scrolled !== 'done')
return scrolled;
progress.logger.info(' done scrolling');
progress.log(' done scrolling');
const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
if (typeof maybePoint === 'string')
@ -344,11 +344,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (!force) {
if ((options as any).__testHookBeforeHitTarget)
await (options as any).__testHookBeforeHitTarget();
progress.logger.info(` checking that element receives pointer events at (${point.x},${point.y})`);
progress.log(` checking that element receives pointer events at (${point.x},${point.y})`);
const hitTargetResult = await this._checkHitTargetAt(point);
if (hitTargetResult !== 'done')
return hitTargetResult;
progress.logger.info(` element does receive pointer events`);
progress.log(` element does receive pointer events`);
}
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
@ -358,16 +358,16 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
let restoreModifiers: types.KeyboardModifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
progress.logger.info(` performing ${actionName} action`);
progress.log(` performing ${actionName} action`);
await action(point);
progress.logger.info(` ${actionName} action done`);
progress.logger.info(' waiting for scheduled navigations to finish');
progress.log(` ${actionName} action done`);
progress.log(' waiting for scheduled navigations to finish');
if ((options as any).__testHookAfterPointerAction)
await (options as any).__testHookAfterPointerAction();
if (restoreModifiers)
await this._page.keyboard._ensureModifiers(restoreModifiers);
}, 'input');
progress.logger.info(' navigations have finished');
progress.log(' navigations have finished');
return 'done';
}
@ -448,10 +448,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
progress.logger.info(`elementHandle.fill("${value}")`);
progress.log(`elementHandle.fill("${value}")`);
assert(helper.isString(value), `value: expected string, got ${typeof value}`);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.logger.info(' waiting for element to be visible, enabled and editable');
progress.log(' waiting for element to be visible, enabled and editable');
const poll = await this._evaluateHandleInUtility(([injected, node, value]) => {
return injected.waitForEnabledAndFill(node, value);
}, value);
@ -460,7 +460,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
progress.throwIfAborted(); // Avoid action that has side-effects.
if (filled === 'error:notconnected')
return filled;
progress.logger.info(' element is visible, enabled and editable');
progress.log(' element is visible, enabled and editable');
if (filled === 'needsinput') {
progress.throwIfAborted(); // Avoid action that has side-effects.
if (value)
@ -534,7 +534,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
progress.logger.info(`elementHandle.type("${text}")`);
progress.log(`elementHandle.type("${text}")`);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
if (result !== 'done')
@ -553,7 +553,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
progress.logger.info(`elementHandle.press("${key}")`);
progress.log(`elementHandle.press("${key}")`);
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
if (result !== 'done')
@ -644,7 +644,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const info = selectors._parseSelector(selector);
const task = waitForSelectorTask(info, state, this);
return this._page._runAbortableTask(async progress => {
progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
const context = await this._context.frame._context(info.world);
const injected = await context.injectedScript();
const pollHandler = new InjectedScriptPollHandler(progress, await task(injected));
@ -669,9 +669,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
async _waitForDisplayedAtStablePosition(progress: Progress, waitForEnabled: boolean): Promise<'error:notconnected' | 'done'> {
if (waitForEnabled)
progress.logger.info(` waiting for element to be visible, enabled and not moving`);
progress.log(` waiting for element to be visible, enabled and not moving`);
else
progress.logger.info(` waiting for element to be visible and not moving`);
progress.log(` waiting for element to be visible and not moving`);
const rafCount = this._page._delegate.rafCountForStablePosition();
const poll = this._evaluateHandleInUtility(([injected, node, { rafCount, waitForEnabled }]) => {
return injected.waitForDisplayedAtStablePosition(node, rafCount, waitForEnabled);
@ -679,9 +679,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const pollHandler = new InjectedScriptPollHandler(progress, await poll);
const result = await pollHandler.finish();
if (waitForEnabled)
progress.logger.info(' element is visible, enabled and does not move');
progress.log(' element is visible, enabled and does not move');
else
progress.logger.info(' element is visible and does not move');
progress.log(' element is visible and does not move');
return result;
}
@ -722,7 +722,7 @@ export class InjectedScriptPollHandler<T> {
if (!this._poll || !this._progress.isRunning())
return;
for (const message of messages)
this._progress.logger.info(message);
this._progress.log(message);
}
}
@ -752,7 +752,7 @@ export class InjectedScriptPollHandler<T> {
// Retrieve all the logs before continuing.
const messages = await this._poll.evaluate(poll => poll.takeLastLogs()).catch(e => [] as string[]);
for (const message of messages)
this._progress.logger.info(message);
this._progress.log(message);
}
async cancel() {

View File

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

View File

@ -16,10 +16,9 @@
*/
import { EventEmitter } from 'events';
import { assert } from '../helper';
import { assert, debugLogger } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol';
import { Loggers, Logger } from '../logger';
import { rewriteErrorMessage } from '../utils/stackTrace';
export const ConnectionEvents = {
@ -34,7 +33,6 @@ export class FFConnection extends EventEmitter {
private _lastId: number;
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
private _transport: ConnectionTransport;
readonly _logger: Logger;
readonly _sessions: Map<string, FFSession>;
_closed: boolean;
@ -44,10 +42,9 @@ 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;
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, loggers: Loggers) {
constructor(transport: ConnectionTransport) {
super();
this._transport = transport;
this._logger = loggers.protocol;
this._lastId = 0;
this._callbacks = new Map();
@ -79,14 +76,14 @@ export class FFConnection extends EventEmitter {
}
_rawSend(message: ProtocolRequest) {
if (this._logger.isEnabled())
this._logger.info('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
if (debugLogger.isEnabled('protocol'))
debugLogger.log('protocol', 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
this._transport.send(message);
}
async _onMessage(message: ProtocolResponse) {
if (this._logger.isEnabled())
this._logger.info('◀ RECV ' + JSON.stringify(message));
if (debugLogger.isEnabled('protocol'))
debugLogger.log('protocol', '◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId)
return;
if (message.sessionId) {
@ -186,9 +183,7 @@ export class FFSession extends EventEmitter {
}
sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
return this.send(method, params).catch(error => {
this._connection._logger.error(error);
});
return this.send(method, params).catch(error => debugLogger.log('error', error));
}
dispatchMessage(object: ProtocolResponse) {

View File

@ -186,7 +186,6 @@ export class FFPage implements PageDelegate {
_onDialogOpened(params: Protocol.Page.dialogOpenedPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
this._page._logger,
params.type,
params.message,
async (accept: boolean, promptText?: string) => {

View File

@ -20,7 +20,7 @@ import * as util from 'util';
import { ConsoleMessage } from './console';
import * as dom from './dom';
import { Events } from './events';
import { assert, helper, RegisteredListener, assertMaxArguments } from './helper';
import { assert, helper, RegisteredListener, assertMaxArguments, debugLogger } from './helper';
import * as js from './javascript';
import * as network from './network';
import { Page } from './page';
@ -201,7 +201,7 @@ export class FrameManager {
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument };
frame._eventEmitter.emit(kNavigationEvent, navigationEvent);
if (!initial) {
this._page._logger.info(` navigated to "${url}"`);
debugLogger.log('api', ` navigated to "${url}"`);
this._page.emit(Events.Page.FrameNavigated, frame);
}
// Restore pending if any - see comments above about keepPending.
@ -215,7 +215,7 @@ export class FrameManager {
frame._url = url;
const navigationEvent: NavigationEvent = { url, name: frame._name };
frame._eventEmitter.emit(kNavigationEvent, navigationEvent);
this._page._logger.info(` navigated to "${url}"`);
debugLogger.log('api', ` navigated to "${url}"`);
this._page.emit(Events.Page.FrameNavigated, frame);
}
@ -414,7 +414,7 @@ export class Frame {
if (!this._subtreeLifecycleEvents.has(event)) {
this._eventEmitter.emit(kAddLifecycleEvent, event);
if (this === mainFrame && this._url !== 'about:blank')
this._page._logger.info(` "${event}" event fired`);
debugLogger.log('api', ` "${event}" event fired`);
if (this === mainFrame && event === 'load')
this._page.emit(Events.Page.Load);
if (this === mainFrame && event === 'domcontentloaded')
@ -431,7 +431,7 @@ export class Frame {
async goto(url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
return runNavigationTask(this, options, async progress => {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.logger.info(`navigating to "${url}", waiting until "${waitUntil}"`);
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
const headers = (this._page._state.extraHTTPHeaders || {});
let referer = headers['referer'] || headers['Referer'];
if (options.referer !== undefined) {
@ -476,13 +476,13 @@ export class Frame {
return runNavigationTask(this, options, async progress => {
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.logger.info(`waiting for navigation${toUrl} until "${waitUntil}"`);
progress.log(`waiting for navigation${toUrl} until "${waitUntil}"`);
const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this._eventEmitter, kNavigationEvent, (event: NavigationEvent) => {
// Any failed navigation results in a rejection.
if (event.error)
return true;
progress.logger.info(` navigated to "${this._url}"`);
progress.log(` navigated to "${this._url}"`);
return helper.urlMatches(this._url, options.url);
}).promise;
if (navigationEvent.error)
@ -565,7 +565,7 @@ export class Frame {
const info = selectors._parseSelector(selector);
const task = dom.waitForSelectorTask(info, state);
return this._page._runAbortableTask(async progress => {
progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
const result = await this._scheduleRerunnableHandleTask(progress, info.world, task);
if (!result.asElement()) {
result.dispose();
@ -580,7 +580,7 @@ export class Frame {
const info = selectors._parseSelector(selector);
const task = dom.dispatchEventTask(info, type, eventInit || {});
return this._page._runAbortableTask(async progress => {
progress.logger.info(`Dispatching "${type}" event on selector "${selector}"...`);
progress.log(`Dispatching "${type}" event on selector "${selector}"...`);
// Note: we always dispatch events in the main world.
await this._scheduleRerunnableTask(progress, 'main', task);
}, this._page._timeoutSettings.timeout(options));
@ -635,7 +635,7 @@ export class Frame {
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
return runNavigationTask(this, options, async progress => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.logger.info(`setting frame content, waiting until "${waitUntil}"`);
progress.log(`setting frame content, waiting until "${waitUntil}"`);
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const context = await this._utilityContext();
const lifecyclePromise = new Promise((resolve, reject) => {
@ -822,7 +822,7 @@ export class Frame {
const info = selectors._parseSelector(selector);
return this._page._runAbortableTask(async progress => {
while (progress.isRunning()) {
progress.logger.info(`waiting for selector "${selector}"`);
progress.log(`waiting for selector "${selector}"`);
const task = dom.waitForSelectorTask(info, 'attached');
const handle = await this._scheduleRerunnableHandleTask(progress, info.world, task);
const element = handle.asElement() as dom.ElementHandle<Element>;
@ -834,7 +834,7 @@ export class Frame {
const result = await action(progress, element);
element.dispose();
if (result === 'error:notconnected') {
progress.logger.info('element was detached from the DOM, retrying');
progress.log('element was detached from the DOM, retrying');
continue;
}
return result;
@ -863,7 +863,7 @@ export class Frame {
const info = selectors._parseSelector(selector);
const task = dom.textContentTask(info);
return this._page._runAbortableTask(async progress => {
progress.logger.info(` retrieving textContent from "${selector}"`);
progress.log(` retrieving textContent from "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
@ -872,7 +872,7 @@ export class Frame {
const info = selectors._parseSelector(selector);
const task = dom.innerTextTask(info);
return this._page._runAbortableTask(async progress => {
progress.logger.info(` retrieving innerText from "${selector}"`);
progress.log(` retrieving innerText from "${selector}"`);
const result = dom.throwFatalDOMError(await this._scheduleRerunnableTask(progress, info.world, task));
return result.innerText;
}, this._page._timeoutSettings.timeout(options));
@ -882,7 +882,7 @@ export class Frame {
const info = selectors._parseSelector(selector);
const task = dom.innerHTMLTask(info);
return this._page._runAbortableTask(async progress => {
progress.logger.info(` retrieving innerHTML from "${selector}"`);
progress.log(` retrieving innerHTML from "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
@ -891,7 +891,7 @@ export class Frame {
const info = selectors._parseSelector(selector);
const task = dom.getAttributeTask(info, name);
return this._page._runAbortableTask(async progress => {
progress.logger.info(` retrieving attribute "${name}" from "${selector}"`);
progress.log(` retrieving attribute "${name}" from "${selector}"`);
return this._scheduleRerunnableTask(progress, info.world, task);
}, this._page._timeoutSettings.timeout(options));
}
@ -1104,7 +1104,7 @@ class SignalBarrier {
this.retain();
const waiter = helper.waitForEvent(null, frame._eventEmitter, kNavigationEvent, (e: NavigationEvent) => {
if (!e.error && this._progress)
this._progress.logger.info(` navigated to "${frame._url}"`);
this._progress.log(` navigated to "${frame._url}"`);
return true;
});
await Promise.race([
@ -1130,7 +1130,7 @@ class SignalBarrier {
async function runNavigationTask<T>(frame: Frame, options: types.TimeoutOptions, task: (progress: Progress) => Promise<T>): Promise<T> {
const page = frame._page;
const controller = new ProgressController(page._logger, page._timeoutSettings.navigationTimeout(options));
const controller = new ProgressController(page._timeoutSettings.navigationTimeout(options));
page._disconnectedPromise.then(() => controller.abort(new Error('Navigation failed because page was closed!')));
page._crashedPromise.then(() => controller.abort(new Error('Navigation failed because page crashed!')));
frame._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!')));

View File

@ -23,6 +23,7 @@ import * as removeFolder from 'rimraf';
import * as util from 'util';
import * as types from './types';
import { Progress } from './progress';
import * as debug from 'debug';
const removeFolderAsync = util.promisify(removeFolder);
const readFileAsync = util.promisify(fs.readFile.bind(fs));
@ -384,3 +385,48 @@ export function logPolitely(toBeLogged: string) {
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
export const helper = Helper;
const debugLoggerColorMap = {
'api': 45, // cyan
'protocol': 34, // green
'browser': 0, // reset
'error': 160, // red,
'channel:command': 33, // blue
'channel:response': 202, // orange
'channel:event': 207, // magenta
};
export type LogName = keyof typeof debugLoggerColorMap;
export class DebugLogger {
private _debuggers = new Map<string, debug.IDebugger>();
constructor() {
if (process.env.DEBUG_FILE) {
const ansiRegex = new RegExp([
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
].join('|'), 'g');
const stream = fs.createWriteStream(process.env.DEBUG_FILE);
(debug as any).log = (data: string) => {
stream.write(data.replace(ansiRegex, ''));
stream.write('\n');
};
}
}
log(name: LogName, message: string | Error | object) {
let cachedDebugger = this._debuggers.get(name);
if (!cachedDebugger) {
cachedDebugger = debug(`pw:${name}`);
this._debuggers.set(name, cachedDebugger);
(cachedDebugger as any).color = debugLoggerColorMap[name];
}
cachedDebugger(message);
}
isEnabled(name: LogName) {
return debug.enabled(`pw:${name}`);
}
}
export const debugLogger = new DebugLogger();

View File

@ -1,190 +0,0 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* 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 * as debug from 'debug';
import * as fs from 'fs';
import { helper } from './helper';
import { LoggerSink, LoggerSeverity } from './loggerSink';
export function logError(logger: Logger): (error: Error) => void {
return error => logger.error(error);
}
export class Logger {
private _loggerSink: LoggerSink;
private _name: string;
private _hints: { color?: string; };
private _scopeName: string | undefined;
private _recording: string[] | undefined;
constructor(loggerSink: LoggerSink, name: string, hints: { color?: string }, scopeName?: string, record?: boolean) {
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 | undefined, record?: boolean): Logger {
if (scopeName)
this._loggerSink.log(this._name, 'info', `=> ${scopeName} started`, [], this._hints);
return new Logger(this._loggerSink, this._name, this._hints, scopeName, record);
}
endScope(status: string) {
if (this._scopeName)
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 Loggers {
readonly api: Logger;
readonly browser: Logger;
readonly protocol: Logger;
constructor(userSink: LoggerSink | undefined) {
const loggerSink = new MultiplexingLoggerSink();
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);
}
get(id: string): LoggerSink | undefined {
return this._loggers.get(id);
}
remove(id: string) {
this._loggers.delete(id);
}
isEnabled(name: string, severity: LoggerSeverity): boolean {
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);
}
}
}
const colorMap = new Map<string, number>([
['red', 160],
['green', 34],
['yellow', 172],
['blue', 33],
['magenta', 207],
['cyan', 45],
['reset', 0],
]);
export class DebugLoggerSink {
private _debuggers = new Map<string, debug.IDebugger>();
constructor() {
if (process.env.DEBUG_FILE) {
const ansiRegex = new RegExp([
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
].join('|'), 'g');
const stream = fs.createWriteStream(process.env.DEBUG_FILE);
(debug as any).log = (data: string) => {
stream.write(data.replace(ansiRegex, ''));
stream.write('\n');
};
}
}
isEnabled(name: string, severity: LoggerSeverity): boolean {
return debug.enabled(`pw:${name}`);
}
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }) {
let cachedDebugger = this._debuggers.get(name);
if (!cachedDebugger) {
cachedDebugger = debug(`pw:${name}`);
this._debuggers.set(name, cachedDebugger);
let color = hints.color || 'reset';
switch (severity) {
case 'error': color = 'red'; break;
case 'warning': color = 'yellow'; break;
}
const escaped = colorMap.get(color) || 0;
if (escaped)
(cachedDebugger as any).color = String(escaped);
}
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

@ -1,25 +0,0 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* 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.
*/
export type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
export interface LoggerSink {
isEnabled(name: string, severity: LoggerSeverity): boolean;
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }): void;
}
// This is a workaround for the documentation generation.
export interface Logger extends LoggerSink {}

View File

@ -17,7 +17,7 @@
import * as dom from './dom';
import * as frames from './frames';
import { assert, helper, Listener, assertMaxArguments } from './helper';
import { assert, helper, Listener, assertMaxArguments, debugLogger } from './helper';
import * as input from './input';
import * as js from './javascript';
import * as network from './network';
@ -30,7 +30,6 @@ import { ConsoleMessage } from './console';
import * as accessibility from './accessibility';
import { EventEmitter } from 'events';
import { FileChooser } from './fileChooser';
import { logError, Logger } from './logger';
import { ProgressController, Progress, runAbortableTask } from './progress';
export interface PageDelegate {
@ -105,7 +104,6 @@ export class Page extends EventEmitter {
readonly mouse: input.Mouse;
readonly _timeoutSettings: TimeoutSettings;
readonly _delegate: PageDelegate;
readonly _logger: Logger;
readonly _state: PageState;
readonly _pageBindings = new Map<string, PageBinding>();
readonly _evaluateOnNewDocumentSources: string[] = [];
@ -121,7 +119,6 @@ export class Page extends EventEmitter {
constructor(delegate: PageDelegate, browserContext: BrowserContextBase) {
super();
this._delegate = delegate;
this._logger = browserContext._apiLogger;
this._closedCallback = () => {};
this._closedPromise = new Promise(f => this._closedCallback = f);
this._disconnectedCallback = () => {};
@ -170,7 +167,7 @@ export class Page extends EventEmitter {
async _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number): Promise<T> {
return runAbortableTask(async progress => {
return task(progress);
}, this._logger, timeout);
}, timeout);
}
async _onFileChooserOpened(handle: dom.ElementHandle) {
@ -278,7 +275,7 @@ export class Page extends EventEmitter {
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options));
const progressController = new ProgressController(this._timeoutSettings.timeout(options));
this._disconnectedPromise.then(error => progressController.abort(error));
if (event !== Events.Page.Crash)
this._crashedPromise.then(error => progressController.abort(error));
@ -524,12 +521,12 @@ export class PageBinding {
if (!binding)
binding = page._browserContext._pageBindings.get(name);
const result = await binding!.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
context.evaluateInternal(deliverResult, { name, seq, result }).catch(logError(page._logger));
context.evaluateInternal(deliverResult, { name, seq, result }).catch(e => debugLogger.log('error', e));
} catch (error) {
if (helper.isError(error))
context.evaluateInternal(deliverError, { name, seq, message: error.message, stack: error.stack }).catch(logError(page._logger));
context.evaluateInternal(deliverError, { name, seq, message: error.message, stack: error.stack }).catch(e => debugLogger.log('error', e));
else
context.evaluateInternal(deliverErrorValue, { name, seq, error }).catch(logError(page._logger));
context.evaluateInternal(deliverErrorValue, { name, seq, error }).catch(e => debugLogger.log('error', e));
}
function deliverResult(arg: { name: string, seq: number, result: any }) {

View File

@ -14,22 +14,21 @@
* limitations under the License.
*/
import { Logger } from './logger';
import { TimeoutError } from './errors';
import { assert } from './helper';
import { assert, debugLogger, LogName } from './helper';
import { rewriteErrorMessage } from './utils/stackTrace';
export interface Progress {
readonly aborted: Promise<void>;
readonly logger: Logger;
log(message: string): void;
timeUntilDeadline(): number;
isRunning(): boolean;
cleanupWhenAborted(cleanup: () => any): void;
throwIfAborted(): void;
}
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, logger: Logger, timeout: number): Promise<T> {
const controller = new ProgressController(logger, timeout);
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, logName: LogName = 'api'): Promise<T> {
const controller = new ProgressController(timeout, logName);
return controller.run(task);
}
@ -47,14 +46,14 @@ export class ProgressController {
// Cleanups to be run only in the case of abort.
private _cleanups: (() => any)[] = [];
private _logger: Logger;
private _logName: LogName;
private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before';
private _deadline: number;
private _timeout: number;
private _logRecordring: string[] = [];
constructor(logger: Logger, timeout: number) {
this._logger = logger;
constructor(timeout: number, logName: LogName = 'api') {
this._logName = logName;
this._timeout = timeout;
this._deadline = timeout ? monotonicTime() + timeout : 0;
@ -67,11 +66,13 @@ export class ProgressController {
assert(this._state === 'before');
this._state = 'running';
const loggerScope = this._logger.createScope(undefined, true);
const progress: Progress = {
aborted: this._abortedPromise,
logger: loggerScope,
log: message => {
if (this._state === 'running')
this._logRecordring.push(message);
debugLogger.log(this._logName, message);
},
timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node.
isRunning: () => this._state === 'running',
cleanupWhenAborted: (cleanup: () => any) => {
@ -93,17 +94,17 @@ export class ProgressController {
const result = await Promise.race([promise, this._forceAbortPromise]);
clearTimeout(timer);
this._state = 'finished';
loggerScope.endScope('succeeded');
this._logRecordring = [];
return result;
} catch (e) {
this._aborted();
rewriteErrorMessage(e,
e.message +
formatLogRecording(loggerScope.recording()) +
formatLogRecording(this._logRecordring) +
kLoggingNote);
clearTimeout(timer);
this._state = 'aborted';
loggerScope.endScope(`failed`);
this._logRecordring = [];
await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
throw e;
}

View File

@ -24,7 +24,7 @@ export { Dialog } from './dialog';
export { Download } from './download';
export { ElementHandle } from './elementHandle';
export { FileChooser } from './fileChooser';
export { Logger } from '../../loggerSink';
export { Logger } from './types';
export { TimeoutError } from '../../errors';
export { Frame } from './frame';
export { Keyboard, Mouse } from './input';

View File

@ -18,7 +18,7 @@ import { EventEmitter } from 'events';
import type { Channel } from '../channels';
import type { Connection } from './connection';
import type { LoggerSink } from './types';
import { DebugLoggerSink } from '../../logger';
import { debugLogger } from '../../helper';
export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}> extends EventEmitter {
private _connection: Connection;
@ -119,10 +119,8 @@ export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}
}
}
const debugLogger = new DebugLoggerSink();
function logApiCall(logger: LoggerSink | undefined, message: string) {
if (logger && logger.isEnabled('api', 'info'))
logger.log('api', 'info', message, [], { color: 'cyan' });
if (debugLogger.isEnabled('api', 'info'))
debugLogger.log('api', 'info', message, [], { color: 'cyan' });
debugLogger.log('api', message);
}

View File

@ -24,7 +24,6 @@ import { JSHandle } from './jsHandle';
import { Request, Response, Route } from './network';
import { Page, BindingCall } from './page';
import { Worker } from './worker';
import debug = require('debug');
import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog';
import { Download } from './download';
@ -40,6 +39,7 @@ import { Stream } from './stream';
import { createScheme, Validator, ValidationError } from '../validator';
import { WebKitBrowser } from './webkitBrowser';
import { FirefoxBrowser } from './firefoxBrowser';
import { debugLogger } from '../../helper';
class Root extends ChannelOwner<Channel, {}> {
constructor(connection: Connection) {
@ -73,7 +73,7 @@ export class Connection {
const id = ++this._lastId;
const validated = method === 'debugScopeState' ? params : validateParams(type, method, params);
const converted = { id, guid, method, params: validated };
debug('pw:channel:command')(converted);
debugLogger.log('channel:command', converted);
this.onmessage(converted);
return new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject }));
}
@ -85,7 +85,7 @@ export class Connection {
dispatch(message: object) {
const { id, guid, method, params, result, error } = message as any;
if (id) {
debug('pw:channel:response')(message);
debugLogger.log('channel:response', message);
const callback = this._callbacks.get(id)!;
this._callbacks.delete(id);
if (error)
@ -95,7 +95,7 @@ export class Connection {
return;
}
debug('pw:channel:event')(message);
debugLogger.log('channel:event', message);
if (method === '__create__') {
this._createRemoteObject(guid, params.type, params.guid, params.initializer);
return;

View File

@ -20,7 +20,6 @@ import * as path from 'path';
import * as util from 'util';
import { BrowserContext, verifyProxySettings, validateBrowserContextOptions } from '../browserContext';
import * as browserPaths from '../install/browserPaths';
import { Loggers } from '../logger';
import { ConnectionTransport, WebSocketTransport } from '../transport';
import { BrowserBase, BrowserOptions, Browser, BrowserProcess } from '../browser';
import { assert, helper } from '../helper';
@ -29,16 +28,13 @@ import { PipeTransport } from './pipeTransport';
import { Progress, runAbortableTask } from '../progress';
import * as types from '../types';
import { TimeoutSettings } from '../timeoutSettings';
import { LoggerSink } from '../loggerSink';
import { validateHostRequirements } from './validateDependencies';
type FirefoxPrefsOptions = { firefoxUserPrefs?: { [key: string]: string | number | boolean } };
type LaunchOptions = types.LaunchOptions & { logger?: LoggerSink };
export type LaunchNonPersistentOptions = LaunchOptions & FirefoxPrefsOptions;
type LaunchPersistentOptions = LaunchOptions & types.BrowserContextOptions;
type LaunchServerOptions = types.LaunchServerOptions & { logger?: LoggerSink } & FirefoxPrefsOptions;
export type LaunchNonPersistentOptions = types.LaunchOptions & FirefoxPrefsOptions;
type LaunchPersistentOptions = types.LaunchOptions & types.BrowserContextOptions;
type LaunchServerOptions = types.LaunchServerOptions & FirefoxPrefsOptions;
export interface BrowserType {
executablePath(): string;
@ -84,8 +80,7 @@ export abstract class BrowserTypeBase implements BrowserType {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
options = validateLaunchOptions(options);
const loggers = new Loggers(options.logger);
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, loggers, undefined), loggers.browser, TimeoutSettings.timeout(options)).catch(e => { throw this._rewriteStartupError(e); });
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, undefined), TimeoutSettings.timeout(options), 'browser').catch(e => { throw this._rewriteStartupError(e); });
return browser;
}
@ -93,14 +88,13 @@ export abstract class BrowserTypeBase implements BrowserType {
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
options = validateLaunchOptions(options);
const persistent = validateBrowserContextOptions(options);
const loggers = new Loggers(options.logger);
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, loggers, persistent, userDataDir), loggers.browser, TimeoutSettings.timeout(options)).catch(e => { throw this._rewriteStartupError(e); });
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, persistent, userDataDir), TimeoutSettings.timeout(options), 'browser').catch(e => { throw this._rewriteStartupError(e); });
return browser._defaultContext!;
}
async _innerLaunch(progress: Progress, options: LaunchOptions, logger: Loggers, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
options.proxy = options.proxy ? verifyProxySettings(options.proxy) : undefined;
const { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, logger, userDataDir);
const { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, userDataDir);
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browserOptions: BrowserOptions = {
@ -108,7 +102,6 @@ export abstract class BrowserTypeBase implements BrowserType {
slowMo: options.slowMo,
persistent,
headful: !options.headless,
loggers: logger,
downloadsPath,
browserProcess,
proxy: options.proxy,
@ -122,7 +115,7 @@ export abstract class BrowserTypeBase implements BrowserType {
return browser;
}
private async _launchProcess(progress: Progress, options: LaunchServerOptions, isPersistent: boolean, loggers: Loggers, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, transport: ConnectionTransport }> {
private async _launchProcess(progress: Progress, options: LaunchServerOptions, isPersistent: boolean, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, transport: ConnectionTransport }> {
const {
ignoreDefaultArgs = false,
args = [],
@ -213,7 +206,7 @@ export abstract class BrowserTypeBase implements BrowserType {
transport = await WebSocketTransport.connect(progress, innerEndpoint);
} else {
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
transport = new PipeTransport(stdio[3], stdio[4], loggers.browser);
transport = new PipeTransport(stdio[3], stdio[4]);
}
return { browserProcess, downloadsPath, transport };
}

View File

@ -20,7 +20,6 @@ import { CRConnection, CRSession } from '../chromium/crConnection';
import { CRExecutionContext } from '../chromium/crExecutionContext';
import { Events } from '../events';
import * as js from '../javascript';
import { Loggers, Logger } from '../logger';
import { Page } from '../page';
import { TimeoutSettings } from '../timeoutSettings';
import { WebSocketTransport } from '../transport';
@ -31,7 +30,6 @@ import type {BrowserWindow} from 'electron';
import { runAbortableTask, ProgressController } from '../progress';
import { EventEmitter } from 'events';
import { helper } from '../helper';
import { LoggerSink } from '../loggerSink';
import { BrowserProcess } from '../browser';
export type ElectronLaunchOptionsBase = {
@ -57,7 +55,6 @@ export interface ElectronPage extends Page {
}
export class ElectronApplication extends EventEmitter {
private _apiLogger: Logger;
private _browserContext: CRBrowserContext;
private _nodeConnection: CRConnection;
private _nodeSession: CRSession;
@ -67,9 +64,8 @@ export class ElectronApplication extends EventEmitter {
private _lastWindowId = 0;
readonly _timeoutSettings = new TimeoutSettings();
constructor(logger: Loggers, browser: CRBrowser, nodeConnection: CRConnection) {
constructor(browser: CRBrowser, nodeConnection: CRConnection) {
super();
this._apiLogger = logger.api;
this._browserContext = browser._defaultContext as CRBrowserContext;
this._browserContext.on(Events.BrowserContext.Close, () => {
// Emit application closed after context closed.
@ -136,7 +132,7 @@ export class ElectronApplication extends EventEmitter {
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
const progressController = new ProgressController(this._apiLogger, this._timeoutSettings.timeout(options));
const progressController = new ProgressController(this._timeoutSettings.timeout(options));
if (event !== ElectronEvents.ElectronApplication.Close)
this._browserContext._closePromise.then(error => progressController.abort(error));
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate).promise);
@ -169,7 +165,7 @@ export class ElectronApplication extends EventEmitter {
}
export class Electron {
async launch(executablePath: string, options: ElectronLaunchOptionsBase & { logger?: LoggerSink } = {}): Promise<ElectronApplication> {
async launch(executablePath: string, options: ElectronLaunchOptionsBase = {}): Promise<ElectronApplication> {
const {
args = [],
env = process.env,
@ -177,7 +173,6 @@ export class Electron {
handleSIGTERM = true,
handleSIGHUP = true,
} = options;
const loggers = new Loggers(options.logger);
return runAbortableTask(async progress => {
let app: ElectronApplication | undefined = undefined;
const electronArguments = ['--inspect=0', '--remote-debugging-port=0', '--require', path.join(__dirname, 'electronLoader.js'), ...args];
@ -198,7 +193,7 @@ export class Electron {
const nodeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/);
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
const nodeConnection = new CRConnection(nodeTransport, loggers);
const nodeConnection = new CRConnection(nodeTransport);
const chromeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/);
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
@ -208,10 +203,10 @@ export class Electron {
close: gracefullyClose,
kill
};
const browser = await CRBrowser.connect(chromeTransport, { name: 'electron', headful: true, loggers, persistent: { viewport: null }, browserProcess });
app = new ElectronApplication(loggers, browser, nodeConnection);
const browser = await CRBrowser.connect(chromeTransport, { name: 'electron', headful: true, persistent: { viewport: null }, browserProcess });
app = new ElectronApplication(browser, nodeConnection);
await app._init();
return app;
}, loggers.browser, TimeoutSettings.timeout(options));
}, TimeoutSettings.timeout(options), 'browser');
}
}

View File

@ -15,9 +15,8 @@
* limitations under the License.
*/
import { helper, RegisteredListener } from '../helper';
import { helper, RegisteredListener, debugLogger } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { logError, Logger } from '../logger';
export class PipeTransport implements ConnectionTransport {
private _pipeWrite: NodeJS.WritableStream;
@ -29,7 +28,7 @@ export class PipeTransport implements ConnectionTransport {
onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void;
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream, logger: Logger) {
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) {
this._pipeWrite = pipeWrite;
this._eventListeners = [
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
@ -39,8 +38,8 @@ export class PipeTransport implements ConnectionTransport {
if (this.onclose)
this.onclose.call(null);
}),
helper.addEventListener(pipeRead, 'error', logError(logger)),
helper.addEventListener(pipeWrite, 'error', logError(logger)),
helper.addEventListener(pipeRead, 'error', e => debugLogger.log('error', e)),
helper.addEventListener(pipeWrite, 'error', e => debugLogger.log('error', e)),
];
this.onmessage = undefined;
this.onclose = undefined;

View File

@ -60,7 +60,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
const progress = options.progress;
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
progress.logger.info(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
progress.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
const spawnedProcess = childProcess.spawn(
options.executablePath,
options.args,
@ -82,16 +82,16 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
});
return cleanup().then(() => failedPromise).then(e => Promise.reject(e));
}
progress.logger.info(`<launched> pid=${spawnedProcess.pid}`);
progress.log(`<launched> pid=${spawnedProcess.pid}`);
const stdout = readline.createInterface({ input: spawnedProcess.stdout });
stdout.on('line', (data: string) => {
progress.logger.info(data);
progress.log('[out] ' + data);
});
const stderr = readline.createInterface({ input: spawnedProcess.stderr });
stderr.on('line', (data: string) => {
progress.logger.warn(data);
progress.log('[err] ' + data);
});
let processClosed = false;
@ -100,7 +100,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
let fulfillCleanup = () => {};
const waitForCleanup = new Promise<void>(f => fulfillCleanup = f);
spawnedProcess.once('exit', (exitCode, signal) => {
progress.logger.info(`<process did exit: exitCode=${exitCode}, signal=${signal}>`);
progress.log(`<process did exit: exitCode=${exitCode}, signal=${signal}>`);
processClosed = true;
helper.removeEventListeners(listeners);
gracefullyCloseSet.delete(gracefullyClose);
@ -136,21 +136,21 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
// reentrancy to this function, for example user sends SIGINT second time.
// In this case, let's forcefully kill the process.
if (gracefullyClosing) {
progress.logger.info(`<forecefully close>`);
progress.log(`<forecefully close>`);
killProcess();
await waitForClose; // Ensure the process is dead and we called options.onkill.
return;
}
gracefullyClosing = true;
progress.logger.info(`<gracefully close start>`);
progress.log(`<gracefully close start>`);
await options.attemptToGracefullyClose().catch(() => killProcess());
await waitForCleanup; // Ensure the process is dead and we have cleaned up.
progress.logger.info(`<gracefully close end>`);
progress.log(`<gracefully close end>`);
}
// This method has to be sync to be used as 'exit' event handler.
function killProcess() {
progress.logger.info(`<kill>`);
progress.log(`<kill>`);
helper.removeEventListeners(listeners);
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
// Force kill the browser.

View File

@ -126,7 +126,7 @@ export class WebSocketTransport implements ConnectionTransport {
onclose?: () => void;
static async connect(progress: Progress, url: string): Promise<WebSocketTransport> {
progress.logger.info(`<ws connecting> ${url}`);
progress.log(`<ws connecting> ${url}`);
const transport = new WebSocketTransport(progress, url);
let success = false;
progress.aborted.then(() => {
@ -135,11 +135,11 @@ export class WebSocketTransport implements ConnectionTransport {
});
await new Promise<WebSocketTransport>((fulfill, reject) => {
transport._ws.addEventListener('open', async () => {
progress.logger.info(`<ws connected> ${url}`);
progress.log(`<ws connected> ${url}`);
fulfill(transport);
});
transport._ws.addEventListener('error', event => {
progress.logger.info(`<ws connect error> ${url} ${event.message}`);
progress.log(`<ws connect error> ${url} ${event.message}`);
reject(new Error('WebSocket error: ' + event.message));
transport._ws.close();
});
@ -169,7 +169,7 @@ export class WebSocketTransport implements ConnectionTransport {
});
this._ws.addEventListener('close', event => {
this._progress && this._progress.logger.info(`<ws disconnected> ${url}`);
this._progress && this._progress.log(`<ws disconnected> ${url}`);
if (this.onclose)
this.onclose.call(null);
});
@ -182,7 +182,7 @@ export class WebSocketTransport implements ConnectionTransport {
}
close() {
this._progress && this._progress.logger.info(`<ws disconnecting> ${this._ws.url}`);
this._progress && this._progress.log(`<ws disconnecting> ${this._ws.url}`);
this._ws.close();
}

View File

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

View File

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

View File

@ -522,7 +522,6 @@ export class WKPage implements PageDelegate {
_onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
this._page._logger,
event.type as dialog.DialogType,
event.message,
async (accept: boolean, promptText?: string) => {

View File

@ -64,8 +64,8 @@ it.skip(WIRE)('should handle timeout', async({browserType, defaultBrowserOptions
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launch(options).catch(e => e);
expect(error.message).toContain(`browserType.launch: Timeout 5000ms exceeded.`);
expect(error.message).toContain(`[browser] <launching>`);
expect(error.message).toContain(`[browser] <launched> pid=`);
expect(error.message).toContain(`<launching>`);
expect(error.message).toContain(`<launched> pid=`);
});
it.skip(WIRE)('should handle exception', async({browserType, defaultBrowserOptions}) => {