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: Worker](#class-worker)
|
||||||
- [class: BrowserServer](#class-browserserver)
|
- [class: BrowserServer](#class-browserserver)
|
||||||
- [class: BrowserType](#class-browsertype)
|
- [class: BrowserType](#class-browsertype)
|
||||||
|
- [class: LoggerSink](#class-loggersink)
|
||||||
- [class: ChromiumBrowser](#class-chromiumbrowser)
|
- [class: ChromiumBrowser](#class-chromiumbrowser)
|
||||||
- [class: ChromiumBrowserContext](#class-chromiumbrowsercontext)
|
- [class: ChromiumBrowserContext](#class-chromiumbrowsercontext)
|
||||||
- [class: ChromiumCoverage](#class-chromiumcoverage)
|
- [class: ChromiumCoverage](#class-chromiumcoverage)
|
||||||
|
|
@ -3767,6 +3768,7 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
|
||||||
- `options` <[Object]>
|
- `options` <[Object]>
|
||||||
- `wsEndpoint` <[string]> A browser websocket endpoint to connect to. **required**
|
- `wsEndpoint` <[string]> A browser websocket endpoint to connect to. **required**
|
||||||
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0.
|
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0.
|
||||||
|
- `loggerSink` <[LoggerSink]> Sink for log messages.
|
||||||
- returns: <[Promise]<[Browser]>>
|
- returns: <[Promise]<[Browser]>>
|
||||||
|
|
||||||
This methods attaches Playwright to an existing browser instance.
|
This methods attaches Playwright to an existing browser instance.
|
||||||
|
|
@ -3783,8 +3785,8 @@ This methods attaches Playwright to an existing browser instance.
|
||||||
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
|
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
|
||||||
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
|
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
|
||||||
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
|
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
|
||||||
|
- `loggerSink` <[LoggerSink]> Sink for log messages.
|
||||||
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
|
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
|
||||||
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
|
|
||||||
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
|
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
|
||||||
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
|
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
|
||||||
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
|
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
|
||||||
|
|
@ -3816,8 +3818,8 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
|
||||||
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
|
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
|
||||||
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
|
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
|
||||||
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
|
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
|
||||||
|
- `loggerSink` <[LoggerSink]> Sink for log messages.
|
||||||
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
|
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
|
||||||
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
|
|
||||||
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
|
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
|
||||||
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
|
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
|
||||||
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0.
|
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0.
|
||||||
|
|
@ -3835,8 +3837,8 @@ Launches browser instance that uses persistent storage located at `userDataDir`.
|
||||||
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
|
- `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`.
|
||||||
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
|
- `handleSIGTERM` <[boolean]> Close the browser process on SIGTERM. Defaults to `true`.
|
||||||
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
|
- `handleSIGHUP` <[boolean]> Close the browser process on SIGHUP. Defaults to `true`.
|
||||||
|
- `loggerSink` <[LoggerSink]> Sink for log messages.
|
||||||
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
|
- `timeout` <[number]> Maximum time in milliseconds to wait for the browser instance to start. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
|
||||||
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
|
|
||||||
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
|
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
|
||||||
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
|
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
|
||||||
- returns: <[Promise]<[BrowserServer]>> Promise which resolves to the browser app instance.
|
- returns: <[Promise]<[BrowserServer]>> Promise which resolves to the browser app instance.
|
||||||
|
|
@ -3862,6 +3864,44 @@ const { chromium } = require('playwright'); // Or 'webkit' or 'firefox'.
|
||||||
|
|
||||||
Returns browser name. For example: `'chromium'`, `'webkit'` or `'firefox'`.
|
Returns browser name. For example: `'chromium'`, `'webkit'` or `'firefox'`.
|
||||||
|
|
||||||
|
### class: LoggerSink
|
||||||
|
|
||||||
|
Playwright generates a lot of logs and they are accessible via the pluggable logger sink.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
loggerSink: {
|
||||||
|
isEnabled: (name, severity) => name === 'browser',
|
||||||
|
log: (name, severity, message, args) => console.log(`${name} ${message}`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
...
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- GEN:toc -->
|
||||||
|
- [loggerSink.isEnabled(name, severity)](#loggersinkisenabledname-severity)
|
||||||
|
- [loggerSink.log(name, severity, message, args, hints)](#loggersinklogname-severity-message-args-hints)
|
||||||
|
<!-- GEN:stop -->
|
||||||
|
|
||||||
|
#### loggerSink.isEnabled(name, severity)
|
||||||
|
- `name` <[string]> logger name
|
||||||
|
- `severity` <"verbose"|"info"|"warning"|"error">
|
||||||
|
- returns: <[boolean]>
|
||||||
|
|
||||||
|
Determines whether sink is interested in the logger with the given name and severity.
|
||||||
|
|
||||||
|
#### loggerSink.log(name, severity, message, args, hints)
|
||||||
|
- `name` <[string]> logger name
|
||||||
|
- `severity` <"verbose"|"info"|"warning"|"error">
|
||||||
|
- `message` <[string]|[Error]> log message format
|
||||||
|
- `args` <[Array]<[Object]>> message arguments
|
||||||
|
- `hints` <[Object]> optional formatting hints
|
||||||
|
- `color` <[string]> preferred logger color
|
||||||
|
|
||||||
### class: ChromiumBrowser
|
### class: ChromiumBrowser
|
||||||
|
|
||||||
* extends: [Browser]
|
* extends: [Browser]
|
||||||
|
|
@ -4218,6 +4258,7 @@ const { chromium } = require('playwright');
|
||||||
[Frame]: #class-frame "Frame"
|
[Frame]: #class-frame "Frame"
|
||||||
[JSHandle]: #class-jshandle "JSHandle"
|
[JSHandle]: #class-jshandle "JSHandle"
|
||||||
[Keyboard]: #class-keyboard "Keyboard"
|
[Keyboard]: #class-keyboard "Keyboard"
|
||||||
|
[LoggerSink]: #class-loggersink "LoggerSink"
|
||||||
[Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map"
|
[Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map"
|
||||||
[Mouse]: #class-mouse "Mouse"
|
[Mouse]: #class-mouse "Mouse"
|
||||||
[Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object"
|
[Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object"
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.1.0",
|
|
||||||
"extract-zip": "^2.0.0",
|
"extract-zip": "^2.0.0",
|
||||||
"https-proxy-agent": "^3.0.0",
|
"https-proxy-agent": "^3.0.0",
|
||||||
"jpeg-js": "^0.3.7",
|
"jpeg-js": "^0.3.7",
|
||||||
|
|
@ -68,6 +67,7 @@
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"commonmark": "^0.28.1",
|
"commonmark": "^0.28.1",
|
||||||
"cross-env": "^5.0.5",
|
"cross-env": "^5.0.5",
|
||||||
|
"debug": "^4.1.0",
|
||||||
"eslint": "^6.6.0",
|
"eslint": "^6.6.0",
|
||||||
"esprima": "^4.0.0",
|
"esprima": "^4.0.0",
|
||||||
"formidable": "^1.2.1",
|
"formidable": "^1.2.1",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export { Dialog } from './dialog';
|
||||||
export { Download } from './download';
|
export { Download } from './download';
|
||||||
export { ElementHandle } from './dom';
|
export { ElementHandle } from './dom';
|
||||||
export { FileChooser } from './fileChooser';
|
export { FileChooser } from './fileChooser';
|
||||||
|
export { LoggerSink } from './logger';
|
||||||
export { TimeoutError } from './errors';
|
export { TimeoutError } from './errors';
|
||||||
export { Frame } from './frames';
|
export { Frame } from './frames';
|
||||||
export { Keyboard, Mouse } from './input';
|
export { Keyboard, Mouse } from './input';
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ import { BrowserContext, BrowserContextOptions } from './browserContext';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Download } from './download';
|
import { Download } from './download';
|
||||||
import { debugProtocol } from './transport';
|
|
||||||
import type { BrowserServer } from './server/browserServer';
|
import type { BrowserServer } from './server/browserServer';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
|
import { Logger, Log } from './logger';
|
||||||
|
|
||||||
export interface Browser extends EventEmitter {
|
export interface Browser extends EventEmitter {
|
||||||
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
|
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
|
||||||
|
|
@ -30,11 +30,16 @@ export interface Browser extends EventEmitter {
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BrowserBase extends EventEmitter implements Browser {
|
export abstract class BrowserBase extends EventEmitter implements Browser, Logger {
|
||||||
_downloadsPath: string = '';
|
_downloadsPath: string = '';
|
||||||
private _downloads = new Map<string, Download>();
|
private _downloads = new Map<string, Download>();
|
||||||
_debugProtocol = debugProtocol;
|
|
||||||
_ownedServer: BrowserServer | null = null;
|
_ownedServer: BrowserServer | null = null;
|
||||||
|
readonly _logger: Logger;
|
||||||
|
|
||||||
|
constructor(logger: Logger) {
|
||||||
|
super();
|
||||||
|
this._logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
abstract newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
|
abstract newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
|
||||||
abstract contexts(): BrowserContext[];
|
abstract contexts(): BrowserContext[];
|
||||||
|
|
@ -71,6 +76,14 @@ export abstract class BrowserBase extends EventEmitter implements Browser {
|
||||||
if (this.isConnected())
|
if (this.isConnected())
|
||||||
await new Promise(x => this.once(Events.Browser.Disconnected, x));
|
await new Promise(x => this.once(Events.Browser.Disconnected, x));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isLogEnabled(log: Log): boolean {
|
||||||
|
return this._logger._isLogEnabled(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log(log: Log, message: string | Error, ...args: any[]) {
|
||||||
|
return this._logger._log(log, message, ...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LaunchType = 'local' | 'server' | 'persistent';
|
export type LaunchType = 'local' | 'server' | 'persistent';
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import * as types from './types';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { ExtendedEventEmitter } from './extendedEventEmitter';
|
import { ExtendedEventEmitter } from './extendedEventEmitter';
|
||||||
import { Download } from './download';
|
import { Download } from './download';
|
||||||
|
import { BrowserBase } from './browser';
|
||||||
|
import { Log, Logger } from './logger';
|
||||||
|
|
||||||
export type BrowserContextOptions = {
|
export type BrowserContextOptions = {
|
||||||
viewport?: types.Size | null,
|
viewport?: types.Size | null,
|
||||||
|
|
@ -44,7 +46,7 @@ export type BrowserContextOptions = {
|
||||||
acceptDownloads?: boolean
|
acceptDownloads?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface BrowserContext {
|
export interface BrowserContext extends Logger {
|
||||||
setDefaultNavigationTimeout(timeout: number): void;
|
setDefaultNavigationTimeout(timeout: number): void;
|
||||||
setDefaultTimeout(timeout: number): void;
|
setDefaultTimeout(timeout: number): void;
|
||||||
pages(): Page[];
|
pages(): Page[];
|
||||||
|
|
@ -76,9 +78,11 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
|
||||||
private _closePromiseFulfill: ((error: Error) => void) | undefined;
|
private _closePromiseFulfill: ((error: Error) => void) | undefined;
|
||||||
readonly _permissions = new Map<string, string[]>();
|
readonly _permissions = new Map<string, string[]>();
|
||||||
readonly _downloads = new Set<Download>();
|
readonly _downloads = new Set<Download>();
|
||||||
|
readonly _browserBase: BrowserBase;
|
||||||
|
|
||||||
constructor(options: BrowserContextOptions) {
|
constructor(browserBase: BrowserBase, options: BrowserContextOptions) {
|
||||||
super();
|
super();
|
||||||
|
this._browserBase = browserBase;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||||
}
|
}
|
||||||
|
|
@ -149,6 +153,14 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
|
||||||
setDefaultTimeout(timeout: number) {
|
setDefaultTimeout(timeout: number) {
|
||||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isLogEnabled(log: Log): boolean {
|
||||||
|
return this._browserBase._isLogEnabled(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log(log: Log, message: string | Error, ...args: any[]) {
|
||||||
|
return this._browserBase._log(log, message, ...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertBrowserContextIsNotOwned(context: BrowserContextBase) {
|
export function assertBrowserContextIsNotOwned(context: BrowserContextBase) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
import { BrowserBase } from '../browser';
|
import { BrowserBase } from '../browser';
|
||||||
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
|
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
|
||||||
import { Events as CommonEvents } from '../events';
|
import { Events as CommonEvents } from '../events';
|
||||||
import { assert, debugError, helper } from '../helper';
|
import { assert, helper } from '../helper';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import { Page, PageBinding, Worker } from '../page';
|
import { Page, PageBinding, Worker } from '../page';
|
||||||
import { ConnectionTransport, SlowMoTransport } from '../transport';
|
import { ConnectionTransport, SlowMoTransport } from '../transport';
|
||||||
|
|
@ -29,6 +29,7 @@ import { readProtocolStream } from './crProtocolHelper';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { CRExecutionContext } from './crExecutionContext';
|
import { CRExecutionContext } from './crExecutionContext';
|
||||||
|
import { Logger, logError } from '../logger';
|
||||||
|
|
||||||
export class CRBrowser extends BrowserBase {
|
export class CRBrowser extends BrowserBase {
|
||||||
readonly _connection: CRConnection;
|
readonly _connection: CRConnection;
|
||||||
|
|
@ -46,9 +47,9 @@ export class CRBrowser extends BrowserBase {
|
||||||
private _tracingPath: string | null = '';
|
private _tracingPath: string | null = '';
|
||||||
private _tracingClient: CRSession | undefined;
|
private _tracingClient: CRSession | undefined;
|
||||||
|
|
||||||
static async connect(transport: ConnectionTransport, isPersistent: boolean, slowMo?: number): Promise<CRBrowser> {
|
static async connect(transport: ConnectionTransport, isPersistent: boolean, logger: Logger, slowMo?: number): Promise<CRBrowser> {
|
||||||
const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo));
|
const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo), logger);
|
||||||
const browser = new CRBrowser(connection, isPersistent);
|
const browser = new CRBrowser(connection, logger, isPersistent);
|
||||||
const session = connection.rootSession;
|
const session = connection.rootSession;
|
||||||
if (!isPersistent) {
|
if (!isPersistent) {
|
||||||
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
|
await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
|
||||||
|
|
@ -83,8 +84,8 @@ export class CRBrowser extends BrowserBase {
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(connection: CRConnection, isPersistent: boolean) {
|
constructor(connection: CRConnection, logger: Logger, isPersistent: boolean) {
|
||||||
super();
|
super(logger);
|
||||||
this._connection = connection;
|
this._connection = connection;
|
||||||
this._session = this._connection.rootSession;
|
this._session = this._connection.rootSession;
|
||||||
|
|
||||||
|
|
@ -128,8 +129,8 @@ export class CRBrowser extends BrowserBase {
|
||||||
if (targetInfo.type === 'other' || !context) {
|
if (targetInfo.type === 'other' || !context) {
|
||||||
if (waitingForDebugger) {
|
if (waitingForDebugger) {
|
||||||
// Ideally, detaching should resume any target, but there is a bug in the backend.
|
// Ideally, detaching should resume any target, but there is a bug in the backend.
|
||||||
session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => {
|
session.send('Runtime.runIfWaitingForDebugger').catch(logError(this)).then(() => {
|
||||||
this._session.send('Target.detachFromTarget', { sessionId }).catch(debugError);
|
this._session.send('Target.detachFromTarget', { sessionId }).catch(logError(this));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -266,7 +267,7 @@ class CRServiceWorker extends Worker {
|
||||||
readonly _browserContext: CRBrowserContext;
|
readonly _browserContext: CRBrowserContext;
|
||||||
|
|
||||||
constructor(browserContext: CRBrowserContext, session: CRSession, url: string) {
|
constructor(browserContext: CRBrowserContext, session: CRSession, url: string) {
|
||||||
super(url);
|
super(browserContext, url);
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
session.once('Runtime.executionContextCreated', event => {
|
session.once('Runtime.executionContextCreated', event => {
|
||||||
this._createExecutionContext(new CRExecutionContext(session, event.context));
|
this._createExecutionContext(new CRExecutionContext(session, event.context));
|
||||||
|
|
@ -283,7 +284,7 @@ export class CRBrowserContext extends BrowserContextBase {
|
||||||
readonly _evaluateOnNewDocumentSources: string[];
|
readonly _evaluateOnNewDocumentSources: string[];
|
||||||
|
|
||||||
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
|
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
|
||||||
super(options);
|
super(browser, options);
|
||||||
this._browser = browser;
|
this._browser = browser;
|
||||||
this._browserContextId = browserContextId;
|
this._browserContextId = browserContextId;
|
||||||
this._evaluateOnNewDocumentSources = [];
|
this._evaluateOnNewDocumentSources = [];
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from '../helper';
|
import { assert } from '../helper';
|
||||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport';
|
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
export const ConnectionEvents = {
|
export const ConnectionEvents = {
|
||||||
Disconnected: Symbol('ConnectionEvents.Disconnected')
|
Disconnected: Symbol('ConnectionEvents.Disconnected')
|
||||||
|
|
@ -34,16 +35,19 @@ export class CRConnection extends EventEmitter {
|
||||||
private readonly _sessions = new Map<string, CRSession>();
|
private readonly _sessions = new Map<string, CRSession>();
|
||||||
readonly rootSession: CRSession;
|
readonly rootSession: CRSession;
|
||||||
_closed = false;
|
_closed = false;
|
||||||
|
private _logger: Logger;
|
||||||
|
|
||||||
constructor(transport: ConnectionTransport) {
|
constructor(transport: ConnectionTransport, logger: Logger) {
|
||||||
super();
|
super();
|
||||||
this._transport = transport;
|
this._transport = transport;
|
||||||
|
this._logger = logger;
|
||||||
this._transport.onmessage = this._onMessage.bind(this);
|
this._transport.onmessage = this._onMessage.bind(this);
|
||||||
this._transport.onclose = this._onClose.bind(this);
|
this._transport.onclose = this._onClose.bind(this);
|
||||||
this.rootSession = new CRSession(this, '', 'browser', '');
|
this.rootSession = new CRSession(this, '', 'browser', '');
|
||||||
this._sessions.set('', this.rootSession);
|
this._sessions.set('', this.rootSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static fromSession(session: CRSession): CRConnection {
|
static fromSession(session: CRSession): CRConnection {
|
||||||
return session._connection!;
|
return session._connection!;
|
||||||
}
|
}
|
||||||
|
|
@ -57,15 +61,15 @@ export class CRConnection extends EventEmitter {
|
||||||
const message: ProtocolRequest = { id, method, params };
|
const message: ProtocolRequest = { id, method, params };
|
||||||
if (sessionId)
|
if (sessionId)
|
||||||
message.sessionId = sessionId;
|
message.sessionId = sessionId;
|
||||||
if (debugProtocol.enabled)
|
if (this._logger._isLogEnabled(protocolLog))
|
||||||
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
this._logger._log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||||
this._transport.send(message);
|
this._transport.send(message);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onMessage(message: ProtocolResponse) {
|
async _onMessage(message: ProtocolResponse) {
|
||||||
if (debugProtocol.enabled)
|
if (this._logger._isLogEnabled(protocolLog))
|
||||||
debugProtocol('◀ RECV ' + JSON.stringify(message));
|
this._logger._log(protocolLog, '◀ RECV ' + JSON.stringify(message));
|
||||||
if (message.id === kBrowserCloseMessageId)
|
if (message.id === kBrowserCloseMessageId)
|
||||||
return;
|
return;
|
||||||
if (message.method === 'Target.attachedToTarget') {
|
if (message.method === 'Target.attachedToTarget') {
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CRSession } from './crConnection';
|
import { CRSession } from './crConnection';
|
||||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
import { assert, helper, RegisteredListener } from '../helper';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
|
||||||
import { EVALUATION_SCRIPT_URL } from './crExecutionContext';
|
import { EVALUATION_SCRIPT_URL } from './crExecutionContext';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
import { logError, Logger } from '../logger';
|
||||||
|
|
||||||
type JSRange = {
|
type JSRange = {
|
||||||
startOffset: number,
|
startOffset: number,
|
||||||
|
|
@ -50,9 +51,9 @@ export class CRCoverage {
|
||||||
private _jsCoverage: JSCoverage;
|
private _jsCoverage: JSCoverage;
|
||||||
private _cssCoverage: CSSCoverage;
|
private _cssCoverage: CSSCoverage;
|
||||||
|
|
||||||
constructor(client: CRSession) {
|
constructor(client: CRSession, logger: Logger) {
|
||||||
this._jsCoverage = new JSCoverage(client);
|
this._jsCoverage = new JSCoverage(client, logger);
|
||||||
this._cssCoverage = new CSSCoverage(client);
|
this._cssCoverage = new CSSCoverage(client, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async startJSCoverage(options?: types.JSCoverageOptions) {
|
async startJSCoverage(options?: types.JSCoverageOptions) {
|
||||||
|
|
@ -80,9 +81,11 @@ class JSCoverage {
|
||||||
_eventListeners: RegisteredListener[];
|
_eventListeners: RegisteredListener[];
|
||||||
_resetOnNavigation: boolean;
|
_resetOnNavigation: boolean;
|
||||||
_reportAnonymousScripts = false;
|
_reportAnonymousScripts = false;
|
||||||
|
private _logger: Logger;
|
||||||
|
|
||||||
constructor(client: CRSession) {
|
constructor(client: CRSession, logger: Logger) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
|
this._logger = logger;
|
||||||
this._enabled = false;
|
this._enabled = false;
|
||||||
this._scriptIds = new Set();
|
this._scriptIds = new Set();
|
||||||
this._scriptSources = new Map();
|
this._scriptSources = new Map();
|
||||||
|
|
@ -134,7 +137,7 @@ class JSCoverage {
|
||||||
this._scriptSources.set(event.scriptId, response.scriptSource);
|
this._scriptSources.set(event.scriptId, response.scriptSource);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// This might happen if the page has already navigated away.
|
// This might happen if the page has already navigated away.
|
||||||
debugError(e);
|
logError(this._logger)(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,9 +175,11 @@ class CSSCoverage {
|
||||||
_stylesheetSources: Map<string, string>;
|
_stylesheetSources: Map<string, string>;
|
||||||
_eventListeners: RegisteredListener[];
|
_eventListeners: RegisteredListener[];
|
||||||
_resetOnNavigation: boolean;
|
_resetOnNavigation: boolean;
|
||||||
|
private _logger: Logger;
|
||||||
|
|
||||||
constructor(client: CRSession) {
|
constructor(client: CRSession, logger: Logger) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
|
this._logger = logger;
|
||||||
this._enabled = false;
|
this._enabled = false;
|
||||||
this._stylesheetURLs = new Map();
|
this._stylesheetURLs = new Map();
|
||||||
this._stylesheetSources = new Map();
|
this._stylesheetSources = new Map();
|
||||||
|
|
@ -218,7 +223,7 @@ class CSSCoverage {
|
||||||
this._stylesheetSources.set(header.styleSheetId, response.text);
|
this._stylesheetSources.set(header.styleSheetId, response.text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// This might happen if the page has already navigated away.
|
// This might happen if the page has already navigated away.
|
||||||
debugError(e);
|
logError(this._logger)(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,13 @@
|
||||||
|
|
||||||
import { CRSession } from './crConnection';
|
import { CRSession } from './crConnection';
|
||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
import { assert, helper, RegisteredListener } from '../helper';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { Credentials } from '../types';
|
import { Credentials } from '../types';
|
||||||
import { CRPage } from './crPage';
|
import { CRPage } from './crPage';
|
||||||
|
import { logError } from '../logger';
|
||||||
|
|
||||||
export class CRNetworkManager {
|
export class CRNetworkManager {
|
||||||
private _client: CRSession;
|
private _client: CRSession;
|
||||||
|
|
@ -130,14 +131,14 @@ export class CRNetworkManager {
|
||||||
this._client.send('Fetch.continueWithAuth', {
|
this._client.send('Fetch.continueWithAuth', {
|
||||||
requestId: event.requestId,
|
requestId: event.requestId,
|
||||||
authChallengeResponse: { response, username, password },
|
authChallengeResponse: { response, username, password },
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestPaused(workerFrame: frames.Frame | undefined, event: Protocol.Fetch.requestPausedPayload) {
|
_onRequestPaused(workerFrame: frames.Frame | undefined, event: Protocol.Fetch.requestPausedPayload) {
|
||||||
if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
|
if (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled) {
|
||||||
this._client.send('Fetch.continueRequest', {
|
this._client.send('Fetch.continueRequest', {
|
||||||
requestId: event.requestId
|
requestId: event.requestId
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
}
|
}
|
||||||
if (!event.networkId || event.request.url.startsWith('data:'))
|
if (!event.networkId || event.request.url.startsWith('data:'))
|
||||||
return;
|
return;
|
||||||
|
|
@ -176,7 +177,7 @@ export class CRNetworkManager {
|
||||||
|
|
||||||
if (!frame) {
|
if (!frame) {
|
||||||
if (requestPausedEvent)
|
if (requestPausedEvent)
|
||||||
this._client.send('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }).catch(debugError);
|
this._client.send('Fetch.continueRequest', { requestId: requestPausedEvent.requestId }).catch(logError(this._page));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
|
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
|
||||||
|
|
@ -299,7 +300,7 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
// In certain cases, protocol will return error if the request was already canceled
|
// In certain cases, protocol will return error if the request was already canceled
|
||||||
// or the page was closed. We should tolerate these errors.
|
// or the page was closed. We should tolerate these errors.
|
||||||
debugError(error);
|
logError(this.request._page)(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,7 +326,7 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
// In certain cases, protocol will return error if the request was already canceled
|
// In certain cases, protocol will return error if the request was already canceled
|
||||||
// or the page was closed. We should tolerate these errors.
|
// or the page was closed. We should tolerate these errors.
|
||||||
debugError(error);
|
logError(this.request._page)(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,7 +339,7 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
// In certain cases, protocol will return error if the request was already canceled
|
// In certain cases, protocol will return error if the request was already canceled
|
||||||
// or the page was closed. We should tolerate these errors.
|
// or the page was closed. We should tolerate these errors.
|
||||||
debugError(error);
|
logError(this.request._page)(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { debugError, helper, RegisteredListener, assert } from '../helper';
|
import { helper, RegisteredListener, assert } from '../helper';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import { CRSession, CRConnection, CRSessionEvents } from './crConnection';
|
import { CRSession, CRConnection, CRSessionEvents } from './crConnection';
|
||||||
import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext';
|
import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext';
|
||||||
|
|
@ -37,6 +37,7 @@ import { CRBrowserContext } from './crBrowser';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { ConsoleMessage } from '../console';
|
import { ConsoleMessage } from '../console';
|
||||||
import { NotConnectedError } from '../errors';
|
import { NotConnectedError } from '../errors';
|
||||||
|
import { logError } from '../logger';
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
|
||||||
|
|
@ -60,7 +61,7 @@ export class CRPage implements PageDelegate {
|
||||||
this.rawKeyboard = new RawKeyboardImpl(client);
|
this.rawKeyboard = new RawKeyboardImpl(client);
|
||||||
this.rawMouse = new RawMouseImpl(client);
|
this.rawMouse = new RawMouseImpl(client);
|
||||||
this._pdf = new CRPDF(client);
|
this._pdf = new CRPDF(client);
|
||||||
this._coverage = new CRCoverage(client);
|
this._coverage = new CRCoverage(client, browserContext);
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
this._page = new Page(this, browserContext);
|
this._page = new Page(this, browserContext);
|
||||||
this._mainFrameSession = new FrameSession(this, client, targetId);
|
this._mainFrameSession = new FrameSession(this, client, targetId);
|
||||||
|
|
@ -127,7 +128,7 @@ export class CRPage implements PageDelegate {
|
||||||
|
|
||||||
async exposeBinding(binding: PageBinding) {
|
async exposeBinding(binding: PageBinding) {
|
||||||
await this._forAllFrameSessions(frame => frame._initBinding(binding));
|
await this._forAllFrameSessions(frame => frame._initBinding(binding));
|
||||||
await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(debugError)));
|
await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(logError(this._page))));
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExtraHTTPHeaders(): Promise<void> {
|
async updateExtraHTTPHeaders(): Promise<void> {
|
||||||
|
|
@ -381,9 +382,9 @@ class FrameSession {
|
||||||
frameId: frame._id,
|
frameId: frame._id,
|
||||||
grantUniveralAccess: true,
|
grantUniveralAccess: true,
|
||||||
worldName: UTILITY_WORLD_NAME,
|
worldName: UTILITY_WORLD_NAME,
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
for (const binding of this._crPage._browserContext._pageBindings.values())
|
for (const binding of this._crPage._browserContext._pageBindings.values())
|
||||||
frame.evaluate(binding.source).catch(debugError);
|
frame.evaluate(binding.source).catch(logError(this._page));
|
||||||
}
|
}
|
||||||
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
|
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
|
||||||
if (isInitialEmptyPage) {
|
if (isInitialEmptyPage) {
|
||||||
|
|
@ -543,14 +544,14 @@ class FrameSession {
|
||||||
|
|
||||||
if (event.targetInfo.type !== 'worker') {
|
if (event.targetInfo.type !== 'worker') {
|
||||||
// Ideally, detaching should resume any target, but there is a bug in the backend.
|
// Ideally, detaching should resume any target, but there is a bug in the backend.
|
||||||
session.send('Runtime.runIfWaitingForDebugger').catch(debugError).then(() => {
|
session.send('Runtime.runIfWaitingForDebugger').catch(logError(this._page)).then(() => {
|
||||||
this._client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(debugError);
|
this._client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(logError(this._page));
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = event.targetInfo.url;
|
const url = event.targetInfo.url;
|
||||||
const worker = new Worker(url);
|
const worker = new Worker(this._page, url);
|
||||||
this._page._addWorker(event.sessionId, worker);
|
this._page._addWorker(event.sessionId, worker);
|
||||||
session.once('Runtime.executionContextCreated', async event => {
|
session.once('Runtime.executionContextCreated', async event => {
|
||||||
worker._createExecutionContext(new CRExecutionContext(session, event.context));
|
worker._createExecutionContext(new CRExecutionContext(session, event.context));
|
||||||
|
|
@ -559,7 +560,7 @@ class FrameSession {
|
||||||
session.send('Runtime.enable'),
|
session.send('Runtime.enable'),
|
||||||
session.send('Network.enable'),
|
session.send('Network.enable'),
|
||||||
session.send('Runtime.runIfWaitingForDebugger'),
|
session.send('Runtime.runIfWaitingForDebugger'),
|
||||||
]).catch(debugError); // This might fail if the target is closed before we initialize.
|
]).catch(logError(this._page)); // This might fail if the target is closed before we initialize.
|
||||||
session.on('Runtime.consoleAPICalled', event => {
|
session.on('Runtime.consoleAPICalled', event => {
|
||||||
const args = event.args.map(o => worker._existingExecutionContext!._createHandle(o));
|
const args = event.args.map(o => worker._existingExecutionContext!._createHandle(o));
|
||||||
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
|
this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace));
|
||||||
|
|
@ -750,7 +751,7 @@ class FrameSession {
|
||||||
async _getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
|
async _getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
|
||||||
const result = await this._client.send('DOM.getBoxModel', {
|
const result = await this._client.send('DOM.getBoxModel', {
|
||||||
objectId: toRemoteObject(handle).objectId
|
objectId: toRemoteObject(handle).objectId
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
if (!result)
|
if (!result)
|
||||||
return null;
|
return null;
|
||||||
const quad = result.model.border;
|
const quad = result.model.border;
|
||||||
|
|
@ -777,7 +778,7 @@ class FrameSession {
|
||||||
async _getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
|
async _getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
|
||||||
const result = await this._client.send('DOM.getContentQuads', {
|
const result = await this._client.send('DOM.getContentQuads', {
|
||||||
objectId: toRemoteObject(handle).objectId
|
objectId: toRemoteObject(handle).objectId
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
if (!result)
|
if (!result)
|
||||||
return null;
|
return null;
|
||||||
return result.quads.map(quad => [
|
return result.quads.map(quad => [
|
||||||
|
|
@ -799,7 +800,7 @@ class FrameSession {
|
||||||
const result = await this._client.send('DOM.resolveNode', {
|
const result = await this._client.send('DOM.resolveNode', {
|
||||||
backendNodeId,
|
backendNodeId,
|
||||||
executionContextId: (to._delegate as CRExecutionContext)._contextId,
|
executionContextId: (to._delegate as CRExecutionContext)._contextId,
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
if (!result || result.object.subtype === 'null')
|
if (!result || result.object.subtype === 'null')
|
||||||
throw new Error('Unable to adopt element handle from a different document');
|
throw new Error('Unable to adopt element handle from a different document');
|
||||||
return to._createHandle(result.object).asElement()!;
|
return to._createHandle(result.object).asElement()!;
|
||||||
|
|
|
||||||
28
src/dom.ts
28
src/dom.ts
|
|
@ -19,7 +19,7 @@ import * as mime from 'mime';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import * as frames from './frames';
|
import * as frames from './frames';
|
||||||
import { assert, debugError, helper, debugInput } from './helper';
|
import { assert, helper } from './helper';
|
||||||
import { Injected, InjectedResult } from './injected/injected';
|
import { Injected, InjectedResult } from './injected/injected';
|
||||||
import * as input from './input';
|
import * as input from './input';
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
|
|
@ -27,6 +27,7 @@ import { Page } from './page';
|
||||||
import { selectors } from './selectors';
|
import { selectors } from './selectors';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { NotConnectedError, TimeoutError } from './errors';
|
import { NotConnectedError, TimeoutError } from './errors';
|
||||||
|
import { Log, logError } from './logger';
|
||||||
|
|
||||||
export type PointerActionOptions = {
|
export type PointerActionOptions = {
|
||||||
modifiers?: input.Modifier[];
|
modifiers?: input.Modifier[];
|
||||||
|
|
@ -37,12 +38,17 @@ export type ClickOptions = PointerActionOptions & input.MouseClickOptions;
|
||||||
|
|
||||||
export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions;
|
export type MultiClickOptions = PointerActionOptions & input.MouseMultiClickOptions;
|
||||||
|
|
||||||
|
export const inputLog: Log = {
|
||||||
|
name: 'input',
|
||||||
|
color: 'cyan'
|
||||||
|
};
|
||||||
|
|
||||||
export class FrameExecutionContext extends js.ExecutionContext {
|
export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
readonly frame: frames.Frame;
|
readonly frame: frames.Frame;
|
||||||
private _injectedPromise?: Promise<js.JSHandle>;
|
private _injectedPromise?: Promise<js.JSHandle>;
|
||||||
|
|
||||||
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame) {
|
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame) {
|
||||||
super(delegate);
|
super(delegate, frame._page);
|
||||||
this.frame = frame;
|
this.frame = frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,9 +150,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<void> {
|
async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<void> {
|
||||||
debugInput('scrolling into veiw if needed...');
|
this._page._log(inputLog, 'scrolling into view if needed...');
|
||||||
await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect);
|
await this._page._delegate.scrollRectIntoViewIfNeeded(this, rect);
|
||||||
debugInput('...done');
|
this._page._log(inputLog, '...done');
|
||||||
}
|
}
|
||||||
|
|
||||||
async scrollIntoViewIfNeeded() {
|
async scrollIntoViewIfNeeded() {
|
||||||
|
|
@ -195,7 +201,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
private async _offsetPoint(offset: types.Point): Promise<types.Point> {
|
private async _offsetPoint(offset: types.Point): Promise<types.Point> {
|
||||||
const [box, border] = await Promise.all([
|
const [box, border] = await Promise.all([
|
||||||
this.boundingBox(),
|
this.boundingBox(),
|
||||||
this._evaluateInUtility(({ injected, node }) => injected.getElementBorderWidth(node), {}).catch(debugError),
|
this._evaluateInUtility(({ injected, node }) => injected.getElementBorderWidth(node), {}).catch(logError(this._context._logger)),
|
||||||
]);
|
]);
|
||||||
const point = { x: offset.x, y: offset.y };
|
const point = { x: offset.x, y: offset.y };
|
||||||
if (box) {
|
if (box) {
|
||||||
|
|
@ -227,9 +233,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
let restoreModifiers: input.Modifier[] | undefined;
|
let restoreModifiers: input.Modifier[] | undefined;
|
||||||
if (options && options.modifiers)
|
if (options && options.modifiers)
|
||||||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
||||||
debugInput('performing input action...');
|
this._page._log(inputLog, 'performing input action...');
|
||||||
await action(point);
|
await action(point);
|
||||||
debugInput('...done');
|
this._page._log(inputLog, '...done');
|
||||||
if (restoreModifiers)
|
if (restoreModifiers)
|
||||||
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
||||||
}, deadline, options, true);
|
}, deadline, options, true);
|
||||||
|
|
@ -404,18 +410,18 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _waitForDisplayedAtStablePosition(deadline: number): Promise<void> {
|
async _waitForDisplayedAtStablePosition(deadline: number): Promise<void> {
|
||||||
debugInput('waiting for element to be displayed and not moving...');
|
this._page._log(inputLog, 'waiting for element to be displayed and not moving...');
|
||||||
const stablePromise = this._evaluateInUtility(({ injected, node }, timeout) => {
|
const stablePromise = this._evaluateInUtility(({ injected, node }, timeout) => {
|
||||||
return injected.waitForDisplayedAtStablePosition(node, timeout);
|
return injected.waitForDisplayedAtStablePosition(node, timeout);
|
||||||
}, helper.timeUntilDeadline(deadline));
|
}, helper.timeUntilDeadline(deadline));
|
||||||
const timeoutMessage = 'element to be displayed and not moving';
|
const timeoutMessage = 'element to be displayed and not moving';
|
||||||
const injectedResult = await helper.waitWithDeadline(stablePromise, timeoutMessage, deadline);
|
const injectedResult = await helper.waitWithDeadline(stablePromise, timeoutMessage, deadline);
|
||||||
handleInjectedResult(injectedResult, timeoutMessage);
|
handleInjectedResult(injectedResult, timeoutMessage);
|
||||||
debugInput('...done');
|
this._page._log(inputLog, '...done');
|
||||||
}
|
}
|
||||||
|
|
||||||
async _waitForHitTargetAt(point: types.Point, deadline: number): Promise<void> {
|
async _waitForHitTargetAt(point: types.Point, deadline: number): Promise<void> {
|
||||||
debugInput(`waiting for element to receive pointer events at (${point.x},${point.y}) ...`);
|
this._page._log(inputLog, `waiting for element to receive pointer events at (${point.x},${point.y}) ...`);
|
||||||
const frame = await this.ownerFrame();
|
const frame = await this.ownerFrame();
|
||||||
if (frame && frame.parentFrame()) {
|
if (frame && frame.parentFrame()) {
|
||||||
const element = await frame.frameElement();
|
const element = await frame.frameElement();
|
||||||
|
|
@ -431,7 +437,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
const timeoutMessage = 'element to receive pointer events';
|
const timeoutMessage = 'element to receive pointer events';
|
||||||
const injectedResult = await helper.waitWithDeadline(hitTargetPromise, timeoutMessage, deadline);
|
const injectedResult = await helper.waitWithDeadline(hitTargetPromise, timeoutMessage, deadline);
|
||||||
handleInjectedResult(injectedResult, timeoutMessage);
|
handleInjectedResult(injectedResult, timeoutMessage);
|
||||||
debugInput('...done');
|
this._page._log(inputLog, '...done');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { ConnectionEvents, FFConnection } from './ffConnection';
|
||||||
import { headersArray } from './ffNetworkManager';
|
import { headersArray } from './ffNetworkManager';
|
||||||
import { FFPage } from './ffPage';
|
import { FFPage } from './ffPage';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
export class FFBrowser extends BrowserBase {
|
export class FFBrowser extends BrowserBase {
|
||||||
_connection: FFConnection;
|
_connection: FFConnection;
|
||||||
|
|
@ -37,15 +38,15 @@ export class FFBrowser extends BrowserBase {
|
||||||
readonly _firstPagePromise: Promise<void>;
|
readonly _firstPagePromise: Promise<void>;
|
||||||
private _firstPageCallback = () => {};
|
private _firstPageCallback = () => {};
|
||||||
|
|
||||||
static async connect(transport: ConnectionTransport, attachToDefaultContext: boolean, slowMo?: number): Promise<FFBrowser> {
|
static async connect(transport: ConnectionTransport, logger: Logger, attachToDefaultContext: boolean, slowMo?: number): Promise<FFBrowser> {
|
||||||
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo));
|
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo), logger);
|
||||||
const browser = new FFBrowser(connection, attachToDefaultContext);
|
const browser = new FFBrowser(connection, logger, attachToDefaultContext);
|
||||||
await connection.send('Browser.enable', { attachToDefaultContext });
|
await connection.send('Browser.enable', { attachToDefaultContext });
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(connection: FFConnection, isPersistent: boolean) {
|
constructor(connection: FFConnection, logger: Logger, isPersistent: boolean) {
|
||||||
super();
|
super(logger);
|
||||||
this._connection = connection;
|
this._connection = connection;
|
||||||
this._ffPages = new Map();
|
this._ffPages = new Map();
|
||||||
|
|
||||||
|
|
@ -172,7 +173,7 @@ export class FFBrowserContext extends BrowserContextBase {
|
||||||
private readonly _evaluateOnNewDocumentSources: string[];
|
private readonly _evaluateOnNewDocumentSources: string[];
|
||||||
|
|
||||||
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
|
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
|
||||||
super(options);
|
super(browser, options);
|
||||||
this._browser = browser;
|
this._browser = browser;
|
||||||
this._browserContextId = browserContextId;
|
this._browserContextId = browserContextId;
|
||||||
this._evaluateOnNewDocumentSources = [];
|
this._evaluateOnNewDocumentSources = [];
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { assert } from '../helper';
|
import { assert } from '../helper';
|
||||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport';
|
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
export const ConnectionEvents = {
|
export const ConnectionEvents = {
|
||||||
Disconnected: Symbol('Disconnected'),
|
Disconnected: Symbol('Disconnected'),
|
||||||
|
|
@ -32,6 +33,7 @@ export class FFConnection extends EventEmitter {
|
||||||
private _lastId: number;
|
private _lastId: number;
|
||||||
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
|
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
|
||||||
private _transport: ConnectionTransport;
|
private _transport: ConnectionTransport;
|
||||||
|
private _logger: Logger;
|
||||||
readonly _sessions: Map<string, FFSession>;
|
readonly _sessions: Map<string, FFSession>;
|
||||||
_closed: boolean;
|
_closed: boolean;
|
||||||
|
|
||||||
|
|
@ -41,9 +43,10 @@ export class FFConnection extends EventEmitter {
|
||||||
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||||
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||||
|
|
||||||
constructor(transport: ConnectionTransport) {
|
constructor(transport: ConnectionTransport, logger: Logger) {
|
||||||
super();
|
super();
|
||||||
this._transport = transport;
|
this._transport = transport;
|
||||||
|
this._logger = logger;
|
||||||
this._lastId = 0;
|
this._lastId = 0;
|
||||||
this._callbacks = new Map();
|
this._callbacks = new Map();
|
||||||
|
|
||||||
|
|
@ -75,14 +78,14 @@ export class FFConnection extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
_rawSend(message: ProtocolRequest) {
|
_rawSend(message: ProtocolRequest) {
|
||||||
if (debugProtocol.enabled)
|
if (this._logger._isLogEnabled(protocolLog))
|
||||||
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
this._logger._log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||||
this._transport.send(message);
|
this._transport.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onMessage(message: ProtocolResponse) {
|
async _onMessage(message: ProtocolResponse) {
|
||||||
if (debugProtocol.enabled)
|
if (this._logger._isLogEnabled(protocolLog))
|
||||||
debugProtocol('◀ RECV ' + JSON.stringify(message));
|
this._logger._log(protocolLog, '◀ RECV ' + JSON.stringify(message));
|
||||||
if (message.id === kBrowserCloseMessageId)
|
if (message.id === kBrowserCloseMessageId)
|
||||||
return;
|
return;
|
||||||
if (message.sessionId) {
|
if (message.sessionId) {
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debugError, helper, RegisteredListener } from '../helper';
|
import { helper, RegisteredListener } from '../helper';
|
||||||
import { FFSession } from './ffConnection';
|
import { FFSession } from './ffConnection';
|
||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import { logError } from '../logger';
|
||||||
|
|
||||||
export class FFNetworkManager {
|
export class FFNetworkManager {
|
||||||
private _session: FFSession;
|
private _session: FFSession;
|
||||||
|
|
@ -164,9 +165,7 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
method,
|
method,
|
||||||
headers: headers ? headersArray(headers) : undefined,
|
headers: headers ? headersArray(headers) : undefined,
|
||||||
postData: postData ? Buffer.from(postData).toString('base64') : undefined
|
postData: postData ? Buffer.from(postData).toString('base64') : undefined
|
||||||
}).catch(error => {
|
}).catch(logError(this.request._page));
|
||||||
debugError(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(response: network.FulfillResponse) {
|
async fulfill(response: network.FulfillResponse) {
|
||||||
|
|
@ -188,18 +187,14 @@ class InterceptableRequest implements network.RouteDelegate {
|
||||||
statusText: network.STATUS_TEXTS[String(response.status || 200)] || '',
|
statusText: network.STATUS_TEXTS[String(response.status || 200)] || '',
|
||||||
headers: headersArray(responseHeaders),
|
headers: headersArray(responseHeaders),
|
||||||
base64body: responseBody ? responseBody.toString('base64') : undefined,
|
base64body: responseBody ? responseBody.toString('base64') : undefined,
|
||||||
}).catch(error => {
|
}).catch(logError(this.request._page));
|
||||||
debugError(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async abort(errorCode: string) {
|
async abort(errorCode: string) {
|
||||||
await this._session.send('Network.abortInterceptedRequest', {
|
await this._session.send('Network.abortInterceptedRequest', {
|
||||||
requestId: this._id,
|
requestId: this._id,
|
||||||
errorCode,
|
errorCode,
|
||||||
}).catch(error => {
|
}).catch(logError(this.request._page));
|
||||||
debugError(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import * as dialog from '../dialog';
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
import { assert, helper, RegisteredListener } from '../helper';
|
||||||
import { Page, PageBinding, PageDelegate, Worker } from '../page';
|
import { Page, PageBinding, PageDelegate, Worker } from '../page';
|
||||||
import { kScreenshotDuringNavigationError } from '../screenshotter';
|
import { kScreenshotDuringNavigationError } from '../screenshotter';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
|
@ -32,6 +32,7 @@ import { FFNetworkManager, headersArray } from './ffNetworkManager';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { selectors } from '../selectors';
|
import { selectors } from '../selectors';
|
||||||
import { NotConnectedError } from '../errors';
|
import { NotConnectedError } from '../errors';
|
||||||
|
import { logError } from '../logger';
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
|
||||||
|
|
@ -199,7 +200,7 @@ export class FFPage implements PageDelegate {
|
||||||
params.type,
|
params.type,
|
||||||
params.message,
|
params.message,
|
||||||
async (accept: boolean, promptText?: string) => {
|
async (accept: boolean, promptText?: string) => {
|
||||||
await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(debugError);
|
await this._session.send('Page.handleDialog', { dialogId: params.dialogId, accept, promptText }).catch(logError(this._page));
|
||||||
},
|
},
|
||||||
params.defaultValue));
|
params.defaultValue));
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +219,7 @@ export class FFPage implements PageDelegate {
|
||||||
|
|
||||||
async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) {
|
async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) {
|
||||||
const workerId = event.workerId;
|
const workerId = event.workerId;
|
||||||
const worker = new Worker(event.url);
|
const worker = new Worker(this._page, event.url);
|
||||||
const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => {
|
const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => {
|
||||||
this._session.send('Page.sendMessageToWorker', {
|
this._session.send('Page.sendMessageToWorker', {
|
||||||
frameId: event.frameId,
|
frameId: event.frameId,
|
||||||
|
|
@ -434,7 +435,7 @@ export class FFPage implements PageDelegate {
|
||||||
const result = await this._session.send('Page.getContentQuads', {
|
const result = await this._session.send('Page.getContentQuads', {
|
||||||
frameId: handle._context.frame._id,
|
frameId: handle._context.frame._id,
|
||||||
objectId: toRemoteObject(handle).objectId!,
|
objectId: toRemoteObject(handle).objectId!,
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
if (!result)
|
if (!result)
|
||||||
return null;
|
return null;
|
||||||
return result.quads.map(quad => [ quad.p1, quad.p2, quad.p3, quad.p4 ]);
|
return result.quads.map(quad => [ quad.p1, quad.p2, quad.p3, quad.p4 ]);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { ConsoleMessage } from './console';
|
||||||
import * as dom from './dom';
|
import * as dom from './dom';
|
||||||
import { TimeoutError, NotConnectedError } from './errors';
|
import { TimeoutError, NotConnectedError } from './errors';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { assert, helper, RegisteredListener, debugInput } from './helper';
|
import { assert, helper, RegisteredListener } from './helper';
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
|
|
@ -710,7 +710,7 @@ export class Frame {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof NotConnectedError))
|
if (!(e instanceof NotConnectedError))
|
||||||
throw e;
|
throw e;
|
||||||
debugInput('Element was detached from the DOM, retrying');
|
this._page._log(dom.inputLog, 'Element was detached from the DOM, retrying');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded`);
|
throw new TimeoutError(`waiting for selector "${selector}" failed: timeout exceeded`);
|
||||||
|
|
@ -775,7 +775,7 @@ export class Frame {
|
||||||
if (helper.isString(selectorOrFunctionOrTimeout))
|
if (helper.isString(selectorOrFunctionOrTimeout))
|
||||||
return this.waitForSelector(selectorOrFunctionOrTimeout, options) as any;
|
return this.waitForSelector(selectorOrFunctionOrTimeout, options) as any;
|
||||||
if (helper.isNumber(selectorOrFunctionOrTimeout)) {
|
if (helper.isNumber(selectorOrFunctionOrTimeout)) {
|
||||||
waitForTimeWasUsed();
|
waitForTimeWasUsed(this._page);
|
||||||
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
|
return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout));
|
||||||
}
|
}
|
||||||
if (typeof selectorOrFunctionOrTimeout === 'function')
|
if (typeof selectorOrFunctionOrTimeout === 'function')
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import * as debug from 'debug';
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { TimeoutError } from './errors';
|
import { TimeoutError } from './errors';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
|
|
||||||
export const debugError = debug(`pw:error`);
|
|
||||||
export const debugInput = debug('pw:input');
|
|
||||||
|
|
||||||
export type RegisteredListener = {
|
export type RegisteredListener = {
|
||||||
emitter: EventEmitter;
|
emitter: EventEmitter;
|
||||||
eventName: (string | symbol);
|
eventName: (string | symbol);
|
||||||
|
|
@ -66,37 +62,16 @@ class Helper {
|
||||||
}
|
}
|
||||||
|
|
||||||
static installApiHooks(className: string, classType: any) {
|
static installApiHooks(className: string, classType: any) {
|
||||||
const log = debug('pw:api');
|
|
||||||
for (const methodName of Reflect.ownKeys(classType.prototype)) {
|
for (const methodName of Reflect.ownKeys(classType.prototype)) {
|
||||||
const method = Reflect.get(classType.prototype, methodName);
|
const method = Reflect.get(classType.prototype, methodName);
|
||||||
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
|
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
|
||||||
continue;
|
continue;
|
||||||
const isAsync = method.constructor.name === 'AsyncFunction';
|
const isAsync = method.constructor.name === 'AsyncFunction';
|
||||||
if (!isAsync && !log.enabled)
|
if (!isAsync)
|
||||||
continue;
|
continue;
|
||||||
Reflect.set(classType.prototype, methodName, function(this: any, ...args: any[]) {
|
Reflect.set(classType.prototype, methodName, function(this: any, ...args: any[]) {
|
||||||
const syncStack: any = {};
|
const syncStack: any = {};
|
||||||
Error.captureStackTrace(syncStack);
|
Error.captureStackTrace(syncStack);
|
||||||
if (log.enabled) {
|
|
||||||
const frames = syncStack.stack.substring('Error\n'.length)
|
|
||||||
.split('\n')
|
|
||||||
.map((f: string) => f.replace(/\s+at\s/, '').trim());
|
|
||||||
const userCall = frames.length <= 1 || !frames[1].includes('playwright/lib');
|
|
||||||
if (userCall) {
|
|
||||||
const match = /([^/\\]+)(:\d+:\d+)[)]?$/.exec(frames[1]);
|
|
||||||
let location = '';
|
|
||||||
if (match) {
|
|
||||||
const fileName = helper.trimMiddle(match[1], 20 - match[2].length);
|
|
||||||
location = `\u001b[33m[${fileName}${match[2]}]\u001b[39m `;
|
|
||||||
}
|
|
||||||
if (args.length)
|
|
||||||
log(`${location}${className}.${methodName} %o`, args);
|
|
||||||
else
|
|
||||||
log(`${location}${className}.${methodName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isAsync)
|
|
||||||
return method.call(this, ...args);
|
|
||||||
return method.call(this, ...args).catch((e: any) => {
|
return method.call(this, ...args).catch((e: any) => {
|
||||||
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
|
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
|
||||||
const clientStack = stack.substring(stack.indexOf('\n'));
|
const clientStack = stack.substring(stack.indexOf('\n'));
|
||||||
|
|
|
||||||
13
src/hints.ts
13
src/hints.ts
|
|
@ -14,17 +14,20 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as debug from 'debug';
|
import { Page } from './page';
|
||||||
|
import { Log } from './logger';
|
||||||
|
|
||||||
export const debugHints = debug('pw:hints');
|
const hintsLog: Log = {
|
||||||
(debugHints as any).color = '11';
|
name: 'hint',
|
||||||
|
severity: 'warning'
|
||||||
|
};
|
||||||
|
|
||||||
let waitForTimeWasUsedReported = false;
|
let waitForTimeWasUsedReported = false;
|
||||||
export function waitForTimeWasUsed() {
|
export function waitForTimeWasUsed(page: Page) {
|
||||||
if (waitForTimeWasUsedReported)
|
if (waitForTimeWasUsedReported)
|
||||||
return;
|
return;
|
||||||
waitForTimeWasUsedReported = true;
|
waitForTimeWasUsedReported = true;
|
||||||
debugHints(`WARNING: page.waitFor(timeout) should only be used for debugging.
|
page._log(hintsLog, `WARNING: page.waitFor(timeout) should only be used for debugging.
|
||||||
It is likely that the tests using timer in production are going to be flaky.
|
It is likely that the tests using timer in production are going to be flaky.
|
||||||
Use signals such as network events, selectors becoming visible, etc. instead.`);
|
Use signals such as network events, selectors becoming visible, etc. instead.`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import * as dom from './dom';
|
import * as dom from './dom';
|
||||||
import { helper } from './helper';
|
import { helper } from './helper';
|
||||||
|
import { Logger } from './logger';
|
||||||
|
|
||||||
export interface ExecutionContextDelegate {
|
export interface ExecutionContextDelegate {
|
||||||
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
||||||
|
|
@ -28,9 +29,11 @@ export interface ExecutionContextDelegate {
|
||||||
|
|
||||||
export class ExecutionContext {
|
export class ExecutionContext {
|
||||||
readonly _delegate: ExecutionContextDelegate;
|
readonly _delegate: ExecutionContextDelegate;
|
||||||
|
readonly _logger: Logger;
|
||||||
|
|
||||||
constructor(delegate: ExecutionContextDelegate) {
|
constructor(delegate: ExecutionContextDelegate, logger: Logger) {
|
||||||
this._delegate = delegate;
|
this._delegate = delegate;
|
||||||
|
this._logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
_doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise<any> {
|
_doEvaluateInternal(returnByValue: boolean, waitForNavigations: boolean, pageFunction: string | Function, ...args: any[]): Promise<any> {
|
||||||
|
|
|
||||||
109
src/logger.ts
Normal file
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 util from 'util';
|
||||||
import * as frames from './frames';
|
import * as frames from './frames';
|
||||||
import { assert, helper } from './helper';
|
import { assert, helper } from './helper';
|
||||||
|
import { Page } from './page';
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -110,12 +111,14 @@ export class Request {
|
||||||
private _frame: frames.Frame;
|
private _frame: frames.Frame;
|
||||||
private _waitForResponsePromise: Promise<Response | null>;
|
private _waitForResponsePromise: Promise<Response | null>;
|
||||||
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
|
private _waitForResponsePromiseCallback: (value: Response | null) => void = () => {};
|
||||||
|
readonly _page: Page;
|
||||||
|
|
||||||
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
|
constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
|
||||||
url: string, resourceType: string, method: string, postData: string | null, headers: Headers) {
|
url: string, resourceType: string, method: string, postData: string | null, headers: Headers) {
|
||||||
assert(!url.startsWith('data:'), 'Data urls should not fire requests');
|
assert(!url.startsWith('data:'), 'Data urls should not fire requests');
|
||||||
this._routeDelegate = routeDelegate;
|
this._routeDelegate = routeDelegate;
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
|
this._page = frame._page;
|
||||||
this._redirectedFrom = redirectedFrom;
|
this._redirectedFrom = redirectedFrom;
|
||||||
if (redirectedFrom)
|
if (redirectedFrom)
|
||||||
redirectedFrom._redirectedTo = this;
|
redirectedFrom._redirectedTo = this;
|
||||||
|
|
|
||||||
21
src/page.ts
21
src/page.ts
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
import * as dom from './dom';
|
import * as dom from './dom';
|
||||||
import * as frames from './frames';
|
import * as frames from './frames';
|
||||||
import { assert, debugError, helper, Listener } from './helper';
|
import { assert, helper, Listener } from './helper';
|
||||||
import * as input from './input';
|
import * as input from './input';
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
|
|
@ -31,6 +31,7 @@ import * as accessibility from './accessibility';
|
||||||
import { ExtendedEventEmitter } from './extendedEventEmitter';
|
import { ExtendedEventEmitter } from './extendedEventEmitter';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { FileChooser } from './fileChooser';
|
import { FileChooser } from './fileChooser';
|
||||||
|
import { logError, Logger, Log } from './logger';
|
||||||
|
|
||||||
export interface PageDelegate {
|
export interface PageDelegate {
|
||||||
readonly rawMouse: input.RawMouse;
|
readonly rawMouse: input.RawMouse;
|
||||||
|
|
@ -86,7 +87,7 @@ type PageState = {
|
||||||
extraHTTPHeaders: network.Headers | null;
|
extraHTTPHeaders: network.Headers | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Page extends ExtendedEventEmitter {
|
export class Page extends ExtendedEventEmitter implements Logger {
|
||||||
private _closed = false;
|
private _closed = false;
|
||||||
private _closedCallback: () => void;
|
private _closedCallback: () => void;
|
||||||
private _closedPromise: Promise<void>;
|
private _closedPromise: Promise<void>;
|
||||||
|
|
@ -525,23 +526,33 @@ export class Page extends ExtendedEventEmitter {
|
||||||
this._delegate.setFileChooserIntercepted(false);
|
this._delegate.setFileChooserIntercepted(false);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isLogEnabled(log: Log): boolean {
|
||||||
|
return this._browserContext._isLogEnabled(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log(log: Log, message: string | Error, ...args: any[]) {
|
||||||
|
return this._browserContext._log(log, message, ...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Worker extends EventEmitter {
|
export class Worker extends EventEmitter {
|
||||||
|
private _logger: Logger;
|
||||||
private _url: string;
|
private _url: string;
|
||||||
private _executionContextPromise: Promise<js.ExecutionContext>;
|
private _executionContextPromise: Promise<js.ExecutionContext>;
|
||||||
private _executionContextCallback: (value?: js.ExecutionContext) => void;
|
private _executionContextCallback: (value?: js.ExecutionContext) => void;
|
||||||
_existingExecutionContext: js.ExecutionContext | null = null;
|
_existingExecutionContext: js.ExecutionContext | null = null;
|
||||||
|
|
||||||
constructor(url: string) {
|
constructor(logger: Logger, url: string) {
|
||||||
super();
|
super();
|
||||||
|
this._logger = logger;
|
||||||
this._url = url;
|
this._url = url;
|
||||||
this._executionContextCallback = () => {};
|
this._executionContextCallback = () => {};
|
||||||
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
|
this._executionContextPromise = new Promise(x => this._executionContextCallback = x);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createExecutionContext(delegate: js.ExecutionContextDelegate) {
|
_createExecutionContext(delegate: js.ExecutionContextDelegate) {
|
||||||
this._existingExecutionContext = new js.ExecutionContext(delegate);
|
this._existingExecutionContext = new js.ExecutionContext(delegate, this._logger);
|
||||||
this._executionContextCallback(this._existingExecutionContext);
|
this._executionContextCallback(this._existingExecutionContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -588,7 +599,7 @@ export class PageBinding {
|
||||||
else
|
else
|
||||||
expression = helper.evaluationString(deliverErrorValue, name, seq, error);
|
expression = helper.evaluationString(deliverErrorValue, name, seq, error);
|
||||||
}
|
}
|
||||||
context.evaluateInternal(expression).catch(debugError);
|
context.evaluateInternal(expression).catch(logError(page));
|
||||||
|
|
||||||
function deliverResult(name: string, seq: number, result: any) {
|
function deliverResult(name: string, seq: number, result: any) {
|
||||||
(window as any)[name]['callbacks'].get(seq).resolve(result);
|
(window as any)[name]['callbacks'].get(seq).resolve(result);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
import { BrowserServer } from './browserServer';
|
import { BrowserServer } from './browserServer';
|
||||||
|
import { LoggerSink } from '../logger';
|
||||||
|
|
||||||
export type BrowserArgOptions = {
|
export type BrowserArgOptions = {
|
||||||
headless?: boolean,
|
headless?: boolean,
|
||||||
|
|
@ -30,17 +31,14 @@ type LaunchOptionsBase = BrowserArgOptions & {
|
||||||
handleSIGTERM?: boolean,
|
handleSIGTERM?: boolean,
|
||||||
handleSIGHUP?: boolean,
|
handleSIGHUP?: boolean,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
/**
|
loggerSink?: LoggerSink,
|
||||||
* Whether to dump stdio of the browser, this is useful for example when
|
|
||||||
* diagnosing browser launch issues.
|
|
||||||
*/
|
|
||||||
dumpio?: boolean,
|
|
||||||
env?: {[key: string]: string} | undefined
|
env?: {[key: string]: string} | undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConnectOptions = {
|
export type ConnectOptions = {
|
||||||
wsEndpoint: string,
|
wsEndpoint: string,
|
||||||
slowMo?: number
|
slowMo?: number,
|
||||||
|
loggerSink?: LoggerSink,
|
||||||
};
|
};
|
||||||
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
|
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
|
||||||
export type LaunchServerOptions = LaunchOptionsBase & { port?: number };
|
export type LaunchServerOptions = LaunchOptionsBase & { port?: number };
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { debugError, helper, assert } from '../helper';
|
import { helper, assert } from '../helper';
|
||||||
import { CRBrowser } from '../chromium/crBrowser';
|
import { CRBrowser } from '../chromium/crBrowser';
|
||||||
import * as ws from 'ws';
|
import * as ws from 'ws';
|
||||||
import { launchProcess } from './processLauncher';
|
import { launchProcess } from './processLauncher';
|
||||||
|
|
@ -31,6 +31,7 @@ import { BrowserServer, WebSocketWrapper } from './browserServer';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport';
|
import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
|
import { Logger, logError, RootLogger } from '../logger';
|
||||||
|
|
||||||
export class Chromium implements BrowserType<CRBrowser> {
|
export class Chromium implements BrowserType<CRBrowser> {
|
||||||
private _executablePath: (string|undefined);
|
private _executablePath: (string|undefined);
|
||||||
|
|
@ -47,8 +48,8 @@ export class Chromium implements BrowserType<CRBrowser> {
|
||||||
|
|
||||||
async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
|
async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
|
||||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||||
const { browserServer, transport, downloadsPath } = await this._launchServer(options, 'local');
|
const { browserServer, transport, downloadsPath, logger } = await this._launchServer(options, 'local');
|
||||||
const browser = await CRBrowser.connect(transport!, false, options.slowMo);
|
const browser = await CRBrowser.connect(transport!, false, logger, options.slowMo);
|
||||||
browser._ownedServer = browserServer;
|
browser._ownedServer = browserServer;
|
||||||
browser._downloadsPath = downloadsPath;
|
browser._downloadsPath = downloadsPath;
|
||||||
return browser;
|
return browser;
|
||||||
|
|
@ -61,20 +62,19 @@ export class Chromium implements BrowserType<CRBrowser> {
|
||||||
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
|
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
|
||||||
const {
|
const {
|
||||||
timeout = 30000,
|
timeout = 30000,
|
||||||
slowMo = 0
|
slowMo = 0,
|
||||||
} = options;
|
} = options;
|
||||||
const { transport, browserServer } = await this._launchServer(options, 'persistent', userDataDir);
|
const { transport, browserServer, logger } = await this._launchServer(options, 'persistent', userDataDir);
|
||||||
const browser = await CRBrowser.connect(transport!, true, slowMo);
|
const browser = await CRBrowser.connect(transport!, true, logger, slowMo);
|
||||||
browser._ownedServer = browserServer;
|
browser._ownedServer = browserServer;
|
||||||
await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout);
|
await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout);
|
||||||
return browser._defaultContext!;
|
return browser._defaultContext!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string }> {
|
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string, logger: Logger }> {
|
||||||
const {
|
const {
|
||||||
ignoreDefaultArgs = false,
|
ignoreDefaultArgs = false,
|
||||||
args = [],
|
args = [],
|
||||||
dumpio = false,
|
|
||||||
executablePath = null,
|
executablePath = null,
|
||||||
env = process.env,
|
env = process.env,
|
||||||
handleSIGINT = true,
|
handleSIGINT = true,
|
||||||
|
|
@ -83,6 +83,7 @@ export class Chromium implements BrowserType<CRBrowser> {
|
||||||
port = 0,
|
port = 0,
|
||||||
} = options;
|
} = options;
|
||||||
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
||||||
|
const logger = new RootLogger(options.loggerSink);
|
||||||
|
|
||||||
let temporaryUserDataDir: string | null = null;
|
let temporaryUserDataDir: string | null = null;
|
||||||
if (!userDataDir) {
|
if (!userDataDir) {
|
||||||
|
|
@ -108,7 +109,7 @@ export class Chromium implements BrowserType<CRBrowser> {
|
||||||
handleSIGINT,
|
handleSIGINT,
|
||||||
handleSIGTERM,
|
handleSIGTERM,
|
||||||
handleSIGHUP,
|
handleSIGHUP,
|
||||||
dumpio,
|
logger,
|
||||||
pipe: true,
|
pipe: true,
|
||||||
tempDir: temporaryUserDataDir || undefined,
|
tempDir: temporaryUserDataDir || undefined,
|
||||||
attemptToGracefullyClose: async () => {
|
attemptToGracefullyClose: async () => {
|
||||||
|
|
@ -129,14 +130,14 @@ export class Chromium implements BrowserType<CRBrowser> {
|
||||||
let transport: PipeTransport | undefined = undefined;
|
let transport: PipeTransport | undefined = undefined;
|
||||||
let browserServer: BrowserServer | undefined = undefined;
|
let browserServer: BrowserServer | undefined = undefined;
|
||||||
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
||||||
transport = new PipeTransport(stdio[3], stdio[4]);
|
transport = new PipeTransport(stdio[3], stdio[4], logger);
|
||||||
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port) : null);
|
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port) : null);
|
||||||
return { browserServer, transport, downloadsPath };
|
return { browserServer, transport, downloadsPath, logger };
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(options: ConnectOptions): Promise<CRBrowser> {
|
async connect(options: ConnectOptions): Promise<CRBrowser> {
|
||||||
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
|
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
|
||||||
return CRBrowser.connect(transport, false, options.slowMo);
|
return CRBrowser.connect(transport, false, new RootLogger(options.loggerSink), options.slowMo);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,7 +179,7 @@ export class Chromium implements BrowserType<CRBrowser> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper {
|
function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: Logger, port: number): WebSocketWrapper {
|
||||||
const server = new ws.Server({ port });
|
const server = new ws.Server({ port });
|
||||||
const guid = helper.guid();
|
const guid = helper.guid();
|
||||||
|
|
||||||
|
|
@ -275,7 +276,7 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number
|
||||||
session.queue!.push(parsedMessage);
|
session.queue!.push(parsedMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', error => debugError(error));
|
socket.on('error', logError(logger));
|
||||||
|
|
||||||
socket.on('close', (socket as any).__closeListener = () => {
|
socket.on('close', (socket as any).__closeListener = () => {
|
||||||
const session = socketToBrowserSession.get(socket);
|
const session = socketToBrowserSession.get(socket);
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,12 @@ import { TimeoutError } from '../errors';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import { FFBrowser } from '../firefox/ffBrowser';
|
import { FFBrowser } from '../firefox/ffBrowser';
|
||||||
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
|
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
|
||||||
import { debugError, helper, assert } from '../helper';
|
import { helper, assert } from '../helper';
|
||||||
import { BrowserServer, WebSocketWrapper } from './browserServer';
|
import { BrowserServer, WebSocketWrapper } from './browserServer';
|
||||||
import { BrowserArgOptions, BrowserType, LaunchOptions, LaunchServerOptions, ConnectOptions } from './browserType';
|
import { BrowserArgOptions, BrowserType, LaunchOptions, LaunchServerOptions, ConnectOptions } from './browserType';
|
||||||
import { launchProcess, waitForLine } from './processLauncher';
|
import { launchProcess, waitForLine } from './processLauncher';
|
||||||
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
|
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
|
||||||
|
import { RootLogger, Logger, logError } from '../logger';
|
||||||
|
|
||||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||||
|
|
||||||
|
|
@ -49,9 +50,9 @@ export class Firefox implements BrowserType<FFBrowser> {
|
||||||
|
|
||||||
async launch(options: LaunchOptions = {}): Promise<FFBrowser> {
|
async launch(options: LaunchOptions = {}): Promise<FFBrowser> {
|
||||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||||
const {browserServer, downloadsPath} = await this._launchServer(options, 'local');
|
const { browserServer, downloadsPath, logger } = await this._launchServer(options, 'local');
|
||||||
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
|
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
|
||||||
return FFBrowser.connect(transport, false, options.slowMo);
|
return FFBrowser.connect(transport, logger, false, options.slowMo);
|
||||||
});
|
});
|
||||||
browser._ownedServer = browserServer;
|
browser._ownedServer = browserServer;
|
||||||
browser._downloadsPath = downloadsPath;
|
browser._downloadsPath = downloadsPath;
|
||||||
|
|
@ -67,9 +68,9 @@ export class Firefox implements BrowserType<FFBrowser> {
|
||||||
timeout = 30000,
|
timeout = 30000,
|
||||||
slowMo = 0,
|
slowMo = 0,
|
||||||
} = options;
|
} = options;
|
||||||
const {browserServer, downloadsPath} = await this._launchServer(options, 'persistent', userDataDir);
|
const { browserServer, downloadsPath, logger } = await this._launchServer(options, 'persistent', userDataDir);
|
||||||
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
|
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
|
||||||
return FFBrowser.connect(transport, true, slowMo);
|
return FFBrowser.connect(transport, logger, true, slowMo);
|
||||||
});
|
});
|
||||||
browser._ownedServer = browserServer;
|
browser._ownedServer = browserServer;
|
||||||
browser._downloadsPath = downloadsPath;
|
browser._downloadsPath = downloadsPath;
|
||||||
|
|
@ -78,11 +79,10 @@ export class Firefox implements BrowserType<FFBrowser> {
|
||||||
return browserContext;
|
return browserContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string }> {
|
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, logger: Logger }> {
|
||||||
const {
|
const {
|
||||||
ignoreDefaultArgs = false,
|
ignoreDefaultArgs = false,
|
||||||
args = [],
|
args = [],
|
||||||
dumpio = false,
|
|
||||||
executablePath = null,
|
executablePath = null,
|
||||||
env = process.env,
|
env = process.env,
|
||||||
handleSIGHUP = true,
|
handleSIGHUP = true,
|
||||||
|
|
@ -92,6 +92,7 @@ export class Firefox implements BrowserType<FFBrowser> {
|
||||||
port = 0,
|
port = 0,
|
||||||
} = options;
|
} = options;
|
||||||
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
||||||
|
const logger = new RootLogger(options.loggerSink);
|
||||||
|
|
||||||
const firefoxArguments = [];
|
const firefoxArguments = [];
|
||||||
|
|
||||||
|
|
@ -123,7 +124,7 @@ export class Firefox implements BrowserType<FFBrowser> {
|
||||||
handleSIGINT,
|
handleSIGINT,
|
||||||
handleSIGTERM,
|
handleSIGTERM,
|
||||||
handleSIGHUP,
|
handleSIGHUP,
|
||||||
dumpio,
|
logger,
|
||||||
pipe: false,
|
pipe: false,
|
||||||
tempDir: temporaryProfileDir || undefined,
|
tempDir: temporaryProfileDir || undefined,
|
||||||
attemptToGracefullyClose: async () => {
|
attemptToGracefullyClose: async () => {
|
||||||
|
|
@ -145,15 +146,16 @@ export class Firefox implements BrowserType<FFBrowser> {
|
||||||
|
|
||||||
let browserServer: BrowserServer | undefined = undefined;
|
let browserServer: BrowserServer | undefined = undefined;
|
||||||
let browserWSEndpoint: string | undefined = undefined;
|
let browserWSEndpoint: string | undefined = undefined;
|
||||||
const webSocketWrapper = launchType === 'server' ? (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, port))) : new WebSocketWrapper(innerEndpoint, []);
|
const webSocketWrapper = launchType === 'server' ? (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, logger, port))) : new WebSocketWrapper(innerEndpoint, []);
|
||||||
browserWSEndpoint = webSocketWrapper.wsEndpoint;
|
browserWSEndpoint = webSocketWrapper.wsEndpoint;
|
||||||
browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper);
|
browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper);
|
||||||
return {browserServer, downloadsPath};
|
return { browserServer, downloadsPath, logger };
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(options: ConnectOptions): Promise<FFBrowser> {
|
async connect(options: ConnectOptions): Promise<FFBrowser> {
|
||||||
|
const logger = new RootLogger(options.loggerSink);
|
||||||
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
|
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
|
||||||
return FFBrowser.connect(transport, false, options.slowMo);
|
return FFBrowser.connect(transport, logger, false, options.slowMo);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +198,7 @@ export class Firefox implements BrowserType<FFBrowser> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper {
|
function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: Logger, port: number): WebSocketWrapper {
|
||||||
const server = new ws.Server({ port });
|
const server = new ws.Server({ port });
|
||||||
const guid = helper.guid();
|
const guid = helper.guid();
|
||||||
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
|
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
|
||||||
|
|
@ -302,7 +304,7 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number
|
||||||
pendingBrowserContextDeletions.set(seqNum, params.browserContextId);
|
pendingBrowserContextDeletions.set(seqNum, params.browserContextId);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', error => debugError(error));
|
socket.on('error', logError(logger));
|
||||||
|
|
||||||
socket.on('close', (socket as any).__closeListener = () => {
|
socket.on('close', (socket as any).__closeListener = () => {
|
||||||
for (const [browserContextId, s] of browserContextIds) {
|
for (const [browserContextId, s] of browserContextIds) {
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debugError, helper, RegisteredListener } from '../helper';
|
import { helper, RegisteredListener } from '../helper';
|
||||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||||
|
import { logError, Logger } from '../logger';
|
||||||
|
|
||||||
export class PipeTransport implements ConnectionTransport {
|
export class PipeTransport implements ConnectionTransport {
|
||||||
private _pipeWrite: NodeJS.WritableStream | null;
|
private _pipeWrite: NodeJS.WritableStream | null;
|
||||||
|
|
@ -27,7 +28,7 @@ export class PipeTransport implements ConnectionTransport {
|
||||||
onmessage?: (message: ProtocolResponse) => void;
|
onmessage?: (message: ProtocolResponse) => void;
|
||||||
onclose?: () => void;
|
onclose?: () => void;
|
||||||
|
|
||||||
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream) {
|
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream, logger: Logger) {
|
||||||
this._pipeWrite = pipeWrite;
|
this._pipeWrite = pipeWrite;
|
||||||
this._eventListeners = [
|
this._eventListeners = [
|
||||||
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
|
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
|
||||||
|
|
@ -36,8 +37,8 @@ export class PipeTransport implements ConnectionTransport {
|
||||||
if (this.onclose)
|
if (this.onclose)
|
||||||
this.onclose.call(null);
|
this.onclose.call(null);
|
||||||
}),
|
}),
|
||||||
helper.addEventListener(pipeRead, 'error', debugError),
|
helper.addEventListener(pipeRead, 'error', logError(logger)),
|
||||||
helper.addEventListener(pipeWrite, 'error', debugError),
|
helper.addEventListener(pipeWrite, 'error', logError(logger)),
|
||||||
];
|
];
|
||||||
this.onmessage = undefined;
|
this.onmessage = undefined;
|
||||||
this.onclose = undefined;
|
this.onclose = undefined;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import * as debug from 'debug';
|
import { Log, Logger } from '../logger';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
@ -31,6 +31,20 @@ const removeFolderAsync = util.promisify(removeFolder);
|
||||||
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||||
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
|
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
|
||||||
|
|
||||||
|
const browserLog: Log = {
|
||||||
|
name: 'browser',
|
||||||
|
};
|
||||||
|
|
||||||
|
const browserStdOutLog: Log = {
|
||||||
|
name: 'browser:out',
|
||||||
|
};
|
||||||
|
|
||||||
|
const browserStdErrLog: Log = {
|
||||||
|
name: 'browser:err',
|
||||||
|
severity: 'warning'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type LaunchProcessOptions = {
|
export type LaunchProcessOptions = {
|
||||||
executablePath: string,
|
executablePath: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
|
|
@ -39,13 +53,13 @@ export type LaunchProcessOptions = {
|
||||||
handleSIGINT?: boolean,
|
handleSIGINT?: boolean,
|
||||||
handleSIGTERM?: boolean,
|
handleSIGTERM?: boolean,
|
||||||
handleSIGHUP?: boolean,
|
handleSIGHUP?: boolean,
|
||||||
dumpio?: boolean,
|
|
||||||
pipe?: boolean,
|
pipe?: boolean,
|
||||||
tempDir?: string,
|
tempDir?: string,
|
||||||
|
|
||||||
// Note: attemptToGracefullyClose should reject if it does not close the browser.
|
// Note: attemptToGracefullyClose should reject if it does not close the browser.
|
||||||
attemptToGracefullyClose: () => Promise<any>,
|
attemptToGracefullyClose: () => Promise<any>,
|
||||||
onkill: (exitCode: number | null, signal: string | null) => void,
|
onkill: (exitCode: number | null, signal: string | null) => void,
|
||||||
|
logger: Logger,
|
||||||
};
|
};
|
||||||
|
|
||||||
type LaunchResult = {
|
type LaunchResult = {
|
||||||
|
|
@ -54,17 +68,10 @@ type LaunchResult = {
|
||||||
downloadsPath: string
|
downloadsPath: string
|
||||||
};
|
};
|
||||||
|
|
||||||
let lastLaunchedId = 0;
|
|
||||||
|
|
||||||
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
|
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
|
||||||
const id = ++lastLaunchedId;
|
const logger = options.logger;
|
||||||
const debugBrowser = debug(`pw:browser:proc:[${id}]`);
|
|
||||||
const debugBrowserOut = debug(`pw:browser:out:[${id}]`);
|
|
||||||
const debugBrowserErr = debug(`pw:browser:err:[${id}]`);
|
|
||||||
(debugBrowser as any).color = '33';
|
|
||||||
(debugBrowserOut as any).color = '178';
|
|
||||||
(debugBrowserErr as any).color = '160';
|
|
||||||
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
|
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
|
||||||
|
logger._log(browserLog, `<launching> ${options.executablePath} ${options.args.join(' ')}`);
|
||||||
const spawnedProcess = childProcess.spawn(
|
const spawnedProcess = childProcess.spawn(
|
||||||
options.executablePath,
|
options.executablePath,
|
||||||
options.args,
|
options.args,
|
||||||
|
|
@ -77,8 +84,6 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
stdio
|
stdio
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
debugBrowser(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
|
|
||||||
|
|
||||||
if (!spawnedProcess.pid) {
|
if (!spawnedProcess.pid) {
|
||||||
let reject: (e: Error) => void;
|
let reject: (e: Error) => void;
|
||||||
const result = new Promise<LaunchResult>((f, r) => reject = r);
|
const result = new Promise<LaunchResult>((f, r) => reject = r);
|
||||||
|
|
@ -87,19 +92,16 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
logger._log(browserLog, `<launched> pid=${spawnedProcess.pid}`);
|
||||||
|
|
||||||
const stdout = readline.createInterface({ input: spawnedProcess.stdout });
|
const stdout = readline.createInterface({ input: spawnedProcess.stdout });
|
||||||
stdout.on('line', (data: string) => {
|
stdout.on('line', (data: string) => {
|
||||||
debugBrowserOut(data);
|
logger._log(browserStdOutLog, data);
|
||||||
if (options.dumpio)
|
|
||||||
console.log(`\x1b[33m[out]\x1b[0m ${data}`); // eslint-disable-line no-console
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const stderr = readline.createInterface({ input: spawnedProcess.stderr });
|
const stderr = readline.createInterface({ input: spawnedProcess.stderr });
|
||||||
stderr.on('line', (data: string) => {
|
stderr.on('line', (data: string) => {
|
||||||
debugBrowserErr(data);
|
logger._log(browserStdErrLog, data);
|
||||||
if (options.dumpio)
|
|
||||||
console.log(`\x1b[31m[err]\x1b[0m ${data}`); // eslint-disable-line no-console
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER);
|
const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER);
|
||||||
|
|
@ -107,7 +109,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
let processClosed = false;
|
let processClosed = false;
|
||||||
const waitForProcessToClose = new Promise((fulfill, reject) => {
|
const waitForProcessToClose = new Promise((fulfill, reject) => {
|
||||||
spawnedProcess.once('exit', (exitCode, signal) => {
|
spawnedProcess.once('exit', (exitCode, signal) => {
|
||||||
debugBrowser(`<process did exit ${exitCode}, ${signal}>`);
|
logger._log(browserLog, `<process did exit ${exitCode}, ${signal}>`);
|
||||||
processClosed = true;
|
processClosed = true;
|
||||||
helper.removeEventListeners(listeners);
|
helper.removeEventListeners(listeners);
|
||||||
options.onkill(exitCode, signal);
|
options.onkill(exitCode, signal);
|
||||||
|
|
@ -137,20 +139,20 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||||
// reentrancy to this function, for example user sends SIGINT second time.
|
// reentrancy to this function, for example user sends SIGINT second time.
|
||||||
// In this case, let's forcefully kill the process.
|
// In this case, let's forcefully kill the process.
|
||||||
if (gracefullyClosing) {
|
if (gracefullyClosing) {
|
||||||
debugBrowser(`<forecefully close>`);
|
logger._log(browserLog, `<forecefully close>`);
|
||||||
killProcess();
|
killProcess();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gracefullyClosing = true;
|
gracefullyClosing = true;
|
||||||
debugBrowser(`<gracefully close start>`);
|
logger._log(browserLog, `<gracefully close start>`);
|
||||||
await options.attemptToGracefullyClose().catch(() => killProcess());
|
await options.attemptToGracefullyClose().catch(() => killProcess());
|
||||||
await waitForProcessToClose;
|
await waitForProcessToClose;
|
||||||
debugBrowser(`<gracefully close end>`);
|
logger._log(browserLog, `<gracefully close end>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method has to be sync to be used as 'exit' event handler.
|
// This method has to be sync to be used as 'exit' event handler.
|
||||||
function killProcess() {
|
function killProcess() {
|
||||||
debugBrowser(`<kill>`);
|
logger._log(browserLog, `<kill>`);
|
||||||
helper.removeEventListeners(listeners);
|
helper.removeEventListeners(listeners);
|
||||||
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
|
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
|
||||||
// Force kill the browser.
|
// Force kill the browser.
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { debugError, helper, assert } from '../helper';
|
import { helper, assert } from '../helper';
|
||||||
import { kBrowserCloseMessageId } from '../webkit/wkConnection';
|
import { kBrowserCloseMessageId } from '../webkit/wkConnection';
|
||||||
import { LaunchOptions, BrowserArgOptions, BrowserType, LaunchServerOptions, ConnectOptions } from './browserType';
|
import { LaunchOptions, BrowserArgOptions, BrowserType, LaunchServerOptions, ConnectOptions } from './browserType';
|
||||||
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
|
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
|
||||||
|
|
@ -31,6 +31,7 @@ import { LaunchType } from '../browser';
|
||||||
import { BrowserServer, WebSocketWrapper } from './browserServer';
|
import { BrowserServer, WebSocketWrapper } from './browserServer';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import { BrowserContext } from '../browserContext';
|
import { BrowserContext } from '../browserContext';
|
||||||
|
import { Logger, logError, RootLogger } from '../logger';
|
||||||
|
|
||||||
export class WebKit implements BrowserType<WKBrowser> {
|
export class WebKit implements BrowserType<WKBrowser> {
|
||||||
private _executablePath: (string|undefined);
|
private _executablePath: (string|undefined);
|
||||||
|
|
@ -47,8 +48,8 @@ export class WebKit implements BrowserType<WKBrowser> {
|
||||||
|
|
||||||
async launch(options: LaunchOptions = {}): Promise<WKBrowser> {
|
async launch(options: LaunchOptions = {}): Promise<WKBrowser> {
|
||||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||||
const { browserServer, transport, downloadsPath } = await this._launchServer(options, 'local');
|
const { browserServer, transport, downloadsPath, logger } = await this._launchServer(options, 'local');
|
||||||
const browser = await WKBrowser.connect(transport!, options.slowMo, false);
|
const browser = await WKBrowser.connect(transport!, logger, options.slowMo, false);
|
||||||
browser._ownedServer = browserServer;
|
browser._ownedServer = browserServer;
|
||||||
browser._downloadsPath = downloadsPath;
|
browser._downloadsPath = downloadsPath;
|
||||||
return browser;
|
return browser;
|
||||||
|
|
@ -63,18 +64,17 @@ export class WebKit implements BrowserType<WKBrowser> {
|
||||||
timeout = 30000,
|
timeout = 30000,
|
||||||
slowMo = 0,
|
slowMo = 0,
|
||||||
} = options;
|
} = options;
|
||||||
const { transport, browserServer } = await this._launchServer(options, 'persistent', userDataDir);
|
const { transport, browserServer, logger } = await this._launchServer(options, 'persistent', userDataDir);
|
||||||
const browser = await WKBrowser.connect(transport!, slowMo, true);
|
const browser = await WKBrowser.connect(transport!, logger, slowMo, true);
|
||||||
browser._ownedServer = browserServer;
|
browser._ownedServer = browserServer;
|
||||||
await helper.waitWithTimeout(browser._waitForFirstPageTarget(), 'first page', timeout);
|
await helper.waitWithTimeout(browser._waitForFirstPageTarget(), 'first page', timeout);
|
||||||
return browser._defaultContext!;
|
return browser._defaultContext!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string }> {
|
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string, logger: Logger }> {
|
||||||
const {
|
const {
|
||||||
ignoreDefaultArgs = false,
|
ignoreDefaultArgs = false,
|
||||||
args = [],
|
args = [],
|
||||||
dumpio = false,
|
|
||||||
executablePath = null,
|
executablePath = null,
|
||||||
env = process.env,
|
env = process.env,
|
||||||
handleSIGINT = true,
|
handleSIGINT = true,
|
||||||
|
|
@ -83,6 +83,7 @@ export class WebKit implements BrowserType<WKBrowser> {
|
||||||
port = 0,
|
port = 0,
|
||||||
} = options;
|
} = options;
|
||||||
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
|
||||||
|
const logger = new RootLogger(options.loggerSink);
|
||||||
|
|
||||||
let temporaryUserDataDir: string | null = null;
|
let temporaryUserDataDir: string | null = null;
|
||||||
if (!userDataDir) {
|
if (!userDataDir) {
|
||||||
|
|
@ -109,7 +110,7 @@ export class WebKit implements BrowserType<WKBrowser> {
|
||||||
handleSIGINT,
|
handleSIGINT,
|
||||||
handleSIGTERM,
|
handleSIGTERM,
|
||||||
handleSIGHUP,
|
handleSIGHUP,
|
||||||
dumpio,
|
logger,
|
||||||
pipe: true,
|
pipe: true,
|
||||||
tempDir: temporaryUserDataDir || undefined,
|
tempDir: temporaryUserDataDir || undefined,
|
||||||
attemptToGracefullyClose: async () => {
|
attemptToGracefullyClose: async () => {
|
||||||
|
|
@ -129,14 +130,14 @@ export class WebKit implements BrowserType<WKBrowser> {
|
||||||
let transport: ConnectionTransport | undefined = undefined;
|
let transport: ConnectionTransport | undefined = undefined;
|
||||||
let browserServer: BrowserServer | undefined = undefined;
|
let browserServer: BrowserServer | undefined = undefined;
|
||||||
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
||||||
transport = new PipeTransport(stdio[3], stdio[4]);
|
transport = new PipeTransport(stdio[3], stdio[4], logger);
|
||||||
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, port || 0) : null);
|
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port || 0) : null);
|
||||||
return { browserServer, transport, downloadsPath };
|
return { browserServer, transport, downloadsPath, logger };
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(options: ConnectOptions): Promise<WKBrowser> {
|
async connect(options: ConnectOptions): Promise<WKBrowser> {
|
||||||
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
|
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
|
||||||
return WKBrowser.connect(transport, options.slowMo);
|
return WKBrowser.connect(transport, new RootLogger(options.loggerSink), options.slowMo);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,7 +170,7 @@ const mkdtempAsync = util.promisify(fs.mkdtemp);
|
||||||
|
|
||||||
const WEBKIT_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');
|
const WEBKIT_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');
|
||||||
|
|
||||||
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper {
|
function wrapTransportWithWebSocket(transport: ConnectionTransport, logger: Logger, port: number): WebSocketWrapper {
|
||||||
const server = new ws.Server({ port });
|
const server = new ws.Server({ port });
|
||||||
const guid = helper.guid();
|
const guid = helper.guid();
|
||||||
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
|
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
|
||||||
|
|
@ -282,7 +283,7 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number
|
||||||
pendingBrowserContextDeletions.set(seqNum, params.browserContextId);
|
pendingBrowserContextDeletions.set(seqNum, params.browserContextId);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', error => debugError(error));
|
socket.on('error', logError(logger));
|
||||||
|
|
||||||
socket.on('close', (socket as any).__closeListener = () => {
|
socket.on('close', (socket as any).__closeListener = () => {
|
||||||
for (const [pageProxyId, s] of pageProxyIds) {
|
for (const [pageProxyId, s] of pageProxyIds) {
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as debug from 'debug';
|
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import { helper } from './helper';
|
import { helper } from './helper';
|
||||||
|
import { Log } from './logger';
|
||||||
|
|
||||||
export type ProtocolRequest = {
|
export type ProtocolRequest = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -221,5 +221,8 @@ export class InterceptingTransport implements ConnectionTransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const debugProtocol = debug('pw:protocol');
|
export const protocolLog: Log = {
|
||||||
(debugProtocol as any).color = '34';
|
name: 'protocol',
|
||||||
|
severity: 'verbose',
|
||||||
|
color: 'green'
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import * as types from '../types';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection';
|
import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection';
|
||||||
import { WKPage } from './wkPage';
|
import { WKPage } from './wkPage';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
|
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
|
||||||
|
|
||||||
|
|
@ -40,14 +41,14 @@ export class WKBrowser extends BrowserBase {
|
||||||
private _firstPageCallback: () => void = () => {};
|
private _firstPageCallback: () => void = () => {};
|
||||||
private readonly _firstPagePromise: Promise<void>;
|
private readonly _firstPagePromise: Promise<void>;
|
||||||
|
|
||||||
static async connect(transport: ConnectionTransport, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> {
|
static async connect(transport: ConnectionTransport, logger: Logger, slowMo: number = 0, attachToDefaultContext: boolean = false): Promise<WKBrowser> {
|
||||||
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), attachToDefaultContext);
|
const browser = new WKBrowser(SlowMoTransport.wrap(transport, slowMo), logger, attachToDefaultContext);
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(transport: ConnectionTransport, attachToDefaultContext: boolean) {
|
constructor(transport: ConnectionTransport, logger: Logger, attachToDefaultContext: boolean) {
|
||||||
super();
|
super(logger);
|
||||||
this._connection = new WKConnection(transport, this._onDisconnect.bind(this));
|
this._connection = new WKConnection(transport, logger, this._onDisconnect.bind(this));
|
||||||
this._browserSession = this._connection.browserSession;
|
this._browserSession = this._connection.browserSession;
|
||||||
|
|
||||||
if (attachToDefaultContext)
|
if (attachToDefaultContext)
|
||||||
|
|
@ -184,7 +185,7 @@ export class WKBrowserContext extends BrowserContextBase {
|
||||||
readonly _evaluateOnNewDocumentSources: string[];
|
readonly _evaluateOnNewDocumentSources: string[];
|
||||||
|
|
||||||
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
|
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
|
||||||
super(options);
|
super(browser, options);
|
||||||
this._browser = browser;
|
this._browser = browser;
|
||||||
this._browserContextId = browserContextId;
|
this._browserContextId = browserContextId;
|
||||||
this._evaluateOnNewDocumentSources = [];
|
this._evaluateOnNewDocumentSources = [];
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as debug from 'debug';
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { assert } from '../helper';
|
import { assert } from '../helper';
|
||||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, debugProtocol } from '../transport';
|
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
// WKPlaywright uses this special id to issue Browser.close command which we
|
// WKPlaywright uses this special id to issue Browser.close command which we
|
||||||
// should ignore.
|
// should ignore.
|
||||||
|
|
@ -37,9 +37,11 @@ export class WKConnection {
|
||||||
private _closed = false;
|
private _closed = false;
|
||||||
|
|
||||||
readonly browserSession: WKSession;
|
readonly browserSession: WKSession;
|
||||||
|
private _logger: Logger;
|
||||||
|
|
||||||
constructor(transport: ConnectionTransport, onDisconnect: () => void) {
|
constructor(transport: ConnectionTransport, logger: Logger, onDisconnect: () => void) {
|
||||||
this._transport = transport;
|
this._transport = transport;
|
||||||
|
this._logger = logger;
|
||||||
this._transport.onmessage = this._dispatchMessage.bind(this);
|
this._transport.onmessage = this._dispatchMessage.bind(this);
|
||||||
this._transport.onclose = this._onClose.bind(this);
|
this._transport.onclose = this._onClose.bind(this);
|
||||||
this._onDisconnect = onDisconnect;
|
this._onDisconnect = onDisconnect;
|
||||||
|
|
@ -53,14 +55,14 @@ export class WKConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
rawSend(message: ProtocolRequest) {
|
rawSend(message: ProtocolRequest) {
|
||||||
if (debugProtocol.enabled)
|
if (this._logger._isLogEnabled(protocolLog))
|
||||||
debugProtocol('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
this._logger._log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||||
this._transport.send(message);
|
this._transport.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dispatchMessage(message: ProtocolResponse) {
|
private _dispatchMessage(message: ProtocolResponse) {
|
||||||
if (debugProtocol.enabled)
|
if (this._logger._isLogEnabled(protocolLog))
|
||||||
debugProtocol('◀ RECV ' + JSON.stringify(message));
|
this._logger._log(protocolLog, '◀ RECV ' + JSON.stringify(message));
|
||||||
if (message.id === kBrowserCloseMessageId)
|
if (message.id === kBrowserCloseMessageId)
|
||||||
return;
|
return;
|
||||||
if (message.pageProxyId) {
|
if (message.pageProxyId) {
|
||||||
|
|
@ -129,7 +131,6 @@ export class WKSession extends EventEmitter {
|
||||||
throw new Error(`Protocol error (${method}): ${this.errorText}`);
|
throw new Error(`Protocol error (${method}): ${this.errorText}`);
|
||||||
const id = this.connection.nextMessageId();
|
const id = this.connection.nextMessageId();
|
||||||
const messageObj = { id, method, params };
|
const messageObj = { id, method, params };
|
||||||
debug('pw:wrapped:' + this.sessionId)('SEND ► ' + JSON.stringify(messageObj, null, 2));
|
|
||||||
this._rawSend(messageObj);
|
this._rawSend(messageObj);
|
||||||
return new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
|
return new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
|
||||||
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
|
||||||
|
|
@ -152,7 +153,6 @@ export class WKSession extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchMessage(object: any) {
|
dispatchMessage(object: any) {
|
||||||
debug('pw:wrapped:' + this.sessionId)('◀ RECV ' + JSON.stringify(object, null, 2));
|
|
||||||
if (object.id && this._callbacks.has(object.id)) {
|
if (object.id && this._callbacks.has(object.id)) {
|
||||||
const callback = this._callbacks.get(object.id)!;
|
const callback = this._callbacks.get(object.id)!;
|
||||||
this._callbacks.delete(object.id);
|
this._callbacks.delete(object.id);
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { assert, debugError, helper } from '../helper';
|
import { assert, helper } from '../helper';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { WKSession } from './wkConnection';
|
import { WKSession } from './wkConnection';
|
||||||
|
import { logError } from '../logger';
|
||||||
|
|
||||||
const errorReasons: { [reason: string]: string } = {
|
const errorReasons: { [reason: string]: string } = {
|
||||||
'aborted': 'Cancellation',
|
'aborted': 'Cancellation',
|
||||||
|
|
@ -61,7 +62,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
await this._session.send('Network.interceptAsError', { requestId: this._requestId, reason }).catch(error => {
|
await this._session.send('Network.interceptAsError', { requestId: this._requestId, reason }).catch(error => {
|
||||||
// In certain cases, protocol will return error if the request was already canceled
|
// In certain cases, protocol will return error if the request was already canceled
|
||||||
// or the page was closed. We should tolerate these errors.
|
// or the page was closed. We should tolerate these errors.
|
||||||
debugError(error);
|
logError(this.request._page);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +93,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
// In certain cases, protocol will return error if the request was already canceled
|
// In certain cases, protocol will return error if the request was already canceled
|
||||||
// or the page was closed. We should tolerate these errors.
|
// or the page was closed. We should tolerate these errors.
|
||||||
debugError(error);
|
logError(this.request._page);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +107,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
|
||||||
}).catch((error: Error) => {
|
}).catch((error: Error) => {
|
||||||
// In certain cases, protocol will return error if the request was already canceled
|
// In certain cases, protocol will return error if the request was already canceled
|
||||||
// or the page was closed. We should tolerate these errors.
|
// or the page was closed. We should tolerate these errors.
|
||||||
debugError(error);
|
logError(this.request._page);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { debugError, helper, RegisteredListener, assert } from '../helper';
|
import { helper, RegisteredListener, assert } from '../helper';
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import { WKSession } from './wkConnection';
|
import { WKSession } from './wkConnection';
|
||||||
|
|
@ -37,6 +37,7 @@ import { selectors } from '../selectors';
|
||||||
import * as jpeg from 'jpeg-js';
|
import * as jpeg from 'jpeg-js';
|
||||||
import * as png from 'pngjs';
|
import * as png from 'pngjs';
|
||||||
import { NotConnectedError } from '../errors';
|
import { NotConnectedError } from '../errors';
|
||||||
|
import { logError } from '../logger';
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
|
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
|
||||||
|
|
@ -267,7 +268,7 @@ export class WKPage implements PageDelegate {
|
||||||
pageOrError = e;
|
pageOrError = e;
|
||||||
}
|
}
|
||||||
if (targetInfo.isPaused)
|
if (targetInfo.isPaused)
|
||||||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
|
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(logError(this._page));
|
||||||
if ((pageOrError instanceof Page) && this._page.mainFrame().url() === '') {
|
if ((pageOrError instanceof Page) && this._page.mainFrame().url() === '') {
|
||||||
try {
|
try {
|
||||||
// Initial empty page has an empty url. We should wait until the first real url has been loaded,
|
// Initial empty page has an empty url. We should wait until the first real url has been loaded,
|
||||||
|
|
@ -289,7 +290,7 @@ export class WKPage implements PageDelegate {
|
||||||
this._provisionalPage = new WKProvisionalPage(session, this);
|
this._provisionalPage = new WKProvisionalPage(session, this);
|
||||||
if (targetInfo.isPaused) {
|
if (targetInfo.isPaused) {
|
||||||
this._provisionalPage.initializationPromise.then(() => {
|
this._provisionalPage.initializationPromise.then(() => {
|
||||||
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(debugError);
|
this._pageProxySession.send('Target.resume', { targetId: targetInfo.targetId }).catch(logError(this._page));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -344,7 +345,7 @@ export class WKPage implements PageDelegate {
|
||||||
// as well to always be in sync with the backend.
|
// as well to always be in sync with the backend.
|
||||||
if (this._provisionalPage)
|
if (this._provisionalPage)
|
||||||
sessions.push(this._provisionalPage._session);
|
sessions.push(this._provisionalPage._session);
|
||||||
await Promise.all(sessions.map(session => callback(session).catch(debugError)));
|
await Promise.all(sessions.map(session => callback(session).catch(logError(this._page))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onFrameScheduledNavigation(frameId: string) {
|
private _onFrameScheduledNavigation(frameId: string) {
|
||||||
|
|
@ -608,7 +609,7 @@ export class WKPage implements PageDelegate {
|
||||||
|
|
||||||
private async _evaluateBindingScript(binding: PageBinding): Promise<void> {
|
private async _evaluateBindingScript(binding: PageBinding): Promise<void> {
|
||||||
const script = this._bindingToScript(binding);
|
const script = this._bindingToScript(binding);
|
||||||
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError)));
|
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(logError(this._page))));
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateOnNewDocument(script: string): Promise<void> {
|
async evaluateOnNewDocument(script: string): Promise<void> {
|
||||||
|
|
@ -639,7 +640,7 @@ export class WKPage implements PageDelegate {
|
||||||
this._pageProxySession.send('Target.close', {
|
this._pageProxySession.send('Target.close', {
|
||||||
targetId: this._session.sessionId,
|
targetId: this._session.sessionId,
|
||||||
runBeforeUnload
|
runBeforeUnload
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
}
|
}
|
||||||
|
|
||||||
canScreenshotOutsideViewport(): boolean {
|
canScreenshotOutsideViewport(): boolean {
|
||||||
|
|
@ -725,7 +726,7 @@ export class WKPage implements PageDelegate {
|
||||||
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
|
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
|
||||||
const result = await this._session.send('DOM.getContentQuads', {
|
const result = await this._session.send('DOM.getContentQuads', {
|
||||||
objectId: toRemoteObject(handle).objectId!
|
objectId: toRemoteObject(handle).objectId!
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
if (!result)
|
if (!result)
|
||||||
return null;
|
return null;
|
||||||
return result.quads.map(quad => [
|
return result.quads.map(quad => [
|
||||||
|
|
@ -749,7 +750,7 @@ export class WKPage implements PageDelegate {
|
||||||
const result = await this._session.send('DOM.resolveNode', {
|
const result = await this._session.send('DOM.resolveNode', {
|
||||||
objectId: toRemoteObject(handle).objectId,
|
objectId: toRemoteObject(handle).objectId,
|
||||||
executionContextId: (to._delegate as WKExecutionContext)._contextId
|
executionContextId: (to._delegate as WKExecutionContext)._contextId
|
||||||
}).catch(debugError);
|
}).catch(logError(this._page));
|
||||||
if (!result || result.object.subtype === 'null')
|
if (!result || result.object.subtype === 'null')
|
||||||
throw new Error('Unable to adopt element handle from a different document');
|
throw new Error('Unable to adopt element handle from a different document');
|
||||||
return to._createHandle(result.object) as dom.ElementHandle<T>;
|
return to._createHandle(result.object) as dom.ElementHandle<T>;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export class WKWorkers {
|
||||||
this.clear();
|
this.clear();
|
||||||
this._sessionListeners = [
|
this._sessionListeners = [
|
||||||
helper.addEventListener(session, 'Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => {
|
helper.addEventListener(session, 'Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => {
|
||||||
const worker = new Worker(event.url);
|
const worker = new Worker(this._page, event.url);
|
||||||
const workerSession = new WKSession(session.connection, event.workerId, 'Most likely the worker has been closed.', (message: any) => {
|
const workerSession = new WKSession(session.connection, event.workerId, 'Most likely the worker has been closed.', (message: any) => {
|
||||||
session.send('Worker.sendMessageToWorker', {
|
session.send('Worker.sendMessageToWorker', {
|
||||||
workerId: event.workerId,
|
workerId: event.workerId,
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,6 @@ async function testSignal(state, action, exitOnClose) {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Fixtures', function() {
|
describe('Fixtures', function() {
|
||||||
it.slow()('should dump browser process stderr', async state => {
|
|
||||||
let dumpioData = '';
|
|
||||||
const res = spawn('node', [path.join(__dirname, 'fixtures', 'dumpio.js'), state.playwrightPath, state.browserType.name()]);
|
|
||||||
res.stdout.on('data', data => dumpioData += data.toString('utf8'));
|
|
||||||
await new Promise(resolve => res.on('close', resolve));
|
|
||||||
expect(dumpioData).toContain('message from dumpio');
|
|
||||||
});
|
|
||||||
it.slow()('should close the browser when the node process closes', async state => {
|
it.slow()('should close the browser when the node process closes', async state => {
|
||||||
const result = await testSignal(state, child => {
|
const result = await testSignal(state, child => {
|
||||||
if (WIN)
|
if (WIN)
|
||||||
|
|
|
||||||
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) {
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
48
test/test.js
48
test/test.js
|
|
@ -103,43 +103,41 @@ function collect(browserNames) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const browserEnvironment = new Environment(browserName);
|
const browserEnvironment = new Environment(browserName);
|
||||||
|
let logger;
|
||||||
browserEnvironment.beforeAll(async state => {
|
browserEnvironment.beforeAll(async state => {
|
||||||
state.browser = await state.browserType.launch(launchOptions);
|
state.browser = await state.browserType.launch({...launchOptions, loggerSink: {
|
||||||
state._stdout = readline.createInterface({ input: state.browser._ownedServer.process().stdout });
|
isEnabled: (name, severity) => {
|
||||||
state._stderr = readline.createInterface({ input: state.browser._ownedServer.process().stderr });
|
return name === 'browser' ||
|
||||||
|
(name === 'protocol' && config.dumpProtocolOnFailure);
|
||||||
|
},
|
||||||
|
log: (name, severity, message, args) => {
|
||||||
|
if (logger)
|
||||||
|
logger(name, severity, message);
|
||||||
|
}
|
||||||
|
}});
|
||||||
});
|
});
|
||||||
browserEnvironment.afterAll(async state => {
|
browserEnvironment.afterAll(async state => {
|
||||||
await state.browser.close();
|
await state.browser.close();
|
||||||
delete state.browser;
|
delete state.browser;
|
||||||
state._stdout.close();
|
|
||||||
state._stderr.close();
|
|
||||||
delete state._stdout;
|
|
||||||
delete state._stderr;
|
|
||||||
});
|
});
|
||||||
browserEnvironment.beforeEach(async(state, testRun) => {
|
browserEnvironment.beforeEach(async(state, testRun) => {
|
||||||
const dumpout = data => testRun.log(`\x1b[33m[pw:stdio:out]\x1b[0m ${data}`);
|
logger = (name, severity, message) => {
|
||||||
const dumperr = data => testRun.log(`\x1b[31m[pw:stdio:err]\x1b[0m ${data}`);
|
if (name === 'browser') {
|
||||||
state._stdout.on('line', dumpout);
|
if (severity === 'warning')
|
||||||
state._stderr.on('line', dumperr);
|
testRun.log(`\x1b[31m[browser]\x1b[0m ${message}`)
|
||||||
// TODO: figure out debug options.
|
else
|
||||||
if (config.dumpProtocolOnFailure) {
|
testRun.log(`\x1b[33m[browser]\x1b[0m ${message}`)
|
||||||
state.browser._debugProtocol.log = data => testRun.log(`\x1b[32m[pw:protocol]\x1b[0m ${data}`);
|
} else if (name === 'protocol' && config.dumpProtocolOnFailure) {
|
||||||
state.browser._debugProtocol.enabled = true;
|
testRun.log(`\x1b[32m[protocol]\x1b[0m ${message}`)
|
||||||
}
|
}
|
||||||
state._browserTearDown = async (testRun) => {
|
}
|
||||||
state._stdout.off('line', dumpout);
|
});
|
||||||
state._stderr.off('line', dumperr);
|
browserEnvironment.afterEach(async (state, testRun) => {
|
||||||
|
logger = null;
|
||||||
if (config.dumpProtocolOnFailure) {
|
if (config.dumpProtocolOnFailure) {
|
||||||
delete state.browser._debugProtocol.log;
|
|
||||||
state.browser._debugProtocol.enabled = false;
|
|
||||||
if (testRun.ok())
|
if (testRun.ok())
|
||||||
testRun.output().splice(0);
|
testRun.output().splice(0);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
});
|
|
||||||
browserEnvironment.afterEach(async (state, testRun) => {
|
|
||||||
await state._browserTearDown(testRun);
|
|
||||||
delete state._browserTearDown;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageEnvironment = new Environment('Page');
|
const pageEnvironment = new Environment('Page');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue