Skip to content

Commit

Permalink
fix(api): make pipe connection the default, expose webSocket launch o…
Browse files Browse the repository at this point in the history
…ption (#562)
  • Loading branch information
dgozman committed Jan 24, 2020
1 parent b4b81ba commit 056fbbd
Show file tree
Hide file tree
Showing 18 changed files with 182 additions and 153 deletions.
22 changes: 11 additions & 11 deletions docs/api.md
Expand Up @@ -132,7 +132,7 @@ An example of launching a browser executable and connecting to a [Browser] later
const playwright = require('playwright').webkit; // Or 'chromium' or 'firefox'.

(async () => {
const browserApp = await playwright.launchBrowserApp();
const browserApp = await playwright.launchBrowserApp({ webSocket: true });
const connectOptions = browserApp.connectOptions();
// Use connect options later to establish a connection.
const browser = await playwright.connect(connectOptions);
Expand Down Expand Up @@ -229,7 +229,7 @@ Closes the browser gracefully and makes sure the process is terminated.

#### browserApp.connectOptions()
- returns: <[Object]>
- `browserWSEndpoint` <?[string]> a [browser websocket endpoint](#browserwsendpoint) to connect to.
- `browserWSEndpoint` <?[string]> a browser websocket endpoint to connect to.
- `slowMo` <[number]>
- `transport` <[ConnectionTransport]> **Experimental** A custom transport object which should be used to connect.

Expand All @@ -243,8 +243,6 @@ This options object can be passed to [chromiumPlaywright.connect(options)](#chro

Browser websocket endpoint which can be used as an argument to [chromiumPlaywright.connect(options)](#chromiumplaywrightconnectoptions), [firefoxPlaywright.connect(options)](#firefoxplaywrightconnectoptions) or [webkitPlaywright.connect(options)](#webkitplaywrightconnectoptions) to establish connection to the browser.

Learn more about [Chromium devtools protocol](https://chromedevtools.github.io/devtools-protocol) and the [browser endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target).

### class: BrowserContext

* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
Expand Down Expand Up @@ -3294,7 +3292,7 @@ If the function passed to the `worker.evaluateHandle` returns a [Promise], then

#### chromiumPlaywright.connect(options)
- `options` <[Object]>
- `browserWSEndpoint` <?[string]> a [browser websocket endpoint](#browserwsendpoint) to connect to.
- `browserWSEndpoint` <?[string]> a browser websocket endpoint to connect to.
- `browserURL` <?[string]> a browser url to connect to, in format `http://${host}:${port}`. Use interchangeably with `browserWSEndpoint` to let Playwright fetch it from [metadata endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target).
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
- `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Playwright to use.
Expand Down Expand Up @@ -3327,7 +3325,7 @@ The default flags that Chromium will be launched with.
- `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md).
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
- `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`.
- `webSocket` <[boolean]> Connects to the browser over a WebSocket instead of a pipe. Defaults to `false`.
- returns: <[Promise]<[ChromiumBrowser]>> Promise which resolves to browser instance.


Expand Down Expand Up @@ -3361,7 +3359,7 @@ const browser = await playwright.launch({
- `userDataDir` <[string]> Path to a [User Data Directory](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md).
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `devtools` <[boolean]> Whether to auto-open a DevTools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
- `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`.
- `webSocket` <[boolean]> Connects to the browser over a WebSocket instead of a pipe. Defaults to `false`.
- returns: <[Promise]<[BrowserApp]>> Promise which resolves to browser server instance.

### class: ChromiumBrowser
Expand Down Expand Up @@ -3554,7 +3552,7 @@ Identifies what kind of target this is. Can be `"page"`, [`"background_page"`](h

#### firefoxPlaywright.connect(options)
- `options` <[Object]>
- `browserWSEndpoint` <?[string]> a [browser websocket endpoint](#browserwsendpoint) to connect to.
- `browserWSEndpoint` <?[string]> a browser websocket endpoint to connect to.
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
- `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Playwright to use.
- returns: <[Promise]<[FirefoxBrowser]>>
Expand Down Expand Up @@ -3584,6 +3582,7 @@ The default flags that Firefox will be launched with.
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
- `userDataDir` <[string]> Path to a [User Data Directory](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile).
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `webSocket` <[boolean]> Connects to the browser over a WebSocket instead of a pipe. Defaults to `false`.
- returns: <[Promise]<[FirefoxBrowser]>> Promise which resolves to browser instance.


Expand All @@ -3608,6 +3607,7 @@ const browser = await playwright.launch({
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
- `userDataDir` <[string]> Path to a [User Data Directory](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile).
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `webSocket` <[boolean]> Connects to the browser over a WebSocket instead of a pipe. Defaults to `false`.
- returns: <[Promise]<[BrowserApp]>> Promise which resolves to browser server instance.

### class: FirefoxBrowser
Expand All @@ -3630,7 +3630,7 @@ Firefox browser instance does not expose Firefox-specific features.

#### webkitPlaywright.connect(options)
- `options` <[Object]>
- `browserWSEndpoint` <?[string]> a [browser websocket endpoint](#browserwsendpoint) to connect to.
- `browserWSEndpoint` <?[string]> a browser websocket endpoint to connect to.
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
- `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Playwright to use.
- returns: <[Promise]<[WebKitBrowser]>>
Expand Down Expand Up @@ -3660,7 +3660,7 @@ The default flags that WebKit will be launched with.
- `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`.
- `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`.
- `webSocket` <[boolean]> Connects to the browser over a WebSocket instead of a pipe. Defaults to `false`.
- returns: <[Promise]<[WebKitBrowser]>> Promise which resolves to browser instance.


Expand All @@ -3685,7 +3685,7 @@ const browser = await playwright.launch({
- `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`.
- `pipe` <[boolean]> Connects to the browser over a pipe instead of a WebSocket. Defaults to `false`.
- `webSocket` <[boolean]> Connects to the browser over a WebSocket instead of a pipe. Defaults to `false`.
- returns: <[Promise]<[BrowserApp]>> Promise which resolves to browser server instance.

### class: WebKitBrowser
Expand Down
6 changes: 6 additions & 0 deletions src/chromium/crConnection.ts
Expand Up @@ -26,6 +26,10 @@ export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected')
};

// CRPlaywright uses this special id to issue Browser.close command which we
// should ignore.
export const kBrowserCloseMessageId = -9999;

export class CRConnection extends platform.EventEmitter {
private _lastId = 0;
private _transport: ConnectionTransport;
Expand Down Expand Up @@ -64,6 +68,8 @@ export class CRConnection extends platform.EventEmitter {
async _onMessage(message: string) {
debugProtocol('◀ RECV ' + message);
const object = JSON.parse(message);
if (object.id === kBrowserCloseMessageId)
return;
if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId;
const session = new CRSession(this, object.params.targetInfo.type, sessionId);
Expand Down
6 changes: 6 additions & 0 deletions src/firefox/ffConnection.ts
Expand Up @@ -26,6 +26,10 @@ export const ConnectionEvents = {
Disconnected: Symbol('Disconnected'),
};

// FFPlaywright uses this special id to issue Browser.close command which we
// should ignore.
export const kBrowserCloseMessageId = -9999;

export class FFConnection extends platform.EventEmitter {
private _lastId: number;
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
Expand Down Expand Up @@ -89,6 +93,8 @@ export class FFConnection extends platform.EventEmitter {
async _onMessage(message: string) {
debugProtocol('◀ RECV ' + message);
const object = JSON.parse(message);
if (object.id === kBrowserCloseMessageId)
return;
if (object.method === 'Target.attachedToTarget') {
const sessionId = object.params.sessionId;
const session = new FFSession(this, object.params.targetInfo.type, sessionId, message => this._rawSend({...message, sessionId}));
Expand Down
18 changes: 11 additions & 7 deletions src/server/crPlaywright.ts
Expand Up @@ -27,7 +27,7 @@ import { CRBrowser } from '../chromium/crBrowser';
import * as platform from '../platform';
import { TimeoutError } from '../errors';
import { launchProcess, waitForLine } from '../server/processLauncher';
import { CRConnection } from '../chromium/crConnection';
import { kBrowserCloseMessageId } from '../chromium/crConnection';
import { PipeTransport } from './pipeTransport';
import { Playwright } from './playwright';
import { createTransport, ConnectOptions } from '../browser';
Expand All @@ -53,7 +53,7 @@ export type LaunchOptions = ChromiumArgOptions & SlowMoOptions & {
timeout?: number,
dumpio?: boolean,
env?: {[key: string]: string} | undefined,
pipe?: boolean,
webSocket?: boolean,
};

export class CRPlaywright implements Playwright {
Expand All @@ -79,7 +79,7 @@ export class CRPlaywright implements Playwright {
args = [],
dumpio = false,
executablePath = null,
pipe = false,
webSocket = false,
env = process.env,
handleSIGINT = true,
handleSIGTERM = true,
Expand All @@ -99,7 +99,7 @@ export class CRPlaywright implements Playwright {
let temporaryUserDataDir: string | null = null;

if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
chromeArguments.push(webSocket ? '--remote-debugging-port=0' : '--remote-debugging-pipe');
if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) {
temporaryUserDataDir = await mkdtempAsync(CHROMIUM_PROFILE_PATH);
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
Expand All @@ -114,6 +114,8 @@ export class CRPlaywright implements Playwright {
}

const usePipe = chromeArguments.includes('--remote-debugging-pipe');
if (usePipe && webSocket)
throw new Error(`Argument "--remote-debugging-pipe" is not compatible with "webSocket" launch option.`);

const { launchedProcess, gracefullyClose } = await launchProcess({
executablePath: chromeExecutable!,
Expand All @@ -131,10 +133,10 @@ export class CRPlaywright implements Playwright {

// We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since
// our connection is tolerant to unknown responses.
// our connection ignores kBrowserCloseMessageId.
const transport = await createTransport(connectOptions);
const connection = new CRConnection(transport);
connection.rootSession.send('Browser.close');
const message = { method: 'Browser.close', id: kBrowserCloseMessageId };
transport.send(JSON.stringify(message));
},
});

Expand All @@ -152,6 +154,8 @@ export class CRPlaywright implements Playwright {
}

async connect(options: ConnectOptions & { browserURL?: string }): Promise<CRBrowser> {
if (options.transport && options.transport.onmessage)
throw new Error('Transport is already in use');
if (options.browserURL) {
assert(!options.browserWSEndpoint && !options.transport, 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to connect');
let connectionURL: string;
Expand Down
23 changes: 16 additions & 7 deletions src/server/ffPlaywright.ts
Expand Up @@ -21,7 +21,7 @@ import { DeviceDescriptors } from '../deviceDescriptors';
import { launchProcess, waitForLine } from './processLauncher';
import * as types from '../types';
import * as platform from '../platform';
import { FFConnection } from '../firefox/ffConnection';
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -51,6 +51,7 @@ export type LaunchOptions = FirefoxArgOptions & SlowMoOptions & {
timeout?: number,
dumpio?: boolean,
env?: {[key: string]: string} | undefined,
webSocket?: boolean,
};

export class FFPlaywright implements Playwright {
Expand Down Expand Up @@ -82,6 +83,7 @@ export class FFPlaywright implements Playwright {
handleSIGTERM = true,
slowMo = 0,
timeout = 30000,
webSocket = false,
} = options;

const firefoxArguments = [];
Expand Down Expand Up @@ -129,22 +131,29 @@ export class FFPlaywright implements Playwright {
if (!connectOptions)
return Promise.reject();
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that we don't support pipe yet, so there is no issue
// with reusing the same connection - we can always create a new one.
// Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId.
const transport = await createTransport(connectOptions);
const connection = new FFConnection(transport);
connection.send('Browser.close');
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
transport.send(JSON.stringify(message));
},
});

const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`);
const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
const url = match[1];
connectOptions = { browserWSEndpoint: url, slowMo };
const browserWSEndpoint = match[1];
if (webSocket) {
connectOptions = { browserWSEndpoint, slowMo };
} else {
const transport = await platform.createWebSocketTransport(browserWSEndpoint);
connectOptions = { transport, slowMo };
}
return new BrowserApp(launchedProcess, gracefullyClose, connectOptions);
}

async connect(options: ConnectOptions): Promise<FFBrowser> {
if (options.transport && options.transport.onmessage)
throw new Error('Transport is already in use');
return FFBrowser.connect(options);
}

Expand Down
10 changes: 7 additions & 3 deletions src/server/wkPlaywright.ts
Expand Up @@ -56,7 +56,7 @@ export type LaunchOptions = WebKitArgOptions & SlowMoOptions & {
timeout?: number,
dumpio?: boolean,
env?: {[key: string]: string} | undefined,
pipe?: boolean,
webSocket?: boolean,
};

export class WKPlaywright implements Playwright {
Expand Down Expand Up @@ -87,7 +87,7 @@ export class WKPlaywright implements Playwright {
handleSIGTERM = true,
handleSIGHUP = true,
slowMo = 0,
pipe = false,
webSocket = false,
} = options;

const webkitArguments = [];
Expand Down Expand Up @@ -132,6 +132,8 @@ export class WKPlaywright implements Playwright {
if (!transport)
return Promise.reject();
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId.
const message = JSON.stringify({method: 'Browser.close', params: {}, id: kBrowserCloseMessageId});
transport.send(message);
},
Expand All @@ -140,7 +142,7 @@ export class WKPlaywright implements Playwright {
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);

let connectOptions: ConnectOptions;
if (!pipe) {
if (webSocket) {
const browserWSEndpoint = wrapTransportWithWebSocket(transport);
connectOptions = { browserWSEndpoint, slowMo };
} else {
Expand All @@ -150,6 +152,8 @@ export class WKPlaywright implements Playwright {
}

async connect(options: ConnectOptions): Promise<WKBrowser> {
if (options.transport && options.transport.onmessage)
throw new Error('Transport is already in use');
return WKBrowser.connect(options);
}

Expand Down
1 change: 1 addition & 0 deletions test/chromium/browser.spec.js
Expand Up @@ -39,6 +39,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
const options = Object.assign({}, defaultBrowserOptions, {
// Disable DUMPIO to cleanly read stdout.
dumpio: false,
webSocket: true,
});
const res = spawn('node', [path.join(__dirname, '..', 'fixtures', 'closeme.js'), playwrightPath, product, JSON.stringify(options)]);
let wsEndPointCallback;
Expand Down
14 changes: 0 additions & 14 deletions test/chromium/chromium.spec.js
Expand Up @@ -21,20 +21,6 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
const {it, fit, xit, dit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;

describe('Chromium', function() {
it('should work across sessions', async function({browserApp, server, browser, newContext}) {
expect(browser.browserContexts().length).toBe(2);
await newContext();
expect(browser.browserContexts().length).toBe(3);
const remoteBrowser = await playwright.connect({
browserWSEndpoint: browserApp.wsEndpoint()
});
const contexts = remoteBrowser.browserContexts();
expect(contexts.length).toBe(3);
remoteBrowser.disconnect();
});
});

describe('Target', function() {
it('Chromium.targets should return all of the targets', async({page, server, browser}) => {
// The pages will be the testing page and the original newtab page
Expand Down

0 comments on commit 056fbbd

Please sign in to comment.