feat(logging): introduce logger sink api (#1861)

This commit is contained in:
Pavel Feldman 2020-04-20 07:52:26 -07:00 committed by GitHub
parent b8259837a4
commit 1f43ae692f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 461 additions and 290 deletions

View file

@ -27,6 +27,7 @@
- [class: Worker](#class-worker) - [class: Worker](#class-worker)
- [class: BrowserServer](#class-browserserver) - [class: BrowserServer](#class-browserserver)
- [class: BrowserType](#class-browsertype) - [class: BrowserType](#class-browsertype)
- [class: LoggerSink](#class-loggersink)
- [class: ChromiumBrowser](#class-chromiumbrowser) - [class: ChromiumBrowser](#class-chromiumbrowser)
- [class: ChromiumBrowserContext](#class-chromiumbrowsercontext) - [class: ChromiumBrowserContext](#class-chromiumbrowsercontext)
- [class: ChromiumCoverage](#class-chromiumcoverage) - [class: ChromiumCoverage](#class-chromiumcoverage)
@ -3767,6 +3768,7 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
- `options` <[Object]> - `options` <[Object]>
- `wsEndpoint` <[string]> A browser websocket endpoint to connect to. **required** - `wsEndpoint` <[string]> A browser websocket endpoint to connect to. **required**
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0. - `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0.
- `loggerSink` <[LoggerSink]> Sink for log messages.
- returns: <[Promise]<[Browser]>> - returns: <[Promise]<[Browser]>>
This methods attaches Playwright to an existing browser instance. This methods attaches Playwright to an existing browser instance.
@ -3783,8 +3785,8 @@ This methods attaches Playwright to an existing browser instance.
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`. - `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
- `loggerSink` <[LoggerSink]> Sink for log messages.
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. - `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`. - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. - `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. - `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
@ -3816,8 +3818,8 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`. - `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
- `loggerSink` <[LoggerSink]> Sink for log messages.
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. - `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`. - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. - `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0. - `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0.
@ -3835,8 +3837,8 @@ Launches browser instance that uses persistent storage located at `userDataDir`.
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`. - `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`. - `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
- `loggerSink` <[LoggerSink]> Sink for log messages.
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. - `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`. - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. - `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
- returns: <[Promise]<[BrowserServer]>> Promise which resolves to the browser app instance. - returns: <[Promise]<[BrowserServer]>> Promise which resolves to the browser app instance.
@ -3862,6 +3864,44 @@ const { chromium } = require('playwright'); // Or 'webkit' or 'firefox'.
Returns browser name. For example: `'chromium'`, `'webkit'` or `'firefox'`. Returns browser name. For example: `'chromium'`, `'webkit'` or `'firefox'`.
### class: LoggerSink
Playwright generates a lot of logs and they are accessible via the pluggable logger sink.
```js
const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
(async () => {
const browser = await chromium.launch({
loggerSink: {
isEnabled: (name, severity) => name === 'browser',
log: (name, severity, message, args) => console.log(`${name} ${message}`)
}
});
...
})();
```
<!-- GEN:toc -->
- [loggerSink.isEnabled(name, severity)](#loggersinkisenabledname-severity)
- [loggerSink.log(name, severity, message, args, hints)](#loggersinklogname-severity-message-args-hints)
<!-- GEN:stop -->
#### loggerSink.isEnabled(name, severity)
- `name` <[string]> logger name
- `severity` <"verbose"|"info"|"warning"|"error">
- returns: <[boolean]>
Determines whether sink is interested in the logger with the given name and severity.
#### loggerSink.log(name, severity, message, args, hints)
- `name` <[string]> logger name
- `severity` <"verbose"|"info"|"warning"|"error">
- `message` <[string]|[Error]> log message format
- `args` <[Array]<[Object]>> message arguments
- `hints` <[Object]> optional formatting hints
- `color` <[string]> preferred logger color
### class: ChromiumBrowser ### class: ChromiumBrowser
* extends: [Browser] * extends: [Browser]
@ -4218,6 +4258,7 @@ const { chromium } = require('playwright');
[Frame]: #class-frame "Frame" [Frame]: #class-frame "Frame"
[JSHandle]: #class-jshandle "JSHandle" [JSHandle]: #class-jshandle "JSHandle"
[Keyboard]: #class-keyboard "Keyboard" [Keyboard]: #class-keyboard "Keyboard"
[LoggerSink]: #class-loggersink "LoggerSink"
[Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map" [Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map"
[Mouse]: #class-mouse "Mouse" [Mouse]: #class-mouse "Mouse"
[Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object" [Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object"

View file

@ -43,7 +43,6 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"debug": "^4.1.0",
"extract-zip": "^2.0.0", "extract-zip": "^2.0.0",
"https-proxy-agent": "^3.0.0", "https-proxy-agent": "^3.0.0",
"jpeg-js": "^0.3.7", "jpeg-js": "^0.3.7",
@ -68,6 +67,7 @@
"colors": "^1.4.0", "colors": "^1.4.0",
"commonmark": "^0.28.1", "commonmark": "^0.28.1",
"cross-env": "^5.0.5", "cross-env": "^5.0.5",
"debug": "^4.1.0",
"eslint": "^6.6.0", "eslint": "^6.6.0",
"esprima": "^4.0.0", "esprima": "^4.0.0",
"formidable": "^1.2.1", "formidable": "^1.2.1",

View file

@ -22,6 +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 { LoggerSink } from './logger';
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

@ -18,9 +18,9 @@ import { BrowserContext, BrowserContextOptions } from './browserContext';
import { Page } from './page'; import { Page } from './page';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Download } from './download'; import { Download } from './download';
import { debugProtocol } from './transport';
import type { BrowserServer } from './server/browserServer'; import type { BrowserServer } from './server/browserServer';
import { Events } from './events'; import { Events } from './events';
import { Logger, Log } from './logger';
export interface Browser extends EventEmitter { export interface Browser extends EventEmitter {
newContext(options?: BrowserContextOptions): Promise<BrowserContext>; newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
@ -30,11 +30,16 @@ export interface Browser extends EventEmitter {
close(): Promise<void>; close(): Promise<void>;
} }
export abstract class BrowserBase extends EventEmitter implements Browser { export abstract class BrowserBase extends EventEmitter implements Browser, Logger {
_downloadsPath: string = ''; _downloadsPath: string = '';
private _downloads = new Map<string, Download>(); private _downloads = new Map<string, Download>();
_debugProtocol = debugProtocol;
_ownedServer: BrowserServer | null = null; _ownedServer: BrowserServer | null = null;
readonly _logger: Logger;
constructor(logger: Logger) {
super();
this._logger = logger;
}
abstract newContext(options?: BrowserContextOptions): Promise<BrowserContext>; abstract newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
abstract contexts(): BrowserContext[]; abstract contexts(): BrowserContext[];
@ -71,6 +76,14 @@ export abstract class BrowserBase extends EventEmitter implements Browser {
if (this.isConnected()) if (this.isConnected())
await new Promise(x => this.once(Events.Browser.Disconnected, x)); await new Promise(x => this.once(Events.Browser.Disconnected, x));
} }
_isLogEnabled(log: Log): boolean {
return this._logger._isLogEnabled(log);
}
_log(log: Log, message: string | Error, ...args: any[]) {
return this._logger._log(log, message, ...args);
}
} }
export type LaunchType = 'local' | 'server' | 'persistent'; export type LaunchType = 'local' | 'server' | 'persistent';

View file

@ -23,6 +23,8 @@ import * as types from './types';
import { Events } from './events'; import { Events } from './events';
import { ExtendedEventEmitter } from './extendedEventEmitter'; import { ExtendedEventEmitter } from './extendedEventEmitter';
import { Download } from './download'; import { Download } from './download';
import { BrowserBase } from './browser';
import { Log, Logger } from './logger';
export type BrowserContextOptions = { export type BrowserContextOptions = {
viewport?: types.Size | null, viewport?: types.Size | null,
@ -44,7 +46,7 @@ export type BrowserContextOptions = {
acceptDownloads?: boolean acceptDownloads?: boolean
}; };
export interface BrowserContext { export interface BrowserContext extends Logger {
setDefaultNavigationTimeout(timeout: number): void; setDefaultNavigationTimeout(timeout: number): void;
setDefaultTimeout(timeout: number): void; setDefaultTimeout(timeout: number): void;
pages(): Page[]; pages(): Page[];
@ -76,9 +78,11 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
private _closePromiseFulfill: ((error: Error) => void) | undefined; private _closePromiseFulfill: ((error: Error) => void) | undefined;
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;
constructor(options: BrowserContextOptions) { constructor(browserBase: BrowserBase, options: BrowserContextOptions) {
super(); super();
this._browserBase = browserBase;
this._options = options; this._options = options;
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill); this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
} }
@ -149,6 +153,14 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
setDefaultTimeout(timeout: number) { setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout); this._timeoutSettings.setDefaultTimeout(timeout);
} }
_isLogEnabled(log: Log): boolean {
return this._browserBase._isLogEnabled(log);
}
_log(log: Log, message: string | Error, ...args: any[]) {
return this._browserBase._log(log, message, ...args);
}
} }
export function assertBrowserContextIsNotOwned(context: BrowserContextBase) { export function assertBrowserContextIsNotOwned(context: BrowserContextBase) {

View file

@ -18,7 +18,7 @@
import { BrowserBase } from '../browser'; import { BrowserBase } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events as CommonEvents } from '../events'; import { Events as CommonEvents } from '../events';
import { assert, debugError, helper } from '../helper'; import { assert, helper } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding, Worker } from '../page'; import { Page, PageBinding, Worker } from '../page';
import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionTransport, SlowMoTransport } from '../transport';
@ -29,6 +29,7 @@ import { readProtocolStream } from './crProtocolHelper';
import { Events } from './events'; import { Events } from './events';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { CRExecutionContext } from './crExecutionContext'; import { CRExecutionContext } from './crExecutionContext';
import { Logger, logError } from '../logger';
export class CRBrowser extends BrowserBase { export class CRBrowser extends BrowserBase {
readonly _connection: CRConnection; readonly _connection: CRConnection;
@ -46,9 +47,9 @@ export class CRBrowser extends BrowserBase {
private _tracingPath: string | null = ''; private _tracingPath: string | null = '';
private _tracingClient: CRSession | undefined; private _tracingClient: CRSession | undefined;
static async connect(transport: ConnectionTransport, isPersistent: boolean, slowMo?: number): Promise<CRBrowser> { static async connect(transport: ConnectionTransport, isPersistent: boolean, logger: Logger, slowMo?: number): Promise<CRBrowser> {
const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo)); const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo), logger);
const browser = new CRBrowser(connection, isPersistent); const browser = new CRBrowser(connection, logger, isPersistent);
const session = connection.rootSession; const session = connection.rootSession;
if (!isPersistent) { if (!isPersistent) {
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }); await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
@ -83,8 +84,8 @@ export class CRBrowser extends BrowserBase {
return browser; return browser;
} }
constructor(connection: CRConnection, isPersistent: boolean) { constructor(connection: CRConnection, logger: Logger, isPersistent: boolean) {
super(); super(logger);
this._connection = connection; this._connection = connection;
this._session = this._connection.rootSession; this._session = this._connection.rootSession;
@ -128,8 +129,8 @@ export class CRBrowser extends BrowserBase {
if (targetInfo.type === 'other' || !context) { if (targetInfo.type === 'other' || !context) {
if (waitingForDebugger) { if (waitingForDebugger) {
// Ideally, detaching should resume any target, but there is a bug in the backend. // Ideally, detaching should resume any target, but there is a bug in the backend.
session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => { session.send('Runtime.runIfWaitingForDebugger').catch(logError(this)).then(() => {
this._session.send('Target.detachFromTarget', { sessionId }).catch(debugError); this._session.send('Target.detachFromTarget', { sessionId }).catch(logError(this));
}); });
} }
return; return;
@ -266,7 +267,7 @@ class CRServiceWorker extends Worker {
readonly _browserContext: CRBrowserContext; readonly _browserContext: CRBrowserContext;
constructor(browserContext: CRBrowserContext, session: CRSession, url: string) { constructor(browserContext: CRBrowserContext, session: CRSession, url: string) {
super(url); super(browserContext, url);
this._browserContext = browserContext; this._browserContext = browserContext;
session.once('Runtime.executionContextCreated', event => { session.once('Runtime.executionContextCreated', event => {
this._createExecutionContext(new CRExecutionContext(session, event.context)); this._createExecutionContext(new CRExecutionContext(session, event.context));
@ -283,7 +284,7 @@ export class CRBrowserContext extends BrowserContextBase {
readonly _evaluateOnNewDocumentSources: string[]; readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) { constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super(options); super(browser, options);
this._browser = browser; this._browser = browser;
this._browserContextId = browserContextId; this._browserContextId = browserContextId;
this._evaluateOnNewDocumentSources = []; this._evaluateOnNewDocumentSources = [];

View file

@ -16,9 +16,10 @@
*/ */
import { assert } from '../helper'; import { assert } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Logger } from '../logger';
export const ConnectionEvents = { export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected') Disconnected: Symbol('ConnectionEvents.Disconnected')
@ -34,16 +35,19 @@ 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;
private _logger: Logger;
constructor(transport: ConnectionTransport) { constructor(transport: ConnectionTransport, logger: Logger) {
super(); super();
this._transport = transport; this._transport = transport;
this._logger = logger;
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', '');
this._sessions.set('', this.rootSession); this._sessions.set('', this.rootSession);
} }
static fromSession(session: CRSession): CRConnection { static fromSession(session: CRSession): CRConnection {
return session._connection!; return session._connection!;
} }
@ -57,15 +61,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 (debugProtocol.enabled) if (this._logger._isLogEnabled(protocolLog))
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); this._logger._log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
this._transport.send(message); this._transport.send(message);
return id; return id;
} }
async _onMessage(message: ProtocolResponse) { async _onMessage(message: ProtocolResponse) {
if (debugProtocol.enabled) if (this._logger._isLogEnabled(protocolLog))
debugProtocol('◀ RECV ' + JSON.stringify(message)); this._logger._log(protocolLog, '◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.method === 'Target.attachedToTarget') { if (message.method === 'Target.attachedToTarget') {

View file

@ -16,11 +16,12 @@
*/ */
import { CRSession } from './crConnection'; import { CRSession } from './crConnection';
import { assert, debugError, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener } from '../helper';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { EVALUATION_SCRIPT_URL } from './crExecutionContext'; import { EVALUATION_SCRIPT_URL } from './crExecutionContext';
import * as types from '../types'; import * as types from '../types';
import { logError, Logger } from '../logger';
type JSRange = { type JSRange = {
startOffset: number, startOffset: number,
@ -50,9 +51,9 @@ export class CRCoverage {
private _jsCoverage: JSCoverage; private _jsCoverage: JSCoverage;
private _cssCoverage: CSSCoverage; private _cssCoverage: CSSCoverage;
constructor(client: CRSession) { constructor(client: CRSession, logger: Logger) {
this._jsCoverage = new JSCoverage(client); this._jsCoverage = new JSCoverage(client, logger);
this._cssCoverage = new CSSCoverage(client); this._cssCoverage = new CSSCoverage(client, logger);
} }
async startJSCoverage(options?: types.JSCoverageOptions) { async startJSCoverage(options?: types.JSCoverageOptions) {
@ -80,9 +81,11 @@ class JSCoverage {
_eventListeners: RegisteredListener[]; _eventListeners: RegisteredListener[];
_resetOnNavigation: boolean; _resetOnNavigation: boolean;
_reportAnonymousScripts = false; _reportAnonymousScripts = false;
private _logger: Logger;
constructor(client: CRSession) { constructor(client: CRSession, logger: Logger) {
this._client = client; this._client = client;
this._logger = logger;
this._enabled = false; this._enabled = false;
this._scriptIds = new Set(); this._scriptIds = new Set();
this._scriptSources = new Map(); this._scriptSources = new Map();
@ -134,7 +137,7 @@ class JSCoverage {
this._scriptSources.set(event.scriptId, response.scriptSource); this._scriptSources.set(event.scriptId, response.scriptSource);
} catch (e) { } catch (e) {
// This might happen if the page has already navigated away. // This might happen if the page has already navigated away.
debugError(e); logError(this._logger)(e);
} }
} }
@ -172,9 +175,11 @@ class CSSCoverage {
_stylesheetSources: Map<string, string>; _stylesheetSources: Map<string, string>;
_eventListeners: RegisteredListener[]; _eventListeners: RegisteredListener[];
_resetOnNavigation: boolean; _resetOnNavigation: boolean;
private _logger: Logger;
constructor(client: CRSession) { constructor(client: CRSession, logger: Logger) {
this._client = client; this._client = client;
this._logger = logger;
this._enabled = false; this._enabled = false;
this._stylesheetURLs = new Map(); this._stylesheetURLs = new Map();
this._stylesheetSources = new Map(); this._stylesheetSources = new Map();
@ -218,7 +223,7 @@ class CSSCoverage {
this._stylesheetSources.set(header.styleSheetId, response.text); this._stylesheetSources.set(header.styleSheetId, response.text);
} catch (e) { } catch (e) {
// This might happen if the page has already navigated away. // This might happen if the page has already navigated away.
debugError(e); logError(this._logger)(e);
} }
} }

View file

@ -17,12 +17,13 @@
import { CRSession } from './crConnection'; import { CRSession } from './crConnection';
import { Page } from '../page'; import { Page } from '../page';
import { assert, debugError, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener } from '../helper';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import * as network from '../network'; import * as network from '../network';
import * as frames from '../frames'; import * as frames from '../frames';
import { Credentials } from '../types'; import { Credentials } from '../types';
import { CRPage } from './crPage'; import { CRPage } from './crPage';
import { logError } from '../logger';
export class CRNetworkManager { export class CRNetworkManager {
private _client: CRSession; private _client: CRSession;
@ -130,14 +131,14 @@ export class CRNetworkManager {
this._client.send('Fetch.continueWithAuth', { this._client.send('Fetch.continueWithAuth', {
requestId: event.requestId, requestId: event.requestId,
authChallengeResponse: { response, username, password }, authChallengeResponse: { response, username, password },
}).catch(debugError); }).catch(logError(this._page));
} }
_onRequestPaused(workerFrame: frames.Frame | undefined, event: Protocol.Fetch.requestPausedPayload) { _onRequestPaused(workerFrame: frames.Frame | undefined, event: Protocol.Fetch.requestPausedPayload) {
if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) { if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
this._client.send('Fetch.continueRequest', { this._client.send('Fetch.continueRequest', {
requestId: event.requestId requestId: event.requestId
}).catch(debugError); }).catch(logError(this._page));
} }
if (!event.networkId || event.request.url.startsWith('data:')) if (!event.networkId || event.request.url.startsWith('data:'))
return; return;
@ -176,7 +177,7 @@ export class CRNetworkManager {
if (!frame) { if (!frame) {
if (requestPausedEvent) if (requestPausedEvent)
this._client.send('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }).catch(debugError); this._client.send('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }).catch(logError(this._page));
return; return;
} }
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document'; const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
@ -299,7 +300,7 @@ class InterceptableRequest implements network.RouteDelegate {
}).catch(error => { }).catch(error => {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
debugError(error); logError(this.request._page)(error);
}); });
} }
@ -325,7 +326,7 @@ class InterceptableRequest implements network.RouteDelegate {
}).catch(error => { }).catch(error => {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
debugError(error); logError(this.request._page)(error);
}); });
} }
@ -338,7 +339,7 @@ class InterceptableRequest implements network.RouteDelegate {
}).catch(error => { }).catch(error => {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
debugError(error); logError(this.request._page)(error);
}); });
} }
} }

View file

@ -18,7 +18,7 @@
import * as dom from '../dom'; import * as dom from '../dom';
import * as js from '../javascript'; import * as js from '../javascript';
import * as frames from '../frames'; import * as frames from '../frames';
import { debugError, helper, RegisteredListener, assert } from '../helper'; import { helper, RegisteredListener, assert } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { CRSession, CRConnection, CRSessionEvents } from './crConnection'; import { CRSession, CRConnection, CRSessionEvents } from './crConnection';
import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext'; import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext';
@ -37,6 +37,7 @@ import { CRBrowserContext } from './crBrowser';
import * as types from '../types'; import * as types from '../types';
import { ConsoleMessage } from '../console'; import { ConsoleMessage } from '../console';
import { NotConnectedError } from '../errors'; import { NotConnectedError } from '../errors';
import { logError } from '../logger';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -60,7 +61,7 @@ export class CRPage implements PageDelegate {
this.rawKeyboard = new RawKeyboardImpl(client); this.rawKeyboard = new RawKeyboardImpl(client);
this.rawMouse = new RawMouseImpl(client); this.rawMouse = new RawMouseImpl(client);
this._pdf = new CRPDF(client); this._pdf = new CRPDF(client);
this._coverage = new CRCoverage(client); this._coverage = new CRCoverage(client, browserContext);
this._browserContext = browserContext; this._browserContext = browserContext;
this._page = new Page(this, browserContext); this._page = new Page(this, browserContext);
this._mainFrameSession = new FrameSession(this, client, targetId); this._mainFrameSession = new FrameSession(this, client, targetId);
@ -127,7 +128,7 @@ export class CRPage implements PageDelegate {
async exposeBinding(binding: PageBinding) { async exposeBinding(binding: PageBinding) {
await this._forAllFrameSessions(frame => frame._initBinding(binding)); await this._forAllFrameSessions(frame => frame._initBinding(binding));
await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(debugError))); await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(logError(this._page))));
} }
async updateExtraHTTPHeaders(): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
@ -381,9 +382,9 @@ class FrameSession {
frameId: frame._id, frameId: frame._id,
grantUniveralAccess: true, grantUniveralAccess: true,
worldName: UTILITY_WORLD_NAME, worldName: UTILITY_WORLD_NAME,
}).catch(debugError); }).catch(logError(this._page));
for (const binding of this._crPage._browserContext._pageBindings.values()) for (const binding of this._crPage._browserContext._pageBindings.values())
frame.evaluate(binding.source).catch(debugError); frame.evaluate(binding.source).catch(logError(this._page));
} }
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':'; const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) { if (isInitialEmptyPage) {
@ -543,14 +544,14 @@ class FrameSession {
if (event.targetInfo.type !== 'worker') { if (event.targetInfo.type !== 'worker') {
// Ideally, detaching should resume any target, but there is a bug in the backend. // Ideally, detaching should resume any target, but there is a bug in the backend.
session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => { session.send('Runtime.runIfWaitingForDebugger').catch(logError(this._page)).then(() => {
this._client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(debugError); this._client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(logError(this._page));
}); });
return; return;
} }
const url = event.targetInfo.url; const url = event.targetInfo.url;
const worker = new Worker(url); const worker = new Worker(this._page, url);
this._page._addWorker(event.sessionId, worker); this._page._addWorker(event.sessionId, worker);
session.once('Runtime.executionContextCreated', async event => { session.once('Runtime.executionContextCreated', async event => {
worker._createExecutionContext(new CRExecutionContext(session, event.context)); worker._createExecutionContext(new CRExecutionContext(session, event.context));
@ -559,7 +560,7 @@ class FrameSession {
session.send('Runtime.enable'), session.send('Runtime.enable'),
session.send('Network.enable'), session.send('Network.enable'),
session.send('Runtime.runIfWaitingForDebugger'), session.send('Runtime.runIfWaitingForDebugger'),
]).catch(debugError); // This might fail if the target is closed before we initialize. ]).catch(logError(this._page)); // This might fail if the target is closed before we initialize.
session.on('Runtime.consoleAPICalled', event => { session.on('Runtime.consoleAPICalled', event => {
const args = event.args.map(o => worker._existingExecutionContext!._createHandle(o)); const args = event.args.map(o => worker._existingExecutionContext!._createHandle(o));
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
@ -750,7 +751,7 @@ class FrameSession {
async _getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> { async _getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const result = await this._client.send('DOM.getBoxModel', { const result = await this._client.send('DOM.getBoxModel', {
objectId: toRemoteObject(handle).objectId objectId: toRemoteObject(handle).objectId
}).catch(debugError); }).catch(logError(this._page));
if (!result) if (!result)
return null; return null;
const quad = result.model.border; const quad = result.model.border;
@ -777,7 +778,7 @@ class FrameSession {
async _getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> { async _getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._client.send('DOM.getContentQuads', { const result = await this._client.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId objectId: toRemoteObject(handle).objectId
}).catch(debugError); }).catch(logError(this._page));
if (!result) if (!result)
return null; return null;
return result.quads.map(quad => [ return result.quads.map(quad => [
@ -799,7 +800,7 @@ class FrameSession {
const result = await this._client.send('DOM.resolveNode', { const result = await this._client.send('DOM.resolveNode', {
backendNodeId, backendNodeId,
executionContextId: (to._delegate as CRExecutionContext)._contextId, executionContextId: (to._delegate as CRExecutionContext)._contextId,
}).catch(debugError); }).catch(logError(this._page));
if (!result || result.object.subtype === 'null') if (!result || result.object.subtype === 'null')
throw new Error('Unable to adopt element handle from a different document'); throw new Error('Unable to adopt element handle from a different document');
return to._createHandle(result.object).asElement()!; return to._createHandle(result.object).asElement()!;

View file

@ -19,7 +19,7 @@ import * as mime from 'mime';
import * as path from 'path'; import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import * as frames from './frames'; import * as frames from './frames';
import { assert, debugError, helper, debugInput } from './helper'; import { assert, helper } from './helper';
import { Injected, InjectedResult } from './injected/injected'; import { Injected, InjectedResult } from './injected/injected';
import * as input from './input'; import * as input from './input';
import * as js from './javascript'; import * as js from './javascript';
@ -27,6 +27,7 @@ import { Page } from './page';
import { selectors } from './selectors'; import { selectors } from './selectors';
import * as types from './types'; import * as types from './types';
import { NotConnectedError, TimeoutError } from './errors'; import { NotConnectedError, TimeoutError } from './errors';
import { Log, logError } from './logger';
export type PointerActionOptions = { export type PointerActionOptions = {
modifiers?: input.Modifier[]; modifiers?: input.Modifier[];
@ -37,12 +38,17 @@ export type ClickOptions = PointerActionOptions & input.MouseClickOptions;
export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions; export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions;
export const inputLog: Log = {
name: 'input',
color: 'cyan'
};
export class FrameExecutionContext extends js.ExecutionContext { export class FrameExecutionContext extends js.ExecutionContext {
readonly frame: frames.Frame; readonly frame: frames.Frame;
private _injectedPromise?: Promise<js.JSHandle>; private _injectedPromise?: Promise<js.JSHandle>;
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame) { constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame) {
super(delegate); super(delegate, frame._page);
this.frame = frame; this.frame = frame;
} }
@ -144,9 +150,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<void> { async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<void> {
debugInput('scrolling into veiw if needed...'); this._page._log(inputLog, 'scrolling into view if needed...');
await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect); await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect);
debugInput('...done'); this._page._log(inputLog, '...done');
} }
async scrollIntoViewIfNeeded() { async scrollIntoViewIfNeeded() {
@ -195,7 +201,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
private async _offsetPoint(offset: types.Point): Promise<types.Point> { private async _offsetPoint(offset: types.Point): Promise<types.Point> {
const [box, border] = await Promise.all([ const [box, border] = await Promise.all([
this.boundingBox(), this.boundingBox(),
this._evaluateInUtility(({ injected, node }) => injected.getElementBorderWidth(node), {}).catch(debugError), this._evaluateInUtility(({ injected, node }) => injected.getElementBorderWidth(node), {}).catch(logError(this._context._logger)),
]); ]);
const point = { x: offset.x, y: offset.y }; const point = { x: offset.x, y: offset.y };
if (box) { if (box) {
@ -227,9 +233,9 @@ 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);
debugInput('performing input action...'); this._page._log(inputLog, 'performing input action...');
await action(point); await action(point);
debugInput('...done'); this._page._log(inputLog, '...done');
if (restoreModifiers) if (restoreModifiers)
await this._page.keyboard._ensureModifiers(restoreModifiers); await this._page.keyboard._ensureModifiers(restoreModifiers);
}, deadline, options, true); }, deadline, options, true);
@ -404,18 +410,18 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
} }
async _waitForDisplayedAtStablePosition(deadline: number): Promise<void> { async _waitForDisplayedAtStablePosition(deadline: number): Promise<void> {
debugInput('waiting for element to be displayed and not moving...'); this._page._log(inputLog, 'waiting for element to be displayed and not moving...');
const stablePromise = this._evaluateInUtility(({ injected, node }, timeout) => { const stablePromise = this._evaluateInUtility(({ injected, node }, timeout) => {
return injected.waitForDisplayedAtStablePosition(node, timeout); return injected.waitForDisplayedAtStablePosition(node, timeout);
}, helper.timeUntilDeadline(deadline)); }, helper.timeUntilDeadline(deadline));
const timeoutMessage = 'element to be displayed and not moving'; const timeoutMessage = 'element to be displayed and not moving';
const injectedResult = await helper.waitWithDeadline(stablePromise, timeoutMessage, deadline); const injectedResult = await helper.waitWithDeadline(stablePromise, timeoutMessage, deadline);
handleInjectedResult(injectedResult, timeoutMessage); handleInjectedResult(injectedResult, timeoutMessage);
debugInput('...done'); this._page._log(inputLog, '...done');
} }
async _waitForHitTargetAt(point: types.Point, deadline: number): Promise<void> { async _waitForHitTargetAt(point: types.Point, deadline: number): Promise<void> {
debugInput(`waiting for element to receive pointer events at (${point.x},${point.y}) ...`); this._page._log(inputLog, `waiting for element to receive pointer events at (${point.x},${point.y}) ...`);
const frame = await this.ownerFrame(); const frame = await this.ownerFrame();
if (frame && frame.parentFrame()) { if (frame && frame.parentFrame()) {
const element = await frame.frameElement(); const element = await frame.frameElement();
@ -431,7 +437,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
const timeoutMessage = 'element to receive pointer events'; const timeoutMessage = 'element to receive pointer events';
const injectedResult = await helper.waitWithDeadline(hitTargetPromise, timeoutMessage, deadline); const injectedResult = await helper.waitWithDeadline(hitTargetPromise, timeoutMessage, deadline);
handleInjectedResult(injectedResult, timeoutMessage); handleInjectedResult(injectedResult, timeoutMessage);
debugInput('...done'); this._page._log(inputLog, '...done');
} }
} }

View file

@ -27,6 +27,7 @@ import { ConnectionEvents, FFConnection } from './ffConnection';
import { headersArray } from './ffNetworkManager'; import { headersArray } from './ffNetworkManager';
import { FFPage } from './ffPage'; import { FFPage } from './ffPage';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { Logger } from '../logger';
export class FFBrowser extends BrowserBase { export class FFBrowser extends BrowserBase {
_connection: FFConnection; _connection: FFConnection;
@ -37,15 +38,15 @@ export class FFBrowser extends BrowserBase {
readonly _firstPagePromise: Promise<void>; readonly _firstPagePromise: Promise<void>;
private _firstPageCallback = () => {}; private _firstPageCallback = () => {};
static async connect(transport: ConnectionTransport, attachToDefaultContext: boolean, slowMo?: number): Promise<FFBrowser> { static async connect(transport: ConnectionTransport, logger: Logger, attachToDefaultContext: boolean, slowMo?: number): Promise<FFBrowser> {
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo)); const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo), logger);
const browser = new FFBrowser(connection, attachToDefaultContext); const browser = new FFBrowser(connection, logger, attachToDefaultContext);
await connection.send('Browser.enable', { attachToDefaultContext }); await connection.send('Browser.enable', { attachToDefaultContext });
return browser; return browser;
} }
constructor(connection: FFConnection, isPersistent: boolean) { constructor(connection: FFConnection, logger: Logger, isPersistent: boolean) {
super(); super(logger);
this._connection = connection; this._connection = connection;
this._ffPages = new Map(); this._ffPages = new Map();
@ -172,7 +173,7 @@ export class FFBrowserContext extends BrowserContextBase {
private readonly _evaluateOnNewDocumentSources: string[]; private readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) { constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super(options); super(browser, options);
this._browser = browser; this._browser = browser;
this._browserContextId = browserContextId; this._browserContextId = browserContextId;
this._evaluateOnNewDocumentSources = []; this._evaluateOnNewDocumentSources = [];

View file

@ -17,8 +17,9 @@
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { assert } from '../helper'; import { assert } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { Logger } from '../logger';
export const ConnectionEvents = { export const ConnectionEvents = {
Disconnected: Symbol('Disconnected'), Disconnected: Symbol('Disconnected'),
@ -32,6 +33,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;
private _logger: Logger;
readonly _sessions: Map<string, FFSession>; readonly _sessions: Map<string, FFSession>;
_closed: boolean; _closed: boolean;
@ -41,9 +43,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) { constructor(transport: ConnectionTransport, logger: Logger) {
super(); super();
this._transport = transport; this._transport = transport;
this._logger = logger;
this._lastId = 0; this._lastId = 0;
this._callbacks = new Map(); this._callbacks = new Map();
@ -75,14 +78,14 @@ export class FFConnection extends EventEmitter {
} }
_rawSend(message: ProtocolRequest) { _rawSend(message: ProtocolRequest) {
if (debugProtocol.enabled) if (this._logger._isLogEnabled(protocolLog))
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); this._logger._log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
this._transport.send(message); this._transport.send(message);
} }
async _onMessage(message: ProtocolResponse) { async _onMessage(message: ProtocolResponse) {
if (debugProtocol.enabled) if (this._logger._isLogEnabled(protocolLog))
debugProtocol('◀ RECV ' + JSON.stringify(message)); this._logger._log(protocolLog, '◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.sessionId) { if (message.sessionId) {

View file

@ -15,12 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { debugError, helper, RegisteredListener } from '../helper'; import { helper, RegisteredListener } from '../helper';
import { FFSession } from './ffConnection'; import { FFSession } from './ffConnection';
import { Page } from '../page'; import { Page } from '../page';
import * as network from '../network'; import * as network from '../network';
import * as frames from '../frames'; import * as frames from '../frames';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { logError } from '../logger';
export class FFNetworkManager { export class FFNetworkManager {
private _session: FFSession; private _session: FFSession;
@ -164,9 +165,7 @@ class InterceptableRequest implements network.RouteDelegate {
method, method,
headers: headers ? headersArray(headers) : undefined, headers: headers ? headersArray(headers) : undefined,
postData: postData ? Buffer.from(postData).toString('base64') : undefined postData: postData ? Buffer.from(postData).toString('base64') : undefined
}).catch(error => { }).catch(logError(this.request._page));
debugError(error);
});
} }
async fulfill(response: network.FulfillResponse) { async fulfill(response: network.FulfillResponse) {
@ -188,18 +187,14 @@ class InterceptableRequest implements network.RouteDelegate {
statusText: network.STATUS_TEXTS[String(response.status || 200)] || '', statusText: network.STATUS_TEXTS[String(response.status || 200)] || '',
headers: headersArray(responseHeaders), headers: headersArray(responseHeaders),
base64body: responseBody ? responseBody.toString('base64') : undefined, base64body: responseBody ? responseBody.toString('base64') : undefined,
}).catch(error => { }).catch(logError(this.request._page));
debugError(error);
});
} }
async abort(errorCode: string) { async abort(errorCode: string) {
await this._session.send('Network.abortInterceptedRequest', { await this._session.send('Network.abortInterceptedRequest', {
requestId: this._id, requestId: this._id,
errorCode, errorCode,
}).catch(error => { }).catch(logError(this.request._page));
debugError(error);
});
} }
} }

View file

@ -19,7 +19,7 @@ import * as dialog from '../dialog';
import * as dom from '../dom'; import * as dom from '../dom';
import { Events } from '../events'; import { Events } from '../events';
import * as frames from '../frames'; import * as frames from '../frames';
import { assert, debugError, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener } from '../helper';
import { Page, PageBinding, PageDelegate, Worker } from '../page'; import { Page, PageBinding, PageDelegate, Worker } from '../page';
import { kScreenshotDuringNavigationError } from '../screenshotter'; import { kScreenshotDuringNavigationError } from '../screenshotter';
import * as types from '../types'; import * as types from '../types';
@ -32,6 +32,7 @@ import { FFNetworkManager, headersArray } from './ffNetworkManager';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { selectors } from '../selectors'; import { selectors } from '../selectors';
import { NotConnectedError } from '../errors'; import { NotConnectedError } from '../errors';
import { logError } from '../logger';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -199,7 +200,7 @@ export class FFPage implements PageDelegate {
params.type, params.type,
params.message, params.message,
async (accept: boolean, promptText?: string) => { async (accept: boolean, promptText?: string) => {
await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(debugError); await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(logError(this._page));
}, },
params.defaultValue)); params.defaultValue));
} }
@ -218,7 +219,7 @@ export class FFPage implements PageDelegate {
async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) { async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) {
const workerId = event.workerId; const workerId = event.workerId;
const worker = new Worker(event.url); const worker = new Worker(this._page, event.url);
const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => { const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => {
this._session.send('Page.sendMessageToWorker', { this._session.send('Page.sendMessageToWorker', {
frameId: event.frameId, frameId: event.frameId,
@ -434,7 +435,7 @@ export class FFPage implements PageDelegate {
const result = await this._session.send('Page.getContentQuads', { const result = await this._session.send('Page.getContentQuads', {
frameId: handle._context.frame._id, frameId: handle._context.frame._id,
objectId: toRemoteObject(handle).objectId!, objectId: toRemoteObject(handle).objectId!,
}).catch(debugError); }).catch(logError(this._page));
if (!result) if (!result)
return null; return null;
return result.quads.map(quad => [ quad.p1, quad.p2, quad.p3, quad.p4 ]); return result.quads.map(quad => [ quad.p1, quad.p2, quad.p3, quad.p4 ]);

View file

@ -21,7 +21,7 @@ import { ConsoleMessage } from './console';
import * as dom from './dom'; import * as dom from './dom';
import { TimeoutError, NotConnectedError } from './errors'; import { TimeoutError, NotConnectedError } from './errors';
import { Events } from './events'; import { Events } from './events';
import { assert, helper, RegisteredListener, debugInput } from './helper'; import { assert, helper, RegisteredListener } from './helper';
import * as js from './javascript'; import * as js from './javascript';
import * as network from './network'; import * as network from './network';
import { Page } from './page'; import { Page } from './page';
@ -710,7 +710,7 @@ export class Frame {
} catch (e) { } catch (e) {
if (!(e instanceof NotConnectedError)) if (!(e instanceof NotConnectedError))
throw e; throw e;
debugInput('Element was detached from the DOM, retrying'); this._page._log(dom.inputLog, 'Element was detached from the DOM, retrying');
} }
} }
throw new TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded`); throw new TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded`);
@ -775,7 +775,7 @@ export class Frame {
if (helper.isString(selectorOrFunctionOrTimeout)) if (helper.isString(selectorOrFunctionOrTimeout))
return this.waitForSelector(selectorOrFunctionOrTimeout, options) as any; return this.waitForSelector(selectorOrFunctionOrTimeout, options) as any;
if (helper.isNumber(selectorOrFunctionOrTimeout)) { if (helper.isNumber(selectorOrFunctionOrTimeout)) {
waitForTimeWasUsed(); waitForTimeWasUsed(this._page);
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout)); return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
} }
if (typeof selectorOrFunctionOrTimeout === 'function') if (typeof selectorOrFunctionOrTimeout === 'function')

View file

@ -16,16 +16,12 @@
*/ */
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as debug from 'debug';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import * as fs from 'fs'; import * as fs from 'fs';
import * as util from 'util'; import * as util from 'util';
import { TimeoutError } from './errors'; import { TimeoutError } from './errors';
import * as types from './types'; import * as types from './types';
export const debugError = debug(`pw:error`);
export const debugInput = debug('pw:input');
export type RegisteredListener = { export type RegisteredListener = {
emitter: EventEmitter; emitter: EventEmitter;
eventName: (string | symbol); eventName: (string | symbol);
@ -66,37 +62,16 @@ class Helper {
} }
static installApiHooks(className: string, classType: any) { static installApiHooks(className: string, classType: any) {
const log = debug('pw:api');
for (const methodName of Reflect.ownKeys(classType.prototype)) { for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName); const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function') if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
continue; continue;
const isAsync = method.constructor.name === 'AsyncFunction'; const isAsync = method.constructor.name === 'AsyncFunction';
if (!isAsync && !log.enabled) if (!isAsync)
continue; continue;
Reflect.set(classType.prototype, methodName, function(this: any, ...args: any[]) { Reflect.set(classType.prototype, methodName, function(this: any, ...args: any[]) {
const syncStack: any = {}; const syncStack: any = {};
Error.captureStackTrace(syncStack); Error.captureStackTrace(syncStack);
if (log.enabled) {
const frames = syncStack.stack.substring('Error\n'.length)
.split('\n')
.map((f: string) => f.replace(/\s+at\s/, '').trim());
const userCall = frames.length <= 1 || !frames[1].includes('playwright/lib');
if (userCall) {
const match = /([^/\\]+)(:\d+:\d+)[)]?$/.exec(frames[1]);
let location = '';
if (match) {
const fileName = helper.trimMiddle(match[1], 20 - match[2].length);
location = `\u001b[33m[${fileName}${match[2]}]\u001b[39m `;
}
if (args.length)
log(`${location}${className}.${methodName} %o`, args);
else
log(`${location}${className}.${methodName}`);
}
}
if (!isAsync)
return method.call(this, ...args);
return method.call(this, ...args).catch((e: any) => { return method.call(this, ...args).catch((e: any) => {
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1); const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
const clientStack = stack.substring(stack.indexOf('\n')); const clientStack = stack.substring(stack.indexOf('\n'));

View file

@ -14,17 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
import * as debug from 'debug'; import { Page } from './page';
import { Log } from './logger';
export const debugHints = debug('pw:hints'); const hintsLog: Log = {
(debugHints as any).color = '11'; name: 'hint',
severity: 'warning'
};
let waitForTimeWasUsedReported = false; let waitForTimeWasUsedReported = false;
export function waitForTimeWasUsed() { export function waitForTimeWasUsed(page: Page) {
if (waitForTimeWasUsedReported) if (waitForTimeWasUsedReported)
return; return;
waitForTimeWasUsedReported = true; waitForTimeWasUsedReported = true;
debugHints(`WARNING: page.waitFor(timeout) should only be used for debugging. page._log(hintsLog, `WARNING: page.waitFor(timeout) should only be used for debugging.
It is likely that the tests using timer in production are going to be flaky. It is likely that the tests using timer in production are going to be flaky.
Use signals such as network events, selectors becoming visible, etc. instead.`); Use signals such as network events, selectors becoming visible, etc. instead.`);
} }

View file

@ -17,6 +17,7 @@
import * as types from './types'; import * as types from './types';
import * as dom from './dom'; import * as dom from './dom';
import { helper } from './helper'; import { helper } from './helper';
import { Logger } from './logger';
export interface ExecutionContextDelegate { export interface ExecutionContextDelegate {
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>; evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
@ -28,9 +29,11 @@ export interface ExecutionContextDelegate {
export class ExecutionContext { export class ExecutionContext {
readonly _delegate: ExecutionContextDelegate; readonly _delegate: ExecutionContextDelegate;
readonly _logger: Logger;
constructor(delegate: ExecutionContextDelegate) { constructor(delegate: ExecutionContextDelegate, logger: Logger) {
this._delegate = delegate; this._delegate = delegate;
this._logger = logger;
} }
_doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise<any> { _doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise<any> {

109
src/logger.ts Normal file
View file

@ -0,0 +1,109 @@
/**
* 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 util from 'util';
export type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
export type Log = {
name: string;
severity?: LoggerSeverity;
color?: string | undefined;
};
export interface Logger {
_isLogEnabled(log: Log): boolean;
_log(log: Log, message: string | Error, ...args: any[]): void;
}
export interface LoggerSink {
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 function logError(logger: Logger): (error: Error) => void {
return error => logger._log(errorLog, error, []);
}
const colorMap = new Map<string, number>([
['black', 30],
['red', 31],
['green', 32],
['yellow', 33],
['blue', 34],
['magenta', 35],
['cyan', 36],
['white', 37],
['reset', 0],
]);
export class RootLogger implements Logger {
private _userSink: LoggerSink | undefined;
private _consoleSink: ConsoleLoggerSink;
constructor(userSink: LoggerSink | undefined) {
this._userSink = userSink;
this._consoleSink = new ConsoleLoggerSink();
}
_isLogEnabled(log: Log): boolean {
return (this._userSink && this._userSink.isEnabled(log.name, log.severity || 'info')) ||
this._consoleSink.isEnabled(log.name, log.severity || 'info');
}
_log(log: Log, message: string | Error, ...args: any[]) {
if (this._userSink && this._userSink.isEnabled(log.name, log.severity || 'info'))
this._userSink.log(log.name, log.severity || 'info', message, args, log.color ? { color: log.color } : {});
if (this._consoleSink.isEnabled(log.name, log.severity || 'info'))
this._consoleSink.log(log.name, log.severity || 'info', message, args, log.color ? { color: log.color } : {});
}
}
class ConsoleLoggerSink implements LoggerSink {
private _enabled: string[];
private _enabledCache = new Map<string, boolean>();
constructor() {
this._enabled = process.env.PWDEBUG ? process.env.PWDEBUG.split(',') : [];
}
isEnabled(name: string, severity: LoggerSeverity): boolean {
const result = this._enabledCache.get(name);
if (typeof result === 'boolean')
return result;
for (const logger of this._enabled) {
if (name.includes(logger)) {
this._enabledCache.set(name, true);
return true;
}
}
this._enabledCache.set(name, false);
return false;
}
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }) {
let color = hints.color || 'reset';
switch (severity) {
case 'error': color = 'red'; break;
case 'warning': color = 'yellow'; break;
}
const escape = colorMap.get(color) || 0;
console.log(`[${new Date().toISOString()}:\u001b[${escape}m${name}\u001b[0m] ${util.format(message, ...args)}`); // eslint-disable-line no-console
}
}

View file

@ -19,6 +19,7 @@ import * as mime from 'mime';
import * as util from 'util'; import * as util from 'util';
import * as frames from './frames'; import * as frames from './frames';
import { assert, helper } from './helper'; import { assert, helper } from './helper';
import { Page } from './page';
export type NetworkCookie = { export type NetworkCookie = {
name: string, name: string,
@ -110,12 +111,14 @@ export class Request {
private _frame: frames.Frame; private _frame: frames.Frame;
private _waitForResponsePromise: Promise<Response | null>; private _waitForResponsePromise: Promise<Response | null>;
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {}; private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
readonly _page: Page;
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined, constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
url: string, resourceType: string, method: string, postData: string | null, headers: Headers) { url: string, resourceType: string, method: string, postData: string | null, headers: Headers) {
assert(!url.startsWith('data:'), 'Data urls should not fire requests'); assert(!url.startsWith('data:'), 'Data urls should not fire requests');
this._routeDelegate = routeDelegate; this._routeDelegate = routeDelegate;
this._frame = frame; this._frame = frame;
this._page = frame._page;
this._redirectedFrom = redirectedFrom; this._redirectedFrom = redirectedFrom;
if (redirectedFrom) if (redirectedFrom)
redirectedFrom._redirectedTo = this; redirectedFrom._redirectedTo = this;

View file

@ -17,7 +17,7 @@
import * as dom from './dom'; import * as dom from './dom';
import * as frames from './frames'; import * as frames from './frames';
import { assert, debugError, helper, Listener } from './helper'; import { assert, helper, Listener } from './helper';
import * as input from './input'; import * as input from './input';
import * as js from './javascript'; import * as js from './javascript';
import * as network from './network'; import * as network from './network';
@ -31,6 +31,7 @@ import * as accessibility from './accessibility';
import { ExtendedEventEmitter } from './extendedEventEmitter'; import { ExtendedEventEmitter } from './extendedEventEmitter';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
import { logError, Logger, Log } from './logger';
export interface PageDelegate { export interface PageDelegate {
readonly rawMouse: input.RawMouse; readonly rawMouse: input.RawMouse;
@ -86,7 +87,7 @@ type PageState = {
extraHTTPHeaders: network.Headers | null; extraHTTPHeaders: network.Headers | null;
}; };
export class Page extends ExtendedEventEmitter { export class Page extends ExtendedEventEmitter implements Logger {
private _closed = false; private _closed = false;
private _closedCallback: () => void; private _closedCallback: () => void;
private _closedPromise: Promise<void>; private _closedPromise: Promise<void>;
@ -525,23 +526,33 @@ export class Page extends ExtendedEventEmitter {
this._delegate.setFileChooserIntercepted(false); this._delegate.setFileChooserIntercepted(false);
return this; return this;
} }
_isLogEnabled(log: Log): boolean {
return this._browserContext._isLogEnabled(log);
}
_log(log: Log, message: string | Error, ...args: any[]) {
return this._browserContext._log(log, message, ...args);
}
} }
export class Worker extends EventEmitter { export class Worker extends EventEmitter {
private _logger: Logger;
private _url: string; private _url: string;
private _executionContextPromise: Promise<js.ExecutionContext>; private _executionContextPromise: Promise<js.ExecutionContext>;
private _executionContextCallback: (value?: js.ExecutionContext) => void; private _executionContextCallback: (value?: js.ExecutionContext) => void;
_existingExecutionContext: js.ExecutionContext | null = null; _existingExecutionContext: js.ExecutionContext | null = null;
constructor(url: string) { constructor(logger: Logger, url: string) {
super(); super();
this._logger = logger;
this._url = url; this._url = url;
this._executionContextCallback = () => {}; this._executionContextCallback = () => {};
this._executionContextPromise = new Promise(x => this._executionContextCallback = x); this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
} }
_createExecutionContext(delegate: js.ExecutionContextDelegate) { _createExecutionContext(delegate: js.ExecutionContextDelegate) {
this._existingExecutionContext = new js.ExecutionContext(delegate); this._existingExecutionContext = new js.ExecutionContext(delegate, this._logger);
this._executionContextCallback(this._existingExecutionContext); this._executionContextCallback(this._existingExecutionContext);
} }
@ -588,7 +599,7 @@ export class PageBinding {
else else
expression = helper.evaluationString(deliverErrorValue, name, seq, error); expression = helper.evaluationString(deliverErrorValue, name, seq, error);
} }
context.evaluateInternal(expression).catch(debugError); context.evaluateInternal(expression).catch(logError(page));
function deliverResult(name: string, seq: number, result: any) { function deliverResult(name: string, seq: number, result: any) {
(window as any)[name]['callbacks'].get(seq).resolve(result); (window as any)[name]['callbacks'].get(seq).resolve(result);

View file

@ -16,6 +16,7 @@
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { BrowserServer } from './browserServer'; import { BrowserServer } from './browserServer';
import { LoggerSink } from '../logger';
export type BrowserArgOptions = { export type BrowserArgOptions = {
headless?: boolean, headless?: boolean,
@ -30,17 +31,14 @@ type LaunchOptionsBase = BrowserArgOptions & {
handleSIGTERM?: boolean, handleSIGTERM?: boolean,
handleSIGHUP?: boolean, handleSIGHUP?: boolean,
timeout?: number, timeout?: number,
/** loggerSink?: LoggerSink,
* Whether to dump stdio of the browser, this is useful for example when
* diagnosing browser launch issues.
*/
dumpio?: boolean,
env?: {[key: string]: string} | undefined env?: {[key: string]: string} | undefined
}; };
export type ConnectOptions = { export type ConnectOptions = {
wsEndpoint: string, wsEndpoint: string,
slowMo?: number slowMo?: number,
loggerSink?: LoggerSink,
}; };
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number }; export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
export type LaunchServerOptions = LaunchOptionsBase & { port?: number }; export type LaunchServerOptions = LaunchOptionsBase & { port?: number };

View file

@ -19,7 +19,7 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import { debugError, helper, assert } from '../helper'; import { helper, assert } from '../helper';
import { CRBrowser } from '../chromium/crBrowser'; import { CRBrowser } from '../chromium/crBrowser';
import * as ws from 'ws'; import * as ws from 'ws';
import { launchProcess } from './processLauncher'; import { launchProcess } from './processLauncher';
@ -31,6 +31,7 @@ import { BrowserServer, WebSocketWrapper } from './browserServer';
import { Events } from '../events'; import { Events } from '../events';
import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport'; import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { Logger, logError, RootLogger } from '../logger';
export class Chromium implements BrowserType<CRBrowser> { export class Chromium implements BrowserType<CRBrowser> {
private _executablePath: (string|undefined); private _executablePath: (string|undefined);
@ -47,8 +48,8 @@ export class Chromium implements BrowserType<CRBrowser> {
async launch(options: LaunchOptions = {}): Promise<CRBrowser> { async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
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');
const { browserServer, transport, downloadsPath } = await this._launchServer(options, 'local'); const { browserServer, transport, downloadsPath, logger } = await this._launchServer(options, 'local');
const browser = await CRBrowser.connect(transport!, false, options.slowMo); const browser = await CRBrowser.connect(transport!, false, logger, options.slowMo);
browser._ownedServer = browserServer; browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath; browser._downloadsPath = downloadsPath;
return browser; return browser;
@ -61,20 +62,19 @@ export class Chromium implements BrowserType<CRBrowser> {
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> { async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const { const {
timeout = 30000, timeout = 30000,
slowMo = 0 slowMo = 0,
} = options; } = options;
const { transport, browserServer } = await this._launchServer(options, 'persistent', userDataDir); const { transport, browserServer, logger } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await CRBrowser.connect(transport!, true, slowMo); const browser = await CRBrowser.connect(transport!, true, logger, slowMo);
browser._ownedServer = browserServer; browser._ownedServer = browserServer;
await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout); await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout);
return browser._defaultContext!; return browser._defaultContext!;
} }
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string }> { private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string, logger: Logger }> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
dumpio = false,
executablePath = null, executablePath = null,
env = process.env, env = process.env,
handleSIGINT = true, handleSIGINT = true,
@ -83,6 +83,7 @@ export class Chromium implements BrowserType<CRBrowser> {
port = 0, port = 0,
} = options; } = options;
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
const logger = new RootLogger(options.loggerSink);
let temporaryUserDataDir: string | null = null; let temporaryUserDataDir: string | null = null;
if (!userDataDir) { if (!userDataDir) {
@ -108,7 +109,7 @@ export class Chromium implements BrowserType<CRBrowser> {
handleSIGINT, handleSIGINT,
handleSIGTERM, handleSIGTERM,
handleSIGHUP, handleSIGHUP,
dumpio, logger,
pipe: true, pipe: true,
tempDir: temporaryUserDataDir || undefined, tempDir: temporaryUserDataDir || undefined,
attemptToGracefullyClose: async () => { attemptToGracefullyClose: async () => {
@ -129,14 +130,14 @@ export class Chromium implements BrowserType<CRBrowser> {
let transport: PipeTransport | undefined = undefined; let transport: PipeTransport | undefined = undefined;
let browserServer: BrowserServer | undefined = undefined; let browserServer: BrowserServer | undefined = undefined;
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]); transport = new PipeTransport(stdio[3], stdio[4], logger);
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port) : null); browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port) : null);
return { browserServer, transport, downloadsPath }; return { browserServer, transport, downloadsPath, logger };
} }
async connect(options: ConnectOptions): Promise<CRBrowser> { async connect(options: ConnectOptions): Promise<CRBrowser> {
return await WebSocketTransport.connect(options.wsEndpoint, transport => { return await WebSocketTransport.connect(options.wsEndpoint, transport => {
return CRBrowser.connect(transport, false, options.slowMo); return CRBrowser.connect(transport, false, new RootLogger(options.loggerSink), options.slowMo);
}); });
} }
@ -178,7 +179,7 @@ export class Chromium implements BrowserType<CRBrowser> {
} }
} }
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper { function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: Logger, port: number): WebSocketWrapper {
const server = new ws.Server({ port }); const server = new ws.Server({ port });
const guid = helper.guid(); const guid = helper.guid();
@ -275,7 +276,7 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number
session.queue!.push(parsedMessage); session.queue!.push(parsedMessage);
}); });
socket.on('error', error => debugError(error)); socket.on('error', logError(logger));
socket.on('close', (socket as any).__closeListener = () => { socket.on('close', (socket as any).__closeListener = () => {
const session = socketToBrowserSession.get(socket); const session = socketToBrowserSession.get(socket);

View file

@ -26,11 +26,12 @@ import { TimeoutError } from '../errors';
import { Events } from '../events'; import { Events } from '../events';
import { FFBrowser } from '../firefox/ffBrowser'; import { FFBrowser } from '../firefox/ffBrowser';
import { kBrowserCloseMessageId } from '../firefox/ffConnection'; import { kBrowserCloseMessageId } from '../firefox/ffConnection';
import { debugError, helper, assert } from '../helper'; import { helper, assert } from '../helper';
import { BrowserServer, WebSocketWrapper } from './browserServer'; import { BrowserServer, WebSocketWrapper } from './browserServer';
import { BrowserArgOptions, BrowserType, LaunchOptions, LaunchServerOptions, ConnectOptions } from './browserType'; import { BrowserArgOptions, BrowserType, LaunchOptions, LaunchServerOptions, ConnectOptions } from './browserType';
import { launchProcess, waitForLine } from './processLauncher'; import { launchProcess, waitForLine } from './processLauncher';
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport'; import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
import { RootLogger, Logger, logError } from '../logger';
const mkdtempAsync = util.promisify(fs.mkdtemp); const mkdtempAsync = util.promisify(fs.mkdtemp);
@ -49,9 +50,9 @@ export class Firefox implements BrowserType<FFBrowser> {
async launch(options: LaunchOptions = {}): Promise<FFBrowser> { async launch(options: LaunchOptions = {}): Promise<FFBrowser> {
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');
const {browserServer, downloadsPath} = await this._launchServer(options, 'local'); const { browserServer, downloadsPath, logger } = await this._launchServer(options, 'local');
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => { const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, false, options.slowMo); return FFBrowser.connect(transport, logger, false, options.slowMo);
}); });
browser._ownedServer = browserServer; browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath; browser._downloadsPath = downloadsPath;
@ -67,9 +68,9 @@ export class Firefox implements BrowserType<FFBrowser> {
timeout = 30000, timeout = 30000,
slowMo = 0, slowMo = 0,
} = options; } = options;
const {browserServer, downloadsPath} = await this._launchServer(options, 'persistent', userDataDir); const { browserServer, downloadsPath, logger } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => { const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, true, slowMo); return FFBrowser.connect(transport, logger, true, slowMo);
}); });
browser._ownedServer = browserServer; browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath; browser._downloadsPath = downloadsPath;
@ -78,11 +79,10 @@ export class Firefox implements BrowserType<FFBrowser> {
return browserContext; return browserContext;
} }
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string }> { private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, logger: Logger }> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
dumpio = false,
executablePath = null, executablePath = null,
env = process.env, env = process.env,
handleSIGHUP = true, handleSIGHUP = true,
@ -92,6 +92,7 @@ export class Firefox implements BrowserType<FFBrowser> {
port = 0, port = 0,
} = options; } = options;
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
const logger = new RootLogger(options.loggerSink);
const firefoxArguments = []; const firefoxArguments = [];
@ -123,7 +124,7 @@ export class Firefox implements BrowserType<FFBrowser> {
handleSIGINT, handleSIGINT,
handleSIGTERM, handleSIGTERM,
handleSIGHUP, handleSIGHUP,
dumpio, logger,
pipe: false, pipe: false,
tempDir: temporaryProfileDir || undefined, tempDir: temporaryProfileDir || undefined,
attemptToGracefullyClose: async () => { attemptToGracefullyClose: async () => {
@ -145,15 +146,16 @@ export class Firefox implements BrowserType<FFBrowser> {
let browserServer: BrowserServer | undefined = undefined; let browserServer: BrowserServer | undefined = undefined;
let browserWSEndpoint: string | undefined = undefined; let browserWSEndpoint: string | undefined = undefined;
const webSocketWrapper = launchType === 'server' ? (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, port))) : new WebSocketWrapper(innerEndpoint, []); const webSocketWrapper = launchType === 'server' ? (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, logger, port))) : new WebSocketWrapper(innerEndpoint, []);
browserWSEndpoint = webSocketWrapper.wsEndpoint; browserWSEndpoint = webSocketWrapper.wsEndpoint;
browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper); browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper);
return {browserServer, downloadsPath}; return { browserServer, downloadsPath, logger };
} }
async connect(options: ConnectOptions): Promise<FFBrowser> { async connect(options: ConnectOptions): Promise<FFBrowser> {
const logger = new RootLogger(options.loggerSink);
return await WebSocketTransport.connect(options.wsEndpoint, transport => { return await WebSocketTransport.connect(options.wsEndpoint, transport => {
return FFBrowser.connect(transport, false, options.slowMo); return FFBrowser.connect(transport, logger, false, options.slowMo);
}); });
} }
@ -196,7 +198,7 @@ export class Firefox implements BrowserType<FFBrowser> {
} }
} }
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper { function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: Logger, port: number): WebSocketWrapper {
const server = new ws.Server({ port }); const server = new ws.Server({ port });
const guid = helper.guid(); const guid = helper.guid();
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>(); const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
@ -302,7 +304,7 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number
pendingBrowserContextDeletions.set(seqNum, params.browserContextId); pendingBrowserContextDeletions.set(seqNum, params.browserContextId);
}); });
socket.on('error', error => debugError(error)); socket.on('error', logError(logger));
socket.on('close', (socket as any).__closeListener = () => { socket.on('close', (socket as any).__closeListener = () => {
for (const [browserContextId, s] of browserContextIds) { for (const [browserContextId, s] of browserContextIds) {

View file

@ -15,8 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { debugError, helper, RegisteredListener } from '../helper'; import { helper, RegisteredListener } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { logError, Logger } from '../logger';
export class PipeTransport implements ConnectionTransport { export class PipeTransport implements ConnectionTransport {
private _pipeWrite: NodeJS.WritableStream | null; private _pipeWrite: NodeJS.WritableStream | null;
@ -27,7 +28,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) { 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)),
@ -36,8 +37,8 @@ export class PipeTransport implements ConnectionTransport {
if (this.onclose) if (this.onclose)
this.onclose.call(null); this.onclose.call(null);
}), }),
helper.addEventListener(pipeRead, 'error', debugError), helper.addEventListener(pipeRead, 'error', logError(logger)),
helper.addEventListener(pipeWrite, 'error', debugError), helper.addEventListener(pipeWrite, 'error', logError(logger)),
]; ];
this.onmessage = undefined; this.onmessage = undefined;
this.onclose = undefined; this.onclose = undefined;

View file

@ -16,7 +16,7 @@
*/ */
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import * as debug from 'debug'; import { Log, Logger } from '../logger';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
@ -31,6 +31,20 @@ const removeFolderAsync = util.promisify(removeFolder);
const mkdtempAsync = util.promisify(fs.mkdtemp); const mkdtempAsync = util.promisify(fs.mkdtemp);
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-'); const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
const browserLog: Log = {
name: 'browser',
};
const browserStdOutLog: Log = {
name: 'browser:out',
};
const browserStdErrLog: Log = {
name: 'browser:err',
severity: 'warning'
};
export type LaunchProcessOptions = { export type LaunchProcessOptions = {
executablePath: string, executablePath: string,
args: string[], args: string[],
@ -39,13 +53,13 @@ export type LaunchProcessOptions = {
handleSIGINT?: boolean, handleSIGINT?: boolean,
handleSIGTERM?: boolean, handleSIGTERM?: boolean,
handleSIGHUP?: boolean, handleSIGHUP?: boolean,
dumpio?: boolean,
pipe?: boolean, pipe?: boolean,
tempDir?: string, tempDir?: string,
// Note: attemptToGracefullyClose should reject if it does not close the browser. // Note: attemptToGracefullyClose should reject if it does not close the browser.
attemptToGracefullyClose: () => Promise<any>, attemptToGracefullyClose: () => Promise<any>,
onkill: (exitCode: number | null, signal: string | null) => void, onkill: (exitCode: number | null, signal: string | null) => void,
logger: Logger,
}; };
type LaunchResult = { type LaunchResult = {
@ -54,17 +68,10 @@ type LaunchResult = {
downloadsPath: string downloadsPath: string
}; };
let lastLaunchedId = 0;
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> { export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
const id = ++lastLaunchedId; const logger = options.logger;
const debugBrowser = debug(`pw:browser:proc:[${id}]`);
const debugBrowserOut = debug(`pw:browser:out:[${id}]`);
const debugBrowserErr = debug(`pw:browser:err:[${id}]`);
(debugBrowser as any).color = '33';
(debugBrowserOut as any).color = '178';
(debugBrowserErr as any).color = '160';
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'];
logger._log(browserLog, `<launching> ${options.executablePath} ${options.args.join(' ')}`);
const spawnedProcess = childProcess.spawn( const spawnedProcess = childProcess.spawn(
options.executablePath, options.executablePath,
options.args, options.args,
@ -77,8 +84,6 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
stdio stdio
} }
); );
debugBrowser(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
if (!spawnedProcess.pid) { if (!spawnedProcess.pid) {
let reject: (e: Error) => void; let reject: (e: Error) => void;
const result = new Promise<LaunchResult>((f, r) => reject = r); const result = new Promise<LaunchResult>((f, r) => reject = r);
@ -87,19 +92,16 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
}); });
return result; return result;
} }
logger._log(browserLog, `<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) => {
debugBrowserOut(data); logger._log(browserStdOutLog, data);
if (options.dumpio)
console.log(`\x1b[33m[out]\x1b[0m ${data}`); // eslint-disable-line no-console
}); });
const stderr = readline.createInterface({ input: spawnedProcess.stderr }); const stderr = readline.createInterface({ input: spawnedProcess.stderr });
stderr.on('line', (data: string) => { stderr.on('line', (data: string) => {
debugBrowserErr(data); logger._log(browserStdErrLog, data);
if (options.dumpio)
console.log(`\x1b[31m[err]\x1b[0m ${data}`); // eslint-disable-line no-console
}); });
const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER); const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER);
@ -107,7 +109,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
let processClosed = false; let processClosed = false;
const waitForProcessToClose = new Promise((fulfill, reject) => { const waitForProcessToClose = new Promise((fulfill, reject) => {
spawnedProcess.once('exit', (exitCode, signal) => { spawnedProcess.once('exit', (exitCode, signal) => {
debugBrowser(`<process did exit ${exitCode}, ${signal}>`); logger._log(browserLog, `<process did exit ${exitCode}, ${signal}>`);
processClosed = true; processClosed = true;
helper.removeEventListeners(listeners); helper.removeEventListeners(listeners);
options.onkill(exitCode, signal); options.onkill(exitCode, signal);
@ -137,20 +139,20 @@ 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) {
debugBrowser(`<forecefully close>`); logger._log(browserLog, `<forecefully close>`);
killProcess(); killProcess();
return; return;
} }
gracefullyClosing = true; gracefullyClosing = true;
debugBrowser(`<gracefully close start>`); logger._log(browserLog, `<gracefully close start>`);
await options.attemptToGracefullyClose().catch(() => killProcess()); await options.attemptToGracefullyClose().catch(() => killProcess());
await waitForProcessToClose; await waitForProcessToClose;
debugBrowser(`<gracefully close end>`); logger._log(browserLog, `<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() {
debugBrowser(`<kill>`); logger._log(browserLog, `<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

@ -22,7 +22,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import * as util from 'util'; import * as util from 'util';
import { debugError, helper, assert } from '../helper'; import { helper, assert } from '../helper';
import { kBrowserCloseMessageId } from '../webkit/wkConnection'; import { kBrowserCloseMessageId } from '../webkit/wkConnection';
import { LaunchOptions, BrowserArgOptions, BrowserType, LaunchServerOptions, ConnectOptions } from './browserType'; import { LaunchOptions, BrowserArgOptions, BrowserType, LaunchServerOptions, ConnectOptions } from './browserType';
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport'; import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
@ -31,6 +31,7 @@ import { LaunchType } from '../browser';
import { BrowserServer, WebSocketWrapper } from './browserServer'; import { BrowserServer, WebSocketWrapper } from './browserServer';
import { Events } from '../events'; import { Events } from '../events';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { Logger, logError, RootLogger } from '../logger';
export class WebKit implements BrowserType<WKBrowser> { export class WebKit implements BrowserType<WKBrowser> {
private _executablePath: (string|undefined); private _executablePath: (string|undefined);
@ -47,8 +48,8 @@ export class WebKit implements BrowserType<WKBrowser> {
async launch(options: LaunchOptions = {}): Promise<WKBrowser> { async launch(options: LaunchOptions = {}): Promise<WKBrowser> {
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');
const { browserServer, transport, downloadsPath } = await this._launchServer(options, 'local'); const { browserServer, transport, downloadsPath, logger } = await this._launchServer(options, 'local');
const browser = await WKBrowser.connect(transport!, options.slowMo, false); const browser = await WKBrowser.connect(transport!, logger, options.slowMo, false);
browser._ownedServer = browserServer; browser._ownedServer = browserServer;
browser._downloadsPath = downloadsPath; browser._downloadsPath = downloadsPath;
return browser; return browser;
@ -63,18 +64,17 @@ export class WebKit implements BrowserType<WKBrowser> {
timeout = 30000, timeout = 30000,
slowMo = 0, slowMo = 0,
} = options; } = options;
const { transport, browserServer } = await this._launchServer(options, 'persistent', userDataDir); const { transport, browserServer, logger } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await WKBrowser.connect(transport!, slowMo, true); const browser = await WKBrowser.connect(transport!, logger, slowMo, true);
browser._ownedServer = browserServer; browser._ownedServer = browserServer;
await helper.waitWithTimeout(browser._waitForFirstPageTarget(), 'first page', timeout); await helper.waitWithTimeout(browser._waitForFirstPageTarget(), 'first page', timeout);
return browser._defaultContext!; return browser._defaultContext!;
} }
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string }> { private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string, logger: Logger }> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
dumpio = false,
executablePath = null, executablePath = null,
env = process.env, env = process.env,
handleSIGINT = true, handleSIGINT = true,
@ -83,6 +83,7 @@ export class WebKit implements BrowserType<WKBrowser> {
port = 0, port = 0,
} = options; } = options;
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
const logger = new RootLogger(options.loggerSink);
let temporaryUserDataDir: string | null = null; let temporaryUserDataDir: string | null = null;
if (!userDataDir) { if (!userDataDir) {
@ -109,7 +110,7 @@ export class WebKit implements BrowserType<WKBrowser> {
handleSIGINT, handleSIGINT,
handleSIGTERM, handleSIGTERM,
handleSIGHUP, handleSIGHUP,
dumpio, logger,
pipe: true, pipe: true,
tempDir: temporaryUserDataDir || undefined, tempDir: temporaryUserDataDir || undefined,
attemptToGracefullyClose: async () => { attemptToGracefullyClose: async () => {
@ -129,14 +130,14 @@ export class WebKit implements BrowserType<WKBrowser> {
let transport: ConnectionTransport | undefined = undefined; let transport: ConnectionTransport | undefined = undefined;
let browserServer: BrowserServer | undefined = undefined; let browserServer: BrowserServer | undefined = undefined;
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]); transport = new PipeTransport(stdio[3], stdio[4], logger);
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port || 0) : null); browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port || 0) : null);
return { browserServer, transport, downloadsPath }; return { browserServer, transport, downloadsPath, logger };
} }
async connect(options: ConnectOptions): Promise<WKBrowser> { async connect(options: ConnectOptions): Promise<WKBrowser> {
return await WebSocketTransport.connect(options.wsEndpoint, transport => { return await WebSocketTransport.connect(options.wsEndpoint, transport => {
return WKBrowser.connect(transport, options.slowMo); return WKBrowser.connect(transport, new RootLogger(options.loggerSink), options.slowMo);
}); });
} }
@ -169,7 +170,7 @@ const mkdtempAsync = util.promisify(fs.mkdtemp);
const WEBKIT_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-'); const WEBKIT_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper { function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: Logger, port: number): WebSocketWrapper {
const server = new ws.Server({ port }); const server = new ws.Server({ port });
const guid = helper.guid(); const guid = helper.guid();
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>(); const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
@ -282,7 +283,7 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number
pendingBrowserContextDeletions.set(seqNum, params.browserContextId); pendingBrowserContextDeletions.set(seqNum, params.browserContextId);
}); });
socket.on('error', error => debugError(error)); socket.on('error', logError(logger));
socket.on('close', (socket as any).__closeListener = () => { socket.on('close', (socket as any).__closeListener = () => {
for (const [pageProxyId, s] of pageProxyIds) { for (const [pageProxyId, s] of pageProxyIds) {

View file

@ -15,9 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
import * as debug from 'debug';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import { helper } from './helper'; import { helper } from './helper';
import { Log } from './logger';
export type ProtocolRequest = { export type ProtocolRequest = {
id: number; id: number;
@ -221,5 +221,8 @@ export class InterceptingTransport implements ConnectionTransport {
} }
} }
export const debugProtocol = debug('pw:protocol'); export const protocolLog: Log = {
(debugProtocol as any).color = '34'; name: 'protocol',
severity: 'verbose',
color: 'green'
};

View file

@ -26,6 +26,7 @@ import * as types from '../types';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection'; import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection';
import { WKPage } from './wkPage'; import { WKPage } from './wkPage';
import { Logger } from '../logger';
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15'; const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
@ -40,14 +41,14 @@ export class WKBrowser extends BrowserBase {
private _firstPageCallback: () => void = () => {}; private _firstPageCallback: () => void = () => {};
private readonly _firstPagePromise: Promise<void>; private readonly _firstPagePromise: Promise<void>;
static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> { static async connect(transport: ConnectionTransport, logger: Logger, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> {
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext); const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), logger, attachToDefaultContext);
return browser; return browser;
} }
constructor(transport: ConnectionTransport, attachToDefaultContext: boolean) { constructor(transport: ConnectionTransport, logger: Logger, attachToDefaultContext: boolean) {
super(); super(logger);
this._connection = new WKConnection(transport, this._onDisconnect.bind(this)); this._connection = new WKConnection(transport, logger, this._onDisconnect.bind(this));
this._browserSession = this._connection.browserSession; this._browserSession = this._connection.browserSession;
if (attachToDefaultContext) if (attachToDefaultContext)
@ -184,7 +185,7 @@ export class WKBrowserContext extends BrowserContextBase {
readonly _evaluateOnNewDocumentSources: string[]; readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) { constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
super(options); super(browser, options);
this._browser = browser; this._browser = browser;
this._browserContextId = browserContextId; this._browserContextId = browserContextId;
this._evaluateOnNewDocumentSources = []; this._evaluateOnNewDocumentSources = [];

View file

@ -15,11 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
import * as debug from 'debug';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { assert } from '../helper'; import { assert } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { Logger } from '../logger';
// WKPlaywright uses this special id to issue Browser.close command which we // WKPlaywright uses this special id to issue Browser.close command which we
// should ignore. // should ignore.
@ -37,9 +37,11 @@ export class WKConnection {
private _closed = false; private _closed = false;
readonly browserSession: WKSession; readonly browserSession: WKSession;
private _logger: Logger;
constructor(transport: ConnectionTransport, onDisconnect: () => void) { constructor(transport: ConnectionTransport, logger: Logger, onDisconnect: () => void) {
this._transport = transport; this._transport = transport;
this._logger = logger;
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;
@ -53,14 +55,14 @@ export class WKConnection {
} }
rawSend(message: ProtocolRequest) { rawSend(message: ProtocolRequest) {
if (debugProtocol.enabled) if (this._logger._isLogEnabled(protocolLog))
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message)); this._logger._log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
this._transport.send(message); this._transport.send(message);
} }
private _dispatchMessage(message: ProtocolResponse) { private _dispatchMessage(message: ProtocolResponse) {
if (debugProtocol.enabled) if (this._logger._isLogEnabled(protocolLog))
debugProtocol('◀ RECV ' + JSON.stringify(message)); this._logger._log(protocolLog, '◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.pageProxyId) { if (message.pageProxyId) {
@ -129,7 +131,6 @@ export class WKSession extends EventEmitter {
throw new Error(`Protocol error (${method}): ${this.errorText}`); throw new Error(`Protocol error (${method}): ${this.errorText}`);
const id = this.connection.nextMessageId(); const id = this.connection.nextMessageId();
const messageObj = { id, method, params }; const messageObj = { id, method, params };
debug('pw:wrapped:' + this.sessionId)('SEND ► ' + JSON.stringify(messageObj, null, 2));
this._rawSend(messageObj); this._rawSend(messageObj);
return new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => { return new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method}); this._callbacks.set(id, {resolve, reject, error: new Error(), method});
@ -152,7 +153,6 @@ export class WKSession extends EventEmitter {
} }
dispatchMessage(object: any) { dispatchMessage(object: any) {
debug('pw:wrapped:' + this.sessionId)('◀ RECV ' + JSON.stringify(object, null, 2));
if (object.id && this._callbacks.has(object.id)) { if (object.id && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id)!; const callback = this._callbacks.get(object.id)!;
this._callbacks.delete(object.id); this._callbacks.delete(object.id);

View file

@ -16,10 +16,11 @@
*/ */
import * as frames from '../frames'; import * as frames from '../frames';
import { assert, debugError, helper } from '../helper'; import { assert, helper } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
import { logError } from '../logger';
const errorReasons: { [reason: string]: string } = { const errorReasons: { [reason: string]: string } = {
'aborted': 'Cancellation', 'aborted': 'Cancellation',
@ -61,7 +62,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
await this._session.send('Network.interceptAsError', { requestId: this._requestId, reason }).catch(error => { await this._session.send('Network.interceptAsError', { requestId: this._requestId, reason }).catch(error => {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
debugError(error); logError(this.request._page);
}); });
} }
@ -92,7 +93,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
}).catch(error => { }).catch(error => {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
debugError(error); logError(this.request._page);
}); });
} }
@ -106,7 +107,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
}).catch((error: Error) => { }).catch((error: Error) => {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
debugError(error); logError(this.request._page);
}); });
} }

View file

@ -16,7 +16,7 @@
*/ */
import * as frames from '../frames'; import * as frames from '../frames';
import { debugError, helper, RegisteredListener, assert } from '../helper'; import { helper, RegisteredListener, assert } from '../helper';
import * as dom from '../dom'; import * as dom from '../dom';
import * as network from '../network'; import * as network from '../network';
import { WKSession } from './wkConnection'; import { WKSession } from './wkConnection';
@ -37,6 +37,7 @@ import { selectors } from '../selectors';
import * as jpeg from 'jpeg-js'; import * as jpeg from 'jpeg-js';
import * as png from 'pngjs'; import * as png from 'pngjs';
import { NotConnectedError } from '../errors'; import { NotConnectedError } from '../errors';
import { logError } from '../logger';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -267,7 +268,7 @@ export class WKPage implements PageDelegate {
pageOrError = e; pageOrError = e;
} }
if (targetInfo.isPaused) if (targetInfo.isPaused)
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError); this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(logError(this._page));
if ((pageOrError instanceof Page) && this._page.mainFrame().url() === '') { if ((pageOrError instanceof Page) && this._page.mainFrame().url() === '') {
try { try {
// Initial empty page has an empty url. We should wait until the first real url has been loaded, // Initial empty page has an empty url. We should wait until the first real url has been loaded,
@ -289,7 +290,7 @@ export class WKPage implements PageDelegate {
this._provisionalPage = new WKProvisionalPage(session, this); this._provisionalPage = new WKProvisionalPage(session, this);
if (targetInfo.isPaused) { if (targetInfo.isPaused) {
this._provisionalPage.initializationPromise.then(() => { this._provisionalPage.initializationPromise.then(() => {
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError); this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(logError(this._page));
}); });
} }
} }
@ -344,7 +345,7 @@ export class WKPage implements PageDelegate {
// as well to always be in sync with the backend. // as well to always be in sync with the backend.
if (this._provisionalPage) if (this._provisionalPage)
sessions.push(this._provisionalPage._session); sessions.push(this._provisionalPage._session);
await Promise.all(sessions.map(session => callback(session).catch(debugError))); await Promise.all(sessions.map(session => callback(session).catch(logError(this._page))));
} }
private _onFrameScheduledNavigation(frameId: string) { private _onFrameScheduledNavigation(frameId: string) {
@ -608,7 +609,7 @@ export class WKPage implements PageDelegate {
private async _evaluateBindingScript(binding: PageBinding): Promise<void> { private async _evaluateBindingScript(binding: PageBinding): Promise<void> {
const script = this._bindingToScript(binding); const script = this._bindingToScript(binding);
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError))); await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(logError(this._page))));
} }
async evaluateOnNewDocument(script: string): Promise<void> { async evaluateOnNewDocument(script: string): Promise<void> {
@ -639,7 +640,7 @@ export class WKPage implements PageDelegate {
this._pageProxySession.send('Target.close', { this._pageProxySession.send('Target.close', {
targetId: this._session.sessionId, targetId: this._session.sessionId,
runBeforeUnload runBeforeUnload
}).catch(debugError); }).catch(logError(this._page));
} }
canScreenshotOutsideViewport(): boolean { canScreenshotOutsideViewport(): boolean {
@ -725,7 +726,7 @@ export class WKPage implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> { async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('DOM.getContentQuads', { const result = await this._session.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId! objectId: toRemoteObject(handle).objectId!
}).catch(debugError); }).catch(logError(this._page));
if (!result) if (!result)
return null; return null;
return result.quads.map(quad => [ return result.quads.map(quad => [
@ -749,7 +750,7 @@ export class WKPage implements PageDelegate {
const result = await this._session.send('DOM.resolveNode', { const result = await this._session.send('DOM.resolveNode', {
objectId: toRemoteObject(handle).objectId, objectId: toRemoteObject(handle).objectId,
executionContextId: (to._delegate as WKExecutionContext)._contextId executionContextId: (to._delegate as WKExecutionContext)._contextId
}).catch(debugError); }).catch(logError(this._page));
if (!result || result.object.subtype === 'null') if (!result || result.object.subtype === 'null')
throw new Error('Unable to adopt element handle from a different document'); throw new Error('Unable to adopt element handle from a different document');
return to._createHandle(result.object) as dom.ElementHandle<T>; return to._createHandle(result.object) as dom.ElementHandle<T>;

View file

@ -34,7 +34,7 @@ export class WKWorkers {
this.clear(); this.clear();
this._sessionListeners = [ this._sessionListeners = [
helper.addEventListener(session, 'Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => { helper.addEventListener(session, 'Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => {
const worker = new Worker(event.url); const worker = new Worker(this._page, event.url);
const workerSession = new WKSession(session.connection, event.workerId, 'Most likely the worker has been closed.', (message: any) => { const workerSession = new WKSession(session.connection, event.workerId, 'Most likely the worker has been closed.', (message: any) => {
session.send('Worker.sendMessageToWorker', { session.send('Worker.sendMessageToWorker', {
workerId: event.workerId, workerId: event.workerId,

View file

@ -63,13 +63,6 @@ async function testSignal(state, action, exitOnClose) {
} }
describe('Fixtures', function() { describe('Fixtures', function() {
it.slow()('should dump browser process stderr', async state => {
let dumpioData = '';
const res = spawn('node', [path.join(__dirname, 'fixtures', 'dumpio.js'), state.playwrightPath, state.browserType.name()]);
res.stdout.on('data', data => dumpioData += data.toString('utf8'));
await new Promise(resolve => res.on('close', resolve));
expect(dumpioData).toContain('message from dumpio');
});
it.slow()('should close the browser when the node process closes', async state => { it.slow()('should close the browser when the node process closes', async state => {
const result = await testSignal(state, child => { const result = await testSignal(state, child => {
if (WIN) if (WIN)

View file

@ -1,20 +0,0 @@
(async() => {
process.on('unhandledRejection', error => {
// Catch various errors as we launch non-browser binary.
console.log('unhandledRejection', error.message);
});
const [, , playwrightRoot, browserType] = process.argv;
const options = {
ignoreDefaultArgs: true,
dumpio: true,
timeout: 1,
executablePath: 'node',
args: ['-e', 'console.error("message from dumpio")', '--']
}
try {
await require(playwrightRoot)[browserType].launchServer(options);
console.error('Browser launch unexpectedly succeeded.');
} catch (e) {
}
})();

View file

@ -103,43 +103,41 @@ function collect(browserNames) {
} }
const browserEnvironment = new Environment(browserName); const browserEnvironment = new Environment(browserName);
let logger;
browserEnvironment.beforeAll(async state => { browserEnvironment.beforeAll(async state => {
state.browser = await state.browserType.launch(launchOptions); state.browser = await state.browserType.launch({...launchOptions, loggerSink: {
state._stdout = readline.createInterface({ input: state.browser._ownedServer.process().stdout }); isEnabled: (name, severity) => {
state._stderr = readline.createInterface({ input: state.browser._ownedServer.process().stderr }); return name === 'browser' ||
(name === 'protocol' && config.dumpProtocolOnFailure);
},
log: (name, severity, message, args) => {
if (logger)
logger(name, severity, message);
}
}});
}); });
browserEnvironment.afterAll(async state => { browserEnvironment.afterAll(async state => {
await state.browser.close(); await state.browser.close();
delete state.browser; delete state.browser;
state._stdout.close();
state._stderr.close();
delete state._stdout;
delete state._stderr;
}); });
browserEnvironment.beforeEach(async(state, testRun) => { browserEnvironment.beforeEach(async(state, testRun) => {
const dumpout = data => testRun.log(`\x1b[33m[pw:stdio:out]\x1b[0m ${data}`); logger = (name, severity, message) => {
const dumperr = data => testRun.log(`\x1b[31m[pw:stdio:err]\x1b[0m ${data}`); if (name === 'browser') {
state._stdout.on('line', dumpout); if (severity === 'warning')
state._stderr.on('line', dumperr); testRun.log(`\x1b[31m[browser]\x1b[0m ${message}`)
// TODO: figure out debug options. else
if (config.dumpProtocolOnFailure) { testRun.log(`\x1b[33m[browser]\x1b[0m ${message}`)
state.browser._debugProtocol.log = data => testRun.log(`\x1b[32m[pw:protocol]\x1b[0m ${data}`); } else if (name === 'protocol' && config.dumpProtocolOnFailure) {
state.browser._debugProtocol.enabled = true; testRun.log(`\x1b[32m[protocol]\x1b[0m ${message}`)
}
state._browserTearDown = async (testRun) => {
state._stdout.off('line', dumpout);
state._stderr.off('line', dumperr);
if (config.dumpProtocolOnFailure) {
delete state.browser._debugProtocol.log;
state.browser._debugProtocol.enabled = false;
if (testRun.ok())
testRun.output().splice(0);
} }
}; }
}); });
browserEnvironment.afterEach(async (state, testRun) => { browserEnvironment.afterEach(async (state, testRun) => {
await state._browserTearDown(testRun); logger = null;
delete state._browserTearDown; if (config.dumpProtocolOnFailure) {
if (testRun.ok())
testRun.output().splice(0);
}
}); });
const pageEnvironment = new Environment('Page'); const pageEnvironment = new Environment('Page');