From 68e3997e2adac6a9389f68fd57d639ac88c9e6fc Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 23 Jan 2020 17:02:52 -0800 Subject: [PATCH] fix(api): make pipe connection the default, expose webSocket launch option --- docs/api.md | 22 ++++---- src/chromium/crConnection.ts | 6 ++ src/firefox/ffConnection.ts | 6 ++ src/server/crPlaywright.ts | 18 +++--- src/server/ffPlaywright.ts | 23 +++++--- src/server/wkPlaywright.ts | 10 +++- test/chromium/browser.spec.js | 1 + test/chromium/chromium.spec.js | 14 ----- test/chromium/connect.spec.js | 29 ++++++---- test/chromium/launcher.spec.js | 71 +++++++++++++----------- test/firefox/browser.spec.js | 1 + test/fixtures.spec.js | 4 +- test/fixtures/dumpio.js | 6 +- test/launcher.spec.js | 73 +++++++++++++++++++------ test/playwright.spec.js | 4 +- test/web.spec.js | 2 +- test/webkit/launcher.spec.js | 43 +-------------- utils/protocol-types-generator/index.js | 2 +- 18 files changed, 182 insertions(+), 153 deletions(-) diff --git a/docs/api.md b/docs/api.md index a589cb859b040..c51867ef0d812 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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); @@ -229,7 +229,7 @@ Closes the browser gracefully and makes sure the process is terminated. #### browserApp.connectOptions() - returns: <[Object]> - - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserWSEndpoint` a browser websocket endpoint to connect to. - `slowMo` <[number]> - `transport` <[ConnectionTransport]> **Experimental** A custom transport object which should be used to connect. @@ -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) @@ -3294,7 +3292,7 @@ If the function passed to the `worker.evaluateHandle` returns a [Promise], then #### chromiumPlaywright.connect(options) - `options` <[Object]> - - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserWSEndpoint` a browser websocket endpoint to connect to. - `browserURL` 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. @@ -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. @@ -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 @@ -3554,7 +3552,7 @@ Identifies what kind of target this is. Can be `"page"`, [`"background_page"`](h #### firefoxPlaywright.connect(options) - `options` <[Object]> - - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserWSEndpoint` 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]>> @@ -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. @@ -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 @@ -3630,7 +3630,7 @@ Firefox browser instance does not expose Firefox-specific features. #### webkitPlaywright.connect(options) - `options` <[Object]> - - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. + - `browserWSEndpoint` 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]>> @@ -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. @@ -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 diff --git a/src/chromium/crConnection.ts b/src/chromium/crConnection.ts index a23c67e2e08c2..55aae2ccfd5a8 100644 --- a/src/chromium/crConnection.ts +++ b/src/chromium/crConnection.ts @@ -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; @@ -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); diff --git a/src/firefox/ffConnection.ts b/src/firefox/ffConnection.ts index 57b7cc408d034..a6da12347e9df 100644 --- a/src/firefox/ffConnection.ts +++ b/src/firefox/ffConnection.ts @@ -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; @@ -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})); diff --git a/src/server/crPlaywright.ts b/src/server/crPlaywright.ts index 5de39114aed3a..4c10b511aae45 100644 --- a/src/server/crPlaywright.ts +++ b/src/server/crPlaywright.ts @@ -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'; @@ -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 { @@ -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, @@ -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}`); @@ -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!, @@ -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)); }, }); @@ -152,6 +154,8 @@ export class CRPlaywright implements Playwright { } async connect(options: ConnectOptions & { browserURL?: string }): Promise { + 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; diff --git a/src/server/ffPlaywright.ts b/src/server/ffPlaywright.ts index 6dba2b2e78d9e..fd633aa064860 100644 --- a/src/server/ffPlaywright.ts +++ b/src/server/ffPlaywright.ts @@ -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'; @@ -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 { @@ -82,6 +83,7 @@ export class FFPlaywright implements Playwright { handleSIGTERM = true, slowMo = 0, timeout = 30000, + webSocket = false, } = options; const firefoxArguments = []; @@ -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 { + if (options.transport && options.transport.onmessage) + throw new Error('Transport is already in use'); return FFBrowser.connect(options); } diff --git a/src/server/wkPlaywright.ts b/src/server/wkPlaywright.ts index df6c0847cf049..f68901a43fcee 100644 --- a/src/server/wkPlaywright.ts +++ b/src/server/wkPlaywright.ts @@ -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 { @@ -87,7 +87,7 @@ export class WKPlaywright implements Playwright { handleSIGTERM = true, handleSIGHUP = true, slowMo = 0, - pipe = false, + webSocket = false, } = options; const webkitArguments = []; @@ -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); }, @@ -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 { @@ -150,6 +152,8 @@ export class WKPlaywright implements Playwright { } async connect(options: ConnectOptions): Promise { + if (options.transport && options.transport.onmessage) + throw new Error('Transport is already in use'); return WKBrowser.connect(options); } diff --git a/test/chromium/browser.spec.js b/test/chromium/browser.spec.js index b3d41a0a07a6e..343a6bde2834a 100644 --- a/test/chromium/browser.spec.js +++ b/test/chromium/browser.spec.js @@ -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; diff --git a/test/chromium/chromium.spec.js b/test/chromium/chromium.spec.js index 6b44399797668..aa66628de3a0b 100644 --- a/test/chromium/chromium.spec.js +++ b/test/chromium/chromium.spec.js @@ -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 diff --git a/test/chromium/connect.spec.js b/test/chromium/connect.spec.js index a0a52684a8a85..5076fd34fdbb2 100644 --- a/test/chromium/connect.spec.js +++ b/test/chromium/connect.spec.js @@ -24,12 +24,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('Playwright.connect', function() { it('should be able to connect multiple times to the same browser', async({server}) => { - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); const local = await playwright.connect(browserApp.connectOptions()); - const remote = await playwright.connect({ - ...defaultBrowserOptions, - browserWSEndpoint: browserApp.wsEndpoint() - }); + const remote = await playwright.connect(browserApp.connectOptions()); const page = await remote.defaultContext().newPage(); expect(await page.evaluate(() => 7 * 8)).toBe(56); remote.disconnect(); @@ -38,13 +35,21 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work'); await browserApp.close(); }); - it('should be able to close remote browser', async({server}) => { + it('should not be able to connect multiple times without websocket', async({server}) => { const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); + const connectOptions = browserApp.connectOptions(); + expect(connectOptions.transport).toBeTruthy(); + expect(browserApp.wsEndpoint()).toBe(null); + expect(connectOptions.browserWSEndpoint).toBe(undefined); + const local = await playwright.connect(connectOptions); + const error = await playwright.connect(connectOptions).catch(e => e); + expect(error.message).toBe('Transport is already in use'); + await browserApp.close(); + }); + it('should be able to close remote browser', async({server}) => { + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); const local = await playwright.connect(browserApp.connectOptions()); - const remote = await playwright.connect({ - ...defaultBrowserOptions, - browserWSEndpoint: browserApp.wsEndpoint() - }); + const remote = await playwright.connect(browserApp.connectOptions()); await Promise.all([ utils.waitEvent(local, 'disconnected'), remote.close(), @@ -52,9 +57,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); // @see https://github.com/GoogleChrome/puppeteer/issues/4197#issuecomment-481793410 it('should be able to connect to the same page simultaneously', async({server}) => { - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); const local = await playwright.connect(browserApp.connectOptions()); - const remote = await playwright.connect({ ...defaultBrowserOptions, browserWSEndpoint: browserApp.wsEndpoint() }); + const remote = await playwright.connect(browserApp.connectOptions()); const [page1, page2] = await Promise.all([ new Promise(x => local.once('targetcreated', target => x(target.page()))), remote.defaultContext().newPage(), diff --git a/test/chromium/launcher.spec.js b/test/chromium/launcher.spec.js index 25a079a7c14a9..654de57aec995 100644 --- a/test/chromium/launcher.spec.js +++ b/test/chromium/launcher.spec.js @@ -32,12 +32,28 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; describe('CrPlaywright', function() { + describe('BrowserContext', function() { + it('should work across sessions', async () => { + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const browser = await playwright.connect(browserApp.connectOptions()); + expect(browser.browserContexts().length).toBe(1); + await browser.newContext(); + expect(browser.browserContexts().length).toBe(2); + const remoteBrowser = await playwright.connect(browserApp.connectOptions()); + const contexts = remoteBrowser.browserContexts(); + expect(contexts.length).toBe(2); + await browserApp.close(); + }); + }); describe('Playwright.launch |browserURL| option', function() { + function getBrowserUrl(wsEndpoint) { + const port = wsEndpoint.match(/ws:\/\/([0-9A-Za-z\.]*):(\d+)\//)[2]; + return `http://127.0.0.1:${port}`; + } + it('should be able to connect using browserUrl, with and without trailing slash', async({server}) => { - const originalBrowser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { - args: ['--remote-debugging-port=21222'] - })); - const browserURL = 'http://127.0.0.1:21222'; + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const browserURL = getBrowserUrl(browserApp.wsEndpoint()); const browser1 = await playwright.connect({browserURL}); const page1 = await browser1.defaultContext().newPage(); @@ -48,46 +64,42 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const page2 = await browser2.defaultContext().newPage(); expect(await page2.evaluate(() => 8 * 7)).toBe(56); browser2.disconnect(); - originalBrowser.close(); + await browserApp.close(); }); it('should throw when using both browserWSEndpoint and browserURL', async({server}) => { - const browserApp = await playwright.launchBrowserApp(Object.assign({}, defaultBrowserOptions, { - args: ['--remote-debugging-port=21222'] - })); - const browserURL = 'http://127.0.0.1:21222'; + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const browserURL = getBrowserUrl(browserApp.wsEndpoint()); let error = null; await playwright.connect({browserURL, browserWSEndpoint: browserApp.wsEndpoint()}).catch(e => error = e); expect(error.message).toContain('Exactly one of browserWSEndpoint, browserURL or transport'); - browserApp.close(); + await browserApp.close(); }); it('should throw when trying to connect to non-existing browser', async({server}) => { - const originalBrowser = await playwright.launch(Object.assign({}, defaultBrowserOptions, { - args: ['--remote-debugging-port=21222'] - })); - const browserURL = 'http://127.0.0.1:32333'; + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const browserURL = getBrowserUrl(browserApp.wsEndpoint()); let error = null; - await playwright.connect({browserURL}).catch(e => error = e); + await playwright.connect({browserURL: browserURL + 'foo'}).catch(e => error = e); expect(error.message).toContain('Failed to fetch browser webSocket url from'); - originalBrowser.close(); + await browserApp.close(); }); }); - describe('Playwright.launch |pipe| option', function() { - it('should support the pipe option', async() => { - const options = Object.assign({pipe: true}, defaultBrowserOptions); + describe('Playwright.launch webSocket option', function() { + it('should support the remote-debugging-port argument', async() => { + const options = Object.assign({}, defaultBrowserOptions); + options.args = ['--remote-debugging-port=0'].concat(options.args || []); const browserApp = await playwright.launchBrowserApp(options); const browser = await playwright.connect(browserApp.connectOptions()); - expect((await browser.defaultContext().pages()).length).toBe(1); - expect(browserApp.wsEndpoint()).toBe(null); + expect(browserApp.wsEndpoint()).not.toBe(null); const page = await browser.defaultContext().newPage(); expect(await page.evaluate('11 * 11')).toBe(121); await page.close(); await browserApp.close(); }); - it('should support the pipe argument', async() => { + it('should support the remote-debugging-pipe argument', async() => { const options = Object.assign({}, defaultBrowserOptions); options.args = ['--remote-debugging-pipe'].concat(options.args || []); const browserApp = await playwright.launchBrowserApp(options); @@ -98,14 +110,11 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p await page.close(); await browserApp.close(); }); - it('should fire "disconnected" when closing with pipe', async() => { - const options = Object.assign({pipe: true}, defaultBrowserOptions); - const browserApp = await playwright.launchBrowserApp(options); - const browser = await playwright.connect(browserApp.connectOptions()); - const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve)); - // Emulate user exiting browser. - browserApp.process().kill(); - await disconnectedEventPromise; + it('should throw with remote-debugging-pipe argument and webSocket', async() => { + const options = Object.assign({webSocket: true}, defaultBrowserOptions); + options.args = ['--remote-debugging-pipe'].concat(options.args || []); + const error = await playwright.launchBrowserApp(options).catch(e => e); + expect(error.message).toBe('Argument "--remote-debugging-pipe" is not compatible with "webSocket" launch option.'); }); }); }); @@ -127,7 +136,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('Browser.Events.disconnected', function() { it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => { - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); const originalBrowser = await playwright.connect(browserApp.connectOptions()); const browserWSEndpoint = browserApp.wsEndpoint(); const remoteBrowser1 = await playwright.connect({browserWSEndpoint}); diff --git a/test/firefox/browser.spec.js b/test/firefox/browser.spec.js index 4f37deb43b4e2..82c4ae75f27b0 100644 --- a/test/firefox/browser.spec.js +++ b/test/firefox/browser.spec.js @@ -27,6 +27,7 @@ module.exports.describe = function({testRunner, defaultBrowserOptions, playwrigh 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; diff --git a/test/fixtures.spec.js b/test/fixtures.spec.js index 4256c08c1bcde..df3601fd07877 100644 --- a/test/fixtures.spec.js +++ b/test/fixtures.spec.js @@ -24,9 +24,9 @@ module.exports.describe = function({testRunner, expect, product, playwrightPath, const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; describe('Fixtures', function() { - it('dumpio option should work with pipe option ', async({server}) => { + it('dumpio option should work with webSocket option', async({server}) => { let dumpioData = ''; - const res = spawn('node', [path.join(__dirname, 'fixtures', 'dumpio.js'), playwrightPath, product, 'use-pipe']); + const res = spawn('node', [path.join(__dirname, 'fixtures', 'dumpio.js'), playwrightPath, product, 'usewebsocket']); res.stderr.on('data', data => dumpioData += data.toString('utf8')); await new Promise(resolve => res.on('close', resolve)); expect(dumpioData).toContain('message from dumpio'); diff --git a/test/fixtures/dumpio.js b/test/fixtures/dumpio.js index abb8407ced787..b60cc783a0d63 100644 --- a/test/fixtures/dumpio.js +++ b/test/fixtures/dumpio.js @@ -4,16 +4,16 @@ console.log('unhandledRejection', error.message); }); - const [, , playwrightRoot, product, usePipe] = process.argv; + const [, , playwrightRoot, product, useWebSocket] = process.argv; const options = { - pipe: usePipe === 'use-pipe', + webSocket: useWebSocket === 'usewebsocket', ignoreDefaultArgs: true, dumpio: true, timeout: 1, executablePath: 'node', args: ['-e', 'console.error("message from dumpio")', '--'] } - console.error('using pipe: ' + options.pipe); + console.error('using web socket: ' + options.webSocket); if (product.toLowerCase() === 'firefox') options.args.push('-juggler', '-profile'); try { diff --git a/test/launcher.spec.js b/test/launcher.spec.js index d2a85e5b52714..2596de64911fd 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -90,17 +90,16 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('Browser.isConnected', () => { it('should set the browser connected state', async () => { - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); - const browserWSEndpoint = browserApp.wsEndpoint(); - const remote = await playwright.connect({browserWSEndpoint}); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const remote = await playwright.connect({browserWSEndpoint: browserApp.wsEndpoint()}); expect(remote.isConnected()).toBe(true); await remote.disconnect(); expect(remote.isConnected()).toBe(false); await browserApp.close(); }); it('should throw when used after isConnected returns false', async({server}) => { - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); - const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browserApp.wsEndpoint()}); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const remote = await playwright.connect(browserApp.connectOptions()); const page = await remote.defaultContext().newPage(); await Promise.all([ browserApp.close(), @@ -115,8 +114,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('Browser.disconnect', function() { it('should reject navigation when browser closes', async({server}) => { server.setRoute('/one-style.css', () => {}); - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); - const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browserApp.wsEndpoint()}); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const remote = await playwright.connect(browserApp.connectOptions()); const page = await remote.defaultContext().newPage(); const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e); await server.waitForRequest('/one-style.css'); @@ -127,8 +126,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); it('should reject waitForSelector when browser closes', async({server}) => { server.setRoute('/empty.html', () => {}); - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); - const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browserApp.wsEndpoint()}); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const remote = await playwright.connect(browserApp.connectOptions()); const page = await remote.defaultContext().newPage(); const watchdog = page.waitForSelector('div', { timeout: 60000 }).catch(e => e); await remote.disconnect(); @@ -137,8 +136,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p await browserApp.close(); }); it('should throw if used after disconnect', async({server}) => { - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); - const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browserApp.wsEndpoint()}); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const remote = await playwright.connect(browserApp.connectOptions()); const page = await remote.defaultContext().newPage(); await remote.disconnect(); const error = await page.evaluate('1 + 1').catch(e => e); @@ -149,8 +148,8 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('Browser.close', function() { it('should terminate network waiters', async({context, server}) => { - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); - const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browserApp.wsEndpoint()}); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); + const remote = await playwright.connect(browserApp.connectOptions()); const newPage = await remote.defaultContext().newPage(); const results = await Promise.all([ newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e), @@ -165,16 +164,56 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); }); + describe('Playwright.launch |webSocket| option', function() { + it('should not have websocket by default', async() => { + const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); + const browser = await playwright.connect(browserApp.connectOptions()); + expect((await browser.defaultContext().pages()).length).toBe(1); + expect(browserApp.wsEndpoint()).toBe(null); + const page = await browser.defaultContext().newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browserApp.close(); + }); + it('should support the webSocket option', async() => { + const options = Object.assign({webSocket: true}, defaultBrowserOptions); + const browserApp = await playwright.launchBrowserApp(options); + const browser = await playwright.connect(browserApp.connectOptions()); + expect((await browser.defaultContext().pages()).length).toBe(1); + expect(browserApp.wsEndpoint()).not.toBe(null); + const page = await browser.defaultContext().newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + await browserApp.close(); + }); + it('should fire "disconnected" when closing without webSocket', async() => { + const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); + const browser = await playwright.connect(browserApp.connectOptions()); + const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve)); + // Emulate user exiting browser. + process.kill(-browserApp.process().pid, 'SIGKILL'); + await disconnectedEventPromise; + }); + it('should fire "disconnected" when closing with webSocket', async() => { + const options = Object.assign({webSocket: true}, defaultBrowserOptions); + const browserApp = await playwright.launchBrowserApp(options); + const browser = await playwright.connect(browserApp.connectOptions()); + const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve)); + // Emulate user exiting browser. + process.kill(-browserApp.process().pid, 'SIGKILL'); + await disconnectedEventPromise; + }); + }); + describe('Playwright.connect', function() { it.skip(WEBKIT)('should be able to reconnect to a browser', async({server}) => { - const browserApp = await playwright.launchBrowserApp(defaultBrowserOptions); + const browserApp = await playwright.launchBrowserApp({...defaultBrowserOptions, webSocket: true}); const browser = await playwright.connect(browserApp.connectOptions()); - const browserWSEndpoint = browserApp.wsEndpoint(); const page = await browser.defaultContext().newPage(); await page.goto(server.PREFIX + '/frames/nested-frames.html'); await browser.disconnect(); - const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint}); + const remote = await playwright.connect(browserApp.connectOptions()); const pages = await remote.defaultContext().pages(); const restoredPage = pages.find(page => page.url() === server.PREFIX + '/frames/nested-frames.html'); expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([ @@ -189,7 +228,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); }); - describe.skip(FFOX | WEBKIT)('Playwright.launch({userDataDir})', function() { + describe.skip(FFOX || WEBKIT)('Playwright.launch({userDataDir})', function() { it('userDataDir option', async({server}) => { const userDataDir = await mkdtempAsync(TMP_FOLDER); const options = Object.assign({userDataDir}, defaultBrowserOptions); diff --git a/test/playwright.spec.js b/test/playwright.spec.js index 2e8257a6816e1..ed8991963e3e0 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -105,7 +105,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { const onLine = (line) => test.output += line + '\n'; let rl; - if (!WEBKIT) { + if (state.browserApp.process().stderr) { rl = require('readline').createInterface({ input: state.browserApp.process().stderr }); test.output = ''; rl.on('line', onLine); @@ -113,7 +113,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { state.tearDown = async () => { await Promise.all(contexts.map(c => c.close())); - if (!WEBKIT) { + if (rl) { rl.removeListener('line', onLine); rl.close(); } diff --git a/test/web.spec.js b/test/web.spec.js index d0a30c056226b..7394c70b4e65f 100644 --- a/test/web.spec.js +++ b/test/web.spec.js @@ -21,7 +21,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p (CHROMIUM || FFOX) && describe('Web SDK', function() { beforeAll(async state => { - state.controlledBrowserApp = await playwright.launchBrowserApp({ ...defaultBrowserOptions, pipe: false }); + state.controlledBrowserApp = await playwright.launchBrowserApp({ ...defaultBrowserOptions, webSocket: true }); state.hostBrowserApp = await playwright.launchBrowserApp(defaultBrowserOptions); state.hostBrowser = await playwright.connect(state.hostBrowserApp.connectOptions()); }); diff --git a/test/webkit/launcher.spec.js b/test/webkit/launcher.spec.js index d1a302a896ee2..6675f9ecfdcc0 100644 --- a/test/webkit/launcher.spec.js +++ b/test/webkit/launcher.spec.js @@ -21,47 +21,6 @@ module.exports.describe = function ({ testRunner, expect, playwright, defaultBro const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; describe('WKPlaywright', function() { - describe('Playwright.launch |pipe| option', function() { - it('should have websocket by default', async() => { - const options = Object.assign({pipe: false}, defaultBrowserOptions); - const browserApp = await playwright.launchBrowserApp(options); - const browser = await playwright.connect(browserApp.connectOptions()); - expect((await browser.defaultContext().pages()).length).toBe(1); - expect(browserApp.wsEndpoint()).not.toBe(null); - const page = await browser.defaultContext().newPage(); - expect(await page.evaluate('11 * 11')).toBe(121); - await page.close(); - await browserApp.close(); - }); - it('should support the pipe option', async() => { - const options = Object.assign({pipe: true}, defaultBrowserOptions); - const browserApp = await playwright.launchBrowserApp(options); - const browser = await playwright.connect(browserApp.connectOptions()); - expect((await browser.defaultContext().pages()).length).toBe(1); - expect(browserApp.wsEndpoint()).toBe(null); - const page = await browser.defaultContext().newPage(); - expect(await page.evaluate('11 * 11')).toBe(121); - await page.close(); - await browserApp.close(); - }); - it('should fire "disconnected" when closing with pipe', async() => { - const options = Object.assign({pipe: true}, defaultBrowserOptions); - const browserApp = await playwright.launchBrowserApp(options); - const browser = await playwright.connect(browserApp.connectOptions()); - const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve)); - // Emulate user exiting browser. - process.kill(-browserApp.process().pid, 'SIGKILL'); - await disconnectedEventPromise; - }); - it('should fire "disconnected" when closing with websocket', async() => { - const options = Object.assign({pipe: false}, defaultBrowserOptions); - const browserApp = await playwright.launchBrowserApp(options); - const browser = await playwright.connect(browserApp.connectOptions()); - const disconnectedEventPromise = new Promise(resolve => browser.once('disconnected', resolve)); - // Emulate user exiting browser. - process.kill(-browserApp.process().pid, 'SIGKILL'); - await disconnectedEventPromise; - }); - }); + }); }; diff --git a/utils/protocol-types-generator/index.js b/utils/protocol-types-generator/index.js index 756236215548d..f2004d2ce9f69 100644 --- a/utils/protocol-types-generator/index.js +++ b/utils/protocol-types-generator/index.js @@ -10,7 +10,7 @@ async function generateChromiunProtocol(revision) { if (revision.local && fs.existsSync(outputPath)) return; const playwright = await require('../../index').chromium; - const browserApp = await playwright.launchBrowserApp({executablePath: revision.executablePath}); + const browserApp = await playwright.launchBrowserApp({executablePath: revision.executablePath, webSocket: true}); const origin = browserApp.wsEndpoint().match(/ws:\/\/([0-9A-Za-z:\.]*)\//)[1]; const browser = await playwright.connect(browserApp.connectOptions()); const page = await browser.defaultContext().newPage();