mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(logging): introduce logger sink api (#1861)
This commit is contained in:
parent
b8259837a4
commit
1f43ae692f
47
docs/api.md
47
docs/api.md
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()!;
|
||||
|
||||
28
src/dom.ts
28
src/dom.ts
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 ]);
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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'));
|
||||
|
||||
13
src/hints.ts
13
src/hints.ts
@ -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.`);
|
||||
}
|
||||
|
||||
@ -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
109
src/logger.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
21
src/page.ts
21
src/page.ts
@ -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);
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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'
|
||||
};
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
20
test/fixtures/dumpio.js
vendored
20
test/fixtures/dumpio.js
vendored
@ -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) {
|
||||
}
|
||||
})();
|
||||
52
test/test.js
52
test/test.js
@ -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');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user