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: BrowserServer](#class-browserserver)
- [class: BrowserType](#class-browsertype)
- [class: LoggerSink](#class-loggersink)
- [class: ChromiumBrowser](#class-chromiumbrowser)
- [class: ChromiumBrowserContext](#class-chromiumbrowsercontext)
- [class: ChromiumCoverage](#class-chromiumcoverage)
@ -3767,6 +3768,7 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
- `options` <[Object]>
- `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.
- `loggerSink` <[LoggerSink]> Sink for log messages.
- returns: <[Promise]<[Browser]>>
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`.
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. 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.
- `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`.
- `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.
@ -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`.
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. 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.
- `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`.
- `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.
@ -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`.
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. 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.
- `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`.
- `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.
@ -3862,6 +3864,44 @@ const { chromium } = require('playwright'); // Or '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
* extends: [Browser]
@ -4218,6 +4258,7 @@ const { chromium } = require('playwright');
[Frame]: #class-frame "Frame"
[JSHandle]: #class-jshandle "JSHandle"
[Keyboard]: #class-keyboard "Keyboard"
[LoggerSink]: #class-loggersink "LoggerSink"
[Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map"
[Mouse]: #class-mouse "Mouse"
[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",
"dependencies": {
"debug": "^4.1.0",
"extract-zip": "^2.0.0",
"https-proxy-agent": "^3.0.0",
"jpeg-js": "^0.3.7",
@ -68,6 +67,7 @@
"colors": "^1.4.0",
"commonmark": "^0.28.1",
"cross-env": "^5.0.5",
"debug": "^4.1.0",
"eslint": "^6.6.0",
"esprima": "^4.0.0",
"formidable": "^1.2.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,11 +16,12 @@
*/
import { CRSession } from './crConnection';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import { assert, helper, RegisteredListener } from '../helper';
import { Protocol } from './protocol';
import { EVALUATION_SCRIPT_URL } from './crExecutionContext';
import * as types from '../types';
import { logError, Logger } from '../logger';
type JSRange = {
startOffset: number,
@ -50,9 +51,9 @@ export class CRCoverage {
private _jsCoverage: JSCoverage;
private _cssCoverage: CSSCoverage;
constructor(client: CRSession) {
this._jsCoverage = new JSCoverage(client);
this._cssCoverage = new CSSCoverage(client);
constructor(client: CRSession, logger: Logger) {
this._jsCoverage = new JSCoverage(client, logger);
this._cssCoverage = new CSSCoverage(client, logger);
}
async startJSCoverage(options?: types.JSCoverageOptions) {
@ -80,9 +81,11 @@ class JSCoverage {
_eventListeners: RegisteredListener[];
_resetOnNavigation: boolean;
_reportAnonymousScripts = false;
private _logger: Logger;
constructor(client: CRSession) {
constructor(client: CRSession, logger: Logger) {
this._client = client;
this._logger = logger;
this._enabled = false;
this._scriptIds = new Set();
this._scriptSources = new Map();
@ -134,7 +137,7 @@ class JSCoverage {
this._scriptSources.set(event.scriptId, response.scriptSource);
} catch (e) {
// 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>;
_eventListeners: RegisteredListener[];
_resetOnNavigation: boolean;
private _logger: Logger;
constructor(client: CRSession) {
constructor(client: CRSession, logger: Logger) {
this._client = client;
this._logger = logger;
this._enabled = false;
this._stylesheetURLs = new Map();
this._stylesheetSources = new Map();
@ -218,7 +223,7 @@ class CSSCoverage {
this._stylesheetSources.set(header.styleSheetId, response.text);
} catch (e) {
// 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 { Page } from '../page';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import { assert, helper, RegisteredListener } from '../helper';
import { Protocol } from './protocol';
import * as network from '../network';
import * as frames from '../frames';
import { Credentials } from '../types';
import { CRPage } from './crPage';
import { logError } from '../logger';
export class CRNetworkManager {
private _client: CRSession;
@ -130,14 +131,14 @@ export class CRNetworkManager {
this._client.send('Fetch.continueWithAuth', {
requestId: event.requestId,
authChallengeResponse: { response, username, password },
}).catch(debugError);
}).catch(logError(this._page));
}
_onRequestPaused(workerFrame: frames.Frame | undefined, event: Protocol.Fetch.requestPausedPayload) {
if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
this._client.send('Fetch.continueRequest', {
requestId: event.requestId
}).catch(debugError);
}).catch(logError(this._page));
}
if (!event.networkId || event.request.url.startsWith('data:'))
return;
@ -176,7 +177,7 @@ export class CRNetworkManager {
if (!frame) {
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;
}
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
@ -299,7 +300,7 @@ class InterceptableRequest implements network.RouteDelegate {
}).catch(error => {
// In certain cases, protocol will return error if the request was already canceled
// 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 => {
// In certain cases, protocol will return error if the request was already canceled
// 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 => {
// In certain cases, protocol will return error if the request was already canceled
// 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 js from '../javascript';
import * as frames from '../frames';
import { debugError, helper, RegisteredListener, assert } from '../helper';
import { helper, RegisteredListener, assert } from '../helper';
import * as network from '../network';
import { CRSession, CRConnection, CRSessionEvents } from './crConnection';
import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext';
@ -37,6 +37,7 @@ import { CRBrowserContext } from './crBrowser';
import * as types from '../types';
import { ConsoleMessage } from '../console';
import { NotConnectedError } from '../errors';
import { logError } from '../logger';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -60,7 +61,7 @@ export class CRPage implements PageDelegate {
this.rawKeyboard = new RawKeyboardImpl(client);
this.rawMouse = new RawMouseImpl(client);
this._pdf = new CRPDF(client);
this._coverage = new CRCoverage(client);
this._coverage = new CRCoverage(client, browserContext);
this._browserContext = browserContext;
this._page = new Page(this, browserContext);
this._mainFrameSession = new FrameSession(this, client, targetId);
@ -127,7 +128,7 @@ export class CRPage implements PageDelegate {
async exposeBinding(binding: PageBinding) {
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> {
@ -381,9 +382,9 @@ class FrameSession {
frameId: frame._id,
grantUniveralAccess: true,
worldName: UTILITY_WORLD_NAME,
}).catch(debugError);
}).catch(logError(this._page));
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() === ':';
if (isInitialEmptyPage) {
@ -543,14 +544,14 @@ class FrameSession {
if (event.targetInfo.type !== 'worker') {
// Ideally, detaching should resume any target, but there is a bug in the backend.
session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => {
this._client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(debugError);
session.send('Runtime.runIfWaitingForDebugger').catch(logError(this._page)).then(() => {
this._client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(logError(this._page));
});
return;
}
const url = event.targetInfo.url;
const worker = new Worker(url);
const worker = new Worker(this._page, url);
this._page._addWorker(event.sessionId, worker);
session.once('Runtime.executionContextCreated', async event => {
worker._createExecutionContext(new CRExecutionContext(session, event.context));
@ -559,7 +560,7 @@ class FrameSession {
session.send('Runtime.enable'),
session.send('Network.enable'),
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 => {
const args = event.args.map(o => worker._existingExecutionContext!._createHandle(o));
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
@ -750,7 +751,7 @@ class FrameSession {
async _getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const result = await this._client.send('DOM.getBoxModel', {
objectId: toRemoteObject(handle).objectId
}).catch(debugError);
}).catch(logError(this._page));
if (!result)
return null;
const quad = result.model.border;
@ -777,7 +778,7 @@ class FrameSession {
async _getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._client.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId
}).catch(debugError);
}).catch(logError(this._page));
if (!result)
return null;
return result.quads.map(quad => [
@ -799,7 +800,7 @@ class FrameSession {
const result = await this._client.send('DOM.resolveNode', {
backendNodeId,
executionContextId: (to._delegate as CRExecutionContext)._contextId,
}).catch(debugError);
}).catch(logError(this._page));
if (!result || result.object.subtype === 'null')
throw new Error('Unable to adopt element handle from a different document');
return to._createHandle(result.object).asElement()!;

View File

@ -19,7 +19,7 @@ import * as mime from 'mime';
import * as path from 'path';
import * as util from 'util';
import * as frames from './frames';
import { assert, debugError, helper, debugInput } from './helper';
import { assert, helper } from './helper';
import { Injected, InjectedResult } from './injected/injected';
import * as input from './input';
import * as js from './javascript';
@ -27,6 +27,7 @@ import { Page } from './page';
import { selectors } from './selectors';
import * as types from './types';
import { NotConnectedError, TimeoutError } from './errors';
import { Log, logError } from './logger';
export type PointerActionOptions = {
modifiers?: input.Modifier[];
@ -37,12 +38,17 @@ export type ClickOptions = PointerActionOptions & input.MouseClickOptions;
export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions;
export const inputLog: Log = {
name: 'input',
color: 'cyan'
};
export class FrameExecutionContext extends js.ExecutionContext {
readonly frame: frames.Frame;
private _injectedPromise?: Promise<js.JSHandle>;
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame) {
super(delegate);
super(delegate, frame._page);
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> {
debugInput('scrolling into veiw if needed...');
this._page._log(inputLog, 'scrolling into view if needed...');
await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect);
debugInput('...done');
this._page._log(inputLog, '...done');
}
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> {
const [box, border] = await Promise.all([
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 };
if (box) {
@ -227,9 +233,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
debugInput('performing input action...');
this._page._log(inputLog, 'performing input action...');
await action(point);
debugInput('...done');
this._page._log(inputLog, '...done');
if (restoreModifiers)
await this._page.keyboard._ensureModifiers(restoreModifiers);
}, deadline, options, true);
@ -404,18 +410,18 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
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) => {
return injected.waitForDisplayedAtStablePosition(node, timeout);
}, helper.timeUntilDeadline(deadline));
const timeoutMessage = 'element to be displayed and not moving';
const injectedResult = await helper.waitWithDeadline(stablePromise, timeoutMessage, deadline);
handleInjectedResult(injectedResult, timeoutMessage);
debugInput('...done');
this._page._log(inputLog, '...done');
}
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();
if (frame && frame.parentFrame()) {
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 injectedResult = await helper.waitWithDeadline(hitTargetPromise, timeoutMessage, deadline);
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 { FFPage } from './ffPage';
import { Protocol } from './protocol';
import { Logger } from '../logger';
export class FFBrowser extends BrowserBase {
_connection: FFConnection;
@ -37,15 +38,15 @@ export class FFBrowser extends BrowserBase {
readonly _firstPagePromise: Promise<void>;
private _firstPageCallback = () => {};
static async connect(transport: ConnectionTransport, attachToDefaultContext: boolean, slowMo?: number): Promise<FFBrowser> {
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo));
const browser = new FFBrowser(connection, attachToDefaultContext);
static async connect(transport: ConnectionTransport, logger: Logger, attachToDefaultContext: boolean, slowMo?: number): Promise<FFBrowser> {
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo), logger);
const browser = new FFBrowser(connection, logger, attachToDefaultContext);
await connection.send('Browser.enable', { attachToDefaultContext });
return browser;
}
constructor(connection: FFConnection, isPersistent: boolean) {
super();
constructor(connection: FFConnection, logger: Logger, isPersistent: boolean) {
super(logger);
this._connection = connection;
this._ffPages = new Map();
@ -172,7 +173,7 @@ export class FFBrowserContext extends BrowserContextBase {
private readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super(options);
super(browser, options);
this._browser = browser;
this._browserContextId = browserContextId;
this._evaluateOnNewDocumentSources = [];

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import * as dialog from '../dialog';
import * as dom from '../dom';
import { Events } from '../events';
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 { kScreenshotDuringNavigationError } from '../screenshotter';
import * as types from '../types';
@ -32,6 +32,7 @@ import { FFNetworkManager, headersArray } from './ffNetworkManager';
import { Protocol } from './protocol';
import { selectors } from '../selectors';
import { NotConnectedError } from '../errors';
import { logError } from '../logger';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -199,7 +200,7 @@ export class FFPage implements PageDelegate {
params.type,
params.message,
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));
}
@ -218,7 +219,7 @@ export class FFPage implements PageDelegate {
async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) {
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) => {
this._session.send('Page.sendMessageToWorker', {
frameId: event.frameId,
@ -434,7 +435,7 @@ export class FFPage implements PageDelegate {
const result = await this._session.send('Page.getContentQuads', {
frameId: handle._context.frame._id,
objectId: toRemoteObject(handle).objectId!,
}).catch(debugError);
}).catch(logError(this._page));
if (!result)
return null;
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 { TimeoutError, NotConnectedError } from './errors';
import { Events } from './events';
import { assert, helper, RegisteredListener, debugInput } from './helper';
import { assert, helper, RegisteredListener } from './helper';
import * as js from './javascript';
import * as network from './network';
import { Page } from './page';
@ -710,7 +710,7 @@ export class Frame {
} catch (e) {
if (!(e instanceof NotConnectedError))
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`);
@ -775,7 +775,7 @@ export class Frame {
if (helper.isString(selectorOrFunctionOrTimeout))
return this.waitForSelector(selectorOrFunctionOrTimeout, options) as any;
if (helper.isNumber(selectorOrFunctionOrTimeout)) {
waitForTimeWasUsed();
waitForTimeWasUsed(this._page);
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
}
if (typeof selectorOrFunctionOrTimeout === 'function')

View File

@ -16,16 +16,12 @@
*/
import * as crypto from 'crypto';
import * as debug from 'debug';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as util from 'util';
import { TimeoutError } from './errors';
import * as types from './types';
export const debugError = debug(`pw:error`);
export const debugInput = debug('pw:input');
export type RegisteredListener = {
emitter: EventEmitter;
eventName: (string | symbol);
@ -66,37 +62,16 @@ class Helper {
}
static installApiHooks(className: string, classType: any) {
const log = debug('pw:api');
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
continue;
const isAsync = method.constructor.name === 'AsyncFunction';
if (!isAsync && !log.enabled)
if (!isAsync)
continue;
Reflect.set(classType.prototype, methodName, function(this: any, ...args: any[]) {
const syncStack: any = {};
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) => {
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
const clientStack = stack.substring(stack.indexOf('\n'));

View File

@ -14,17 +14,20 @@
* limitations under the License.
*/
import * as debug from 'debug';
import { Page } from './page';
import { Log } from './logger';
export const debugHints = debug('pw:hints');
(debugHints as any).color = '11';
const hintsLog: Log = {
name: 'hint',
severity: 'warning'
};
let waitForTimeWasUsedReported = false;
export function waitForTimeWasUsed() {
export function waitForTimeWasUsed(page: Page) {
if (waitForTimeWasUsedReported)
return;
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.
Use signals such as network events, selectors becoming visible, etc. instead.`);
}

View File

@ -17,6 +17,7 @@
import * as types from './types';
import * as dom from './dom';
import { helper } from './helper';
import { Logger } from './logger';
export interface ExecutionContextDelegate {
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
@ -28,9 +29,11 @@ export interface ExecutionContextDelegate {
export class ExecutionContext {
readonly _delegate: ExecutionContextDelegate;
readonly _logger: Logger;
constructor(delegate: ExecutionContextDelegate) {
constructor(delegate: ExecutionContextDelegate, logger: Logger) {
this._delegate = delegate;
this._logger = logger;
}
_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 frames from './frames';
import { assert, helper } from './helper';
import { Page } from './page';
export type NetworkCookie = {
name: string,
@ -110,12 +111,14 @@ export class Request {
private _frame: frames.Frame;
private _waitForResponsePromise: Promise<Response | null>;
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
readonly _page: Page;
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
url: string, resourceType: string, method: string, postData: string | null, headers: Headers) {
assert(!url.startsWith('data:'), 'Data urls should not fire requests');
this._routeDelegate = routeDelegate;
this._frame = frame;
this._page = frame._page;
this._redirectedFrom = redirectedFrom;
if (redirectedFrom)
redirectedFrom._redirectedTo = this;

View File

@ -17,7 +17,7 @@
import * as dom from './dom';
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 js from './javascript';
import * as network from './network';
@ -31,6 +31,7 @@ import * as accessibility from './accessibility';
import { ExtendedEventEmitter } from './extendedEventEmitter';
import { EventEmitter } from 'events';
import { FileChooser } from './fileChooser';
import { logError, Logger, Log } from './logger';
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
@ -86,7 +87,7 @@ type PageState = {
extraHTTPHeaders: network.Headers | null;
};
export class Page extends ExtendedEventEmitter {
export class Page extends ExtendedEventEmitter implements Logger {
private _closed = false;
private _closedCallback: () => void;
private _closedPromise: Promise<void>;
@ -525,23 +526,33 @@ export class Page extends ExtendedEventEmitter {
this._delegate.setFileChooserIntercepted(false);
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 {
private _logger: Logger;
private _url: string;
private _executionContextPromise: Promise<js.ExecutionContext>;
private _executionContextCallback: (value?: js.ExecutionContext) => void;
_existingExecutionContext: js.ExecutionContext | null = null;
constructor(url: string) {
constructor(logger: Logger, url: string) {
super();
this._logger = logger;
this._url = url;
this._executionContextCallback = () => {};
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
}
_createExecutionContext(delegate: js.ExecutionContextDelegate) {
this._existingExecutionContext = new js.ExecutionContext(delegate);
this._existingExecutionContext = new js.ExecutionContext(delegate, this._logger);
this._executionContextCallback(this._existingExecutionContext);
}
@ -588,7 +599,7 @@ export class PageBinding {
else
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) {
(window as any)[name]['callbacks'].get(seq).resolve(result);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import * as types from '../types';
import { Protocol } from './protocol';
import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection';
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';
@ -40,14 +41,14 @@ export class WKBrowser extends BrowserBase {
private _firstPageCallback: () => void = () => {};
private readonly _firstPagePromise: Promise<void>;
static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> {
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext);
static async connect(transport: ConnectionTransport, logger: Logger, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> {
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), logger, attachToDefaultContext);
return browser;
}
constructor(transport: ConnectionTransport, attachToDefaultContext: boolean) {
super();
this._connection = new WKConnection(transport, this._onDisconnect.bind(this));
constructor(transport: ConnectionTransport, logger: Logger, attachToDefaultContext: boolean) {
super(logger);
this._connection = new WKConnection(transport, logger, this._onDisconnect.bind(this));
this._browserSession = this._connection.browserSession;
if (attachToDefaultContext)
@ -184,7 +185,7 @@ export class WKBrowserContext extends BrowserContextBase {
readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
super(options);
super(browser, options);
this._browser = browser;
this._browserContextId = browserContextId;
this._evaluateOnNewDocumentSources = [];

View File

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

View File

@ -16,10 +16,11 @@
*/
import * as frames from '../frames';
import { assert, debugError, helper } from '../helper';
import { assert, helper } from '../helper';
import * as network from '../network';
import { Protocol } from './protocol';
import { WKSession } from './wkConnection';
import { logError } from '../logger';
const errorReasons: { [reason: string]: string } = {
'aborted': 'Cancellation',
@ -61,7 +62,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
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
// 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 => {
// In certain cases, protocol will return error if the request was already canceled
// 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) => {
// In certain cases, protocol will return error if the request was already canceled
// 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 { debugError, helper, RegisteredListener, assert } from '../helper';
import { helper, RegisteredListener, assert } from '../helper';
import * as dom from '../dom';
import * as network from '../network';
import { WKSession } from './wkConnection';
@ -37,6 +37,7 @@ import { selectors } from '../selectors';
import * as jpeg from 'jpeg-js';
import * as png from 'pngjs';
import { NotConnectedError } from '../errors';
import { logError } from '../logger';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -267,7 +268,7 @@ export class WKPage implements PageDelegate {
pageOrError = e;
}
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() === '') {
try {
// 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);
if (targetInfo.isPaused) {
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.
if (this._provisionalPage)
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) {
@ -608,7 +609,7 @@ export class WKPage implements PageDelegate {
private async _evaluateBindingScript(binding: PageBinding): Promise<void> {
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> {
@ -639,7 +640,7 @@ export class WKPage implements PageDelegate {
this._pageProxySession.send('Target.close', {
targetId: this._session.sessionId,
runBeforeUnload
}).catch(debugError);
}).catch(logError(this._page));
}
canScreenshotOutsideViewport(): boolean {
@ -725,7 +726,7 @@ export class WKPage implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId!
}).catch(debugError);
}).catch(logError(this._page));
if (!result)
return null;
return result.quads.map(quad => [
@ -749,7 +750,7 @@ export class WKPage implements PageDelegate {
const result = await this._session.send('DOM.resolveNode', {
objectId: toRemoteObject(handle).objectId,
executionContextId: (to._delegate as WKExecutionContext)._contextId
}).catch(debugError);
}).catch(logError(this._page));
if (!result || result.object.subtype === 'null')
throw new Error('Unable to adopt element handle from a different document');
return to._createHandle(result.object) as dom.ElementHandle<T>;

View File

@ -34,7 +34,7 @@ export class WKWorkers {
this.clear();
this._sessionListeners = [
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) => {
session.send('Worker.sendMessageToWorker', {
workerId: event.workerId,

View File

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