From a547aa7984a8e531d7351e95b2cbcfef6e4ea466 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 6 Feb 2020 12:41:43 -0800 Subject: [PATCH] feat(connect): allow multiple webkit connections over web socket (#863) --- docs/api.md | 14 +-- docs/web.md | 2 +- src/browser.ts | 1 - src/chromium/crBrowser.ts | 18 +--- src/firefox/ffBrowser.ts | 19 +--- src/platform.ts | 28 +++--- src/server/chromium.ts | 6 +- src/server/firefox.ts | 12 +-- src/server/webkit.ts | 164 +++++++++++++++++++++++++++++---- src/web.ts | 6 +- src/webkit/wkBrowser.ts | 9 +- test/assets/playwrightweb.html | 2 +- test/launcher.spec.js | 49 ++++------ test/multiclient.spec.js | 66 +++++++------ test/playwright.spec.js | 5 +- 15 files changed, 237 insertions(+), 164 deletions(-) diff --git a/docs/api.md b/docs/api.md index 6a7e5e3180..f67f7cc67b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -147,7 +147,6 @@ See [ChromiumBrowser], [FirefoxBrowser] and [WebKitBrowser] for browser-specific - [event: 'disconnected'](#event-disconnected) - [browser.browserContexts()](#browserbrowsercontexts) - [browser.close()](#browserclose) -- [browser.disconnect()](#browserdisconnect) - [browser.isConnected()](#browserisconnected) - [browser.newContext(options)](#browsernewcontextoptions) - [browser.newPage(url, [options])](#browsernewpageurl-options) @@ -168,12 +167,11 @@ a single instance of [BrowserContext]. #### browser.close() - returns: <[Promise]> -Closes browser and all of its pages (if any were opened). The [Browser] object itself is considered to be disposed and cannot be used anymore. +In case this browser is obtained using [browserType.launch](#browsertypelaunchoptions), closes the browser and all of its pages (if any were opened). -#### browser.disconnect() -- returns: <[Promise]> +In case this browser is obtained using [browserType.connect](#browsertypeconnectoptions), clears all created contexts belonging to this browser and disconnects from the browser server. -Disconnects Browser from the browser application, but leaves the application process running. After calling `disconnect`, the [Browser] object is considered disposed and cannot be used anymore. +The [Browser] object itself is considered to be disposed and cannot be used anymore. #### browser.isConnected() @@ -3468,12 +3466,11 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. - [browserType.connect(options)](#browsertypeconnectoptions) -- [browserType.defaultArgs([options])](#browsertypedefaultargsoptions) - [browserType.devices](#browsertypedevices) - [browserType.errors](#browsertypeerrors) - [browserType.executablePath()](#browsertypeexecutablepath) - [browserType.launch([options])](#browsertypelaunchoptions) -- [browserType.launchPersistent([options])](#browsertypelaunchpersistentoptions) +- [browserType.launchPersistent(userDataDir, [options])](#browsertypelaunchpersistentuserdatadir-options) - [browserType.launchServer([options])](#browsertypelaunchserveroptions) - [browserType.name()](#browsertypename) @@ -3648,7 +3645,6 @@ await browser.stopTracing(); - [event: 'disconnected'](#event-disconnected) - [browser.browserContexts()](#browserbrowsercontexts) - [browser.close()](#browserclose) -- [browser.disconnect()](#browserdisconnect) - [browser.isConnected()](#browserisconnected) - [browser.newContext(options)](#browsernewcontextoptions) - [browser.newPage(url, [options])](#browsernewpageurl-options) @@ -3816,7 +3812,6 @@ Firefox browser instance does not expose Firefox-specific features. - [event: 'disconnected'](#event-disconnected) - [browser.browserContexts()](#browserbrowsercontexts) - [browser.close()](#browserclose) -- [browser.disconnect()](#browserdisconnect) - [browser.isConnected()](#browserisconnected) - [browser.newContext(options)](#browsernewcontextoptions) - [browser.newPage(url, [options])](#browsernewpageurl-options) @@ -3833,7 +3828,6 @@ WebKit browser instance does not expose WebKit-specific features. - [event: 'disconnected'](#event-disconnected) - [browser.browserContexts()](#browserbrowsercontexts) - [browser.close()](#browserclose) -- [browser.disconnect()](#browserdisconnect) - [browser.isConnected()](#browserisconnected) - [browser.newContext(options)](#browsernewcontextoptions) - [browser.newPage(url, [options])](#browsernewpageurl-options) diff --git a/docs/web.md b/docs/web.md index 50d2c65b65..6802fd2b4a 100644 --- a/docs/web.md +++ b/docs/web.md @@ -12,7 +12,7 @@ API consists of a single `connect` function, similar to [browserType.connect(opt async function usePlaywright() { const browser = await window.playwrightweb.chromium.connect(options); // or 'firefox', 'webkit' // ... drive automation ... - await browser.disconnect(); + await browser.close(); } ``` diff --git a/src/browser.ts b/src/browser.ts index 6a820281a5..d4701c657b 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -23,7 +23,6 @@ export interface Browser extends platform.EventEmitterType { browserContexts(): BrowserContext[]; pages(): Promise; newPage(url?: string, options?: BrowserContextOptions): Promise; - disconnect(): Promise; isConnected(): boolean; close(): Promise; } diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 74d513b20b..5aca49f961 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -44,28 +44,23 @@ export class CRBrowser extends platform.EventEmitter implements Browser { static async connect(transport: ConnectionTransport, slowMo?: number): Promise { const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo)); - const { browserContextIds } = await connection.rootSession.send('Target.getBrowserContexts'); - const browser = new CRBrowser(connection, browserContextIds); + const browser = new CRBrowser(connection); await connection.rootSession.send('Target.setDiscoverTargets', { discover: true }); await browser.waitForTarget(t => t.type() === 'page'); return browser; } - constructor(connection: CRConnection, contextIds: string[]) { + constructor(connection: CRConnection) { super(); this._connection = connection; this._client = connection.rootSession; this._defaultContext = this._createBrowserContext(null, {}); - for (const contextId of contextIds) - this._contexts.set(contextId, this._createBrowserContext(contextId, {})); - this._connection.on(ConnectionEvents.Disconnected, () => this.emit(CommonEvents.Browser.Disconnected)); this._client.on('Target.targetCreated', this._targetCreated.bind(this)); this._client.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); } - _createBrowserContext(contextId: string | null, options: BrowserContextOptions): BrowserContext { const context = new BrowserContext({ pages: async (): Promise => { @@ -245,7 +240,8 @@ export class CRBrowser extends platform.EventEmitter implements Browser { async close() { const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f)); - await this._connection.rootSession.send('Browser.close'); + await Promise.all(this.browserContexts().map(context => context.close())); + this._connection.close(); await disconnected; } @@ -305,12 +301,6 @@ export class CRBrowser extends platform.EventEmitter implements Browser { return CRTarget.fromPage(page); } - async disconnect() { - const disconnected = new Promise(f => this.once(CommonEvents.Browser.Disconnected, f)); - this._connection.close(); - await disconnected; - } - isConnected(): boolean { return !this._connection._closed; } diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 6174869d1e..a67df172d2 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -37,23 +37,19 @@ export class FFBrowser extends platform.EventEmitter implements Browser { static async connect(transport: ConnectionTransport, slowMo?: number): Promise { const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo)); - const { browserContextIds } = await connection.send('Target.getBrowserContexts'); - const browser = new FFBrowser(connection, browserContextIds); + const browser = new FFBrowser(connection); await connection.send('Target.enable'); await browser._waitForTarget(t => t.type() === 'page'); return browser; } - constructor(connection: FFConnection, browserContextIds: Array) { + constructor(connection: FFConnection) { super(); this._connection = connection; this._targets = new Map(); this._defaultContext = this._createBrowserContext(null, {}); this._contexts = new Map(); - for (const browserContextId of browserContextIds) - this._contexts.set(browserContextId, this._createBrowserContext(browserContextId, {})); - this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); this._eventListeners = [ @@ -63,12 +59,6 @@ export class FFBrowser extends platform.EventEmitter implements Browser { ]; } - async disconnect() { - const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f)); - this._connection.close(); - await disconnected; - } - isConnected(): boolean { return !this._connection._closed; } @@ -154,9 +144,10 @@ export class FFBrowser extends platform.EventEmitter implements Browser { } async close() { + await Promise.all(this.browserContexts().map(context => context.close())); helper.removeEventListeners(this._eventListeners); - const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f)); - await this._connection.send('Browser.close'); + const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f)); + this._connection.close(); await disconnected; } diff --git a/src/platform.ts b/src/platform.ts index 8b14265a07..c4883ad546 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -276,14 +276,22 @@ export function fetchUrl(url: string): Promise { }); } -class WebSocketTransport implements ConnectionTransport { +export class WebSocketTransport implements ConnectionTransport { private _ws: WebSocket; onmessage?: (message: string) => void; onclose?: () => void; + private _connect: Promise; - constructor(ws: WebSocket) { - this._ws = ws; + constructor(url: string) { + this._ws = (isNode ? new NodeWebSocket(url, [], { + perMessageDeflate: false, + maxPayload: 256 * 1024 * 1024, // 256Mb + }) : new WebSocket(url)) as WebSocket; + this._connect = new Promise((fulfill, reject) => { + this._ws.addEventListener('open', () => fulfill()); + this._ws.addEventListener('error', event => reject(new Error(event.toString()))); + }); this._ws.addEventListener('message', event => { if (this.onmessage) this.onmessage.call(null, event.data); @@ -296,7 +304,8 @@ class WebSocketTransport implements ConnectionTransport { this._ws.addEventListener('error', () => {}); } - send(message: string) { + async send(message: string) { + await this._connect; this._ws.send(message); } @@ -304,14 +313,3 @@ class WebSocketTransport implements ConnectionTransport { this._ws.close(); } } - -export function createWebSocketTransport(url: string): Promise { - return new Promise((resolve, reject) => { - const ws = (isNode ? new NodeWebSocket(url, [], { - perMessageDeflate: false, - maxPayload: 256 * 1024 * 1024, // 256Mb - }) : new WebSocket(url)) as WebSocket; - ws.addEventListener('open', () => resolve(new WebSocketTransport(ws))); - ws.addEventListener('error', reject); - }); -} diff --git a/src/server/chromium.ts b/src/server/chromium.ts index 84a8a02720..e987437360 100644 --- a/src/server/chromium.ts +++ b/src/server/chromium.ts @@ -122,9 +122,9 @@ export class Chromium implements BrowserType { // 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 t = transport || await platform.createWebSocketTransport(browserWSEndpoint!); + const t = transport || new platform.WebSocketTransport(browserWSEndpoint!); const message = { method: 'Browser.close', id: kBrowserCloseMessageId }; - t.send(JSON.stringify(message)); + await t.send(JSON.stringify(message)); }, onkill: (exitCode, signal) => { if (browserServer) @@ -147,7 +147,7 @@ export class Chromium implements BrowserType { } async connect(options: ConnectOptions): Promise { - const transport = await platform.createWebSocketTransport(options.wsEndpoint); + const transport = new platform.WebSocketTransport(options.wsEndpoint); return CRBrowser.connect(transport, options.slowMo); } diff --git a/src/server/firefox.ts b/src/server/firefox.ts index 532c553a8a..822226cf7a 100644 --- a/src/server/firefox.ts +++ b/src/server/firefox.ts @@ -70,7 +70,7 @@ export class Firefox implements BrowserType { return browserContext; } - private async _launchServer(options: LaunchOptions = {}, launchType: LaunchType, userDataDir?: string, port?: number): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport }> { + private async _launchServer(options: LaunchOptions = {}, connectionType: LaunchType, userDataDir?: string, port?: number): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport }> { const { ignoreDefaultArgs = false, args = [], @@ -128,9 +128,9 @@ export class Firefox implements BrowserType { // 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 transport = await platform.createWebSocketTransport(browserWSEndpoint); + const transport = new platform.WebSocketTransport(browserWSEndpoint); const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId }; - transport.send(JSON.stringify(message)); + await transport.send(JSON.stringify(message)); }, onkill: (exitCode, signal) => { if (browserServer) @@ -141,12 +141,12 @@ export class Firefox implements BrowserType { 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 browserWSEndpoint = match[1]; - browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? browserWSEndpoint : null); - return { browserServer, transport: launchType === 'server' ? undefined : await platform.createWebSocketTransport(browserWSEndpoint) }; + browserServer = new BrowserServer(launchedProcess, gracefullyClose, connectionType === 'server' ? browserWSEndpoint : null); + return { browserServer, transport: connectionType === 'server' ? undefined : new platform.WebSocketTransport(browserWSEndpoint) }; } async connect(options: ConnectOptions): Promise { - const transport = await platform.createWebSocketTransport(options.wsEndpoint); + const transport = new platform.WebSocketTransport(options.wsEndpoint); return FFBrowser.connect(transport, options.slowMo); } diff --git a/src/server/webkit.ts b/src/server/webkit.ts index dc4dfc3e77..5e3fbfb0f2 100644 --- a/src/server/webkit.ts +++ b/src/server/webkit.ts @@ -141,7 +141,7 @@ export class WebKit implements BrowserType { } async connect(options: ConnectOptions): Promise { - const transport = await platform.createWebSocketTransport(options.wsEndpoint); + const transport = new platform.WebSocketTransport(options.wsEndpoint); return WKBrowser.connect(transport, options.slowMo); } @@ -236,36 +236,160 @@ function getMacVersion(): string { return cachedMacVersion; } +class SequenceNumberMixer { + static _lastSequenceNumber = 1; + private _values = new Map(); + + generate(value: V): number { + const sequenceNumber = ++SequenceNumberMixer._lastSequenceNumber; + this._values.set(sequenceNumber, value); + return sequenceNumber; + } + + take(sequenceNumber: number): V | undefined { + const value = this._values.get(sequenceNumber); + this._values.delete(sequenceNumber); + return value; + } +} + function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number) { const server = new ws.Server({ port }); - let socket: ws | undefined; const guid = uuidv4(); + const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>(); + const pendingBrowserContextCreations = new Set(); + const pendingBrowserContextDeletions = new Map(); + const browserContextIds = new Map(); + const pageProxyIds = new Map(); + const sockets = new Set(); - server.on('connection', (s, req) => { + transport.onmessage = message => { + const parsedMessage = JSON.parse(message); + if ('id' in parsedMessage) { + if (parsedMessage.id === -9999) + return; + // Process command response. + const value = idMixer.take(parsedMessage.id); + if (!value) + return; + const { id, socket } = value; + + if (!socket || socket.readyState === ws.CLOSING) { + if (pendingBrowserContextCreations.has(id)) { + transport.send(JSON.stringify({ + id: ++SequenceNumberMixer._lastSequenceNumber, + method: 'Browser.deleteContext', + params: { browserContextId: parsedMessage.result.browserContextId } + })); + } + return; + } + + if (pendingBrowserContextCreations.has(parsedMessage.id)) { + // Browser.createContext response -> establish context attribution. + browserContextIds.set(parsedMessage.result.browserContextId, socket); + pendingBrowserContextCreations.delete(parsedMessage.id); + } + + const deletedContextId = pendingBrowserContextDeletions.get(parsedMessage.id); + if (deletedContextId) { + // Browser.deleteContext response -> remove context attribution. + browserContextIds.delete(deletedContextId); + pendingBrowserContextDeletions.delete(parsedMessage.id); + } + + parsedMessage.id = id; + socket.send(JSON.stringify(parsedMessage)); + return; + } + + // Process notification response. + const { method, params, pageProxyId } = parsedMessage; + if (pageProxyId) { + const socket = pageProxyIds.get(pageProxyId); + if (!socket || socket.readyState === ws.CLOSING) { + // Drop unattributed messages on the floor. + return; + } + socket.send(message); + return; + } + if (method === 'Browser.pageProxyCreated') { + const socket = browserContextIds.get(params.pageProxyInfo.browserContextId); + if (!socket || socket.readyState === ws.CLOSING) { + // Drop unattributed messages on the floor. + return; + } + pageProxyIds.set(params.pageProxyInfo.pageProxyId, socket); + socket.send(message); + return; + } + if (method === 'Browser.pageProxyDestroyed') { + const socket = pageProxyIds.get(params.pageProxyId); + pageProxyIds.delete(params.pageProxyId); + if (socket && socket.readyState !== ws.CLOSING) + socket.send(message); + return; + } + if (method === 'Browser.provisionalLoadFailed') { + const socket = pageProxyIds.get(params.pageProxyId); + if (socket && socket.readyState !== ws.CLOSING) + socket!.send(message); + return; + } + }; + + server.on('connection', (socket: ws, req) => { if (req.url !== '/' + guid) { - s.close(); + socket.close(); return; } - if (socket) { - s.close(undefined, 'Multiple connections are not supported'); - return; - } - socket = s; - s.on('message', message => transport.send(Buffer.from(message).toString())); - transport.onmessage = message => { - // We are not notified when socket starts closing, and sending messages to a closing - // socket throws an error. - if (s.readyState !== ws.CLOSING) - s.send(message); - }; - s.on('close', () => { - socket = undefined; - transport.onmessage = undefined; + sockets.add(socket); + // Following two messages are reporting the default browser context and the default page. + socket.send(JSON.stringify({ + method: 'Browser.pageProxyCreated', + params: { pageProxyInfo: { pageProxyId: '5', browserContextId: '0000000000000002' } } + })); + socket.send(JSON.stringify({ + method: 'Target.targetCreated', + params: { + targetInfo: { targetId: 'page-6', type: 'page', isPaused: false } + }, + pageProxyId: '5' + })); + + socket.on('message', (message: string) => { + const parsedMessage = JSON.parse(Buffer.from(message).toString()); + const { id, method, params } = parsedMessage; + const seqNum = idMixer.generate({ id, socket }); + transport.send(JSON.stringify({ ...parsedMessage, id: seqNum })); + if (method === 'Browser.createContext') + pendingBrowserContextCreations.add(seqNum); + if (method === 'Browser.deleteContext') + pendingBrowserContextDeletions.set(seqNum, params.browserContextId); + }); + + socket.on('close', () => { + for (const [pageProxyId, s] of pageProxyIds) { + if (s === socket) + pageProxyIds.delete(pageProxyId); + } + for (const [browserContextId, s] of browserContextIds) { + if (s === socket) { + transport.send(JSON.stringify({ + id: ++SequenceNumberMixer._lastSequenceNumber, + method: 'Browser.deleteContext', + params: { browserContextId } + })); + browserContextIds.delete(browserContextId); + } + } + sockets.delete(socket); }); }); transport.onclose = () => { - if (socket) + for (const socket of sockets) socket.close(undefined, 'Browser disconnected'); server.close(); transport.onmessage = undefined; diff --git a/src/web.ts b/src/web.ts index b2d78e37aa..9de9f8f586 100644 --- a/src/web.ts +++ b/src/web.ts @@ -22,19 +22,19 @@ import * as platform from './platform'; const connect = { chromium: { connect: async (url: string) => { - const transport = await platform.createWebSocketTransport(url); + const transport = new platform.WebSocketTransport(url); return ChromiumBrowser.connect(transport); } }, webkit: { connect: async (url: string) => { - const transport = await platform.createWebSocketTransport(url); + const transport = new platform.WebSocketTransport(url); return WebKitBrowser.connect(transport); } }, firefox: { connect: async (url: string) => { - const transport = await platform.createWebSocketTransport(url); + const transport = new platform.WebSocketTransport(url); return FirefoxBrowser.connect(transport); } } diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index d7aa26b3b5..8c98210e0c 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -157,12 +157,6 @@ export class WKBrowser extends platform.EventEmitter implements Browser { pageProxy.handleProvisionalLoadFailed(event); } - async disconnect() { - const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f)); - this._connection.close(); - await disconnected; - } - isConnected(): boolean { return !this._connection.isClosed(); } @@ -170,7 +164,8 @@ export class WKBrowser extends platform.EventEmitter implements Browser { async close() { helper.removeEventListeners(this._eventListeners); const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f)); - await this._browserSession.send('Browser.close'); + await Promise.all(this.browserContexts().map(context => context.close())); + this._connection.close(); await disconnected; } diff --git a/test/assets/playwrightweb.html b/test/assets/playwrightweb.html index 1946dfac20..b836f9c222 100644 --- a/test/assets/playwrightweb.html +++ b/test/assets/playwrightweb.html @@ -7,6 +7,6 @@ async function setup(product, wsEndpoint) { } async function teardown() { await window.context.close(); - await window.browser.disconnect(); + await window.browser.close(); } \ No newline at end of file diff --git a/test/launcher.spec.js b/test/launcher.spec.js index 909404334c..a68b91d772 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -114,7 +114,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const browserServer = await playwright.launchServer({...defaultBrowserOptions }); const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); expect(remote.isConnected()).toBe(true); - await remote.disconnect(); + await remote.close(); expect(remote.isConnected()).toBe(false); await browserServer.close(); }); @@ -140,7 +140,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const page = await remote.newPage(); const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e); await server.waitForRequest('/one-style.css'); - await remote.disconnect(); + await remote.close(); const error = await navigationPromise; expect(error.message).toBe('Navigation failed because browser has disconnected!'); await browserServer.close(); @@ -155,7 +155,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p // Make sure the previous waitForSelector has time to make it to the browser before we disconnect. await page.waitForSelector('body'); - await remote.disconnect(); + await remote.close(); const error = await watchdog; expect(error.message).toContain('Protocol error'); await browserServer.close(); @@ -164,7 +164,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const browserServer = await playwright.launchServer({...defaultBrowserOptions }); const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); const page = await remote.newPage(); - await remote.disconnect(); + await remote.close(); const error = await page.evaluate('1 + 1').catch(e => e); expect(error.message).toContain('has been closed'); await browserServer.close(); @@ -187,14 +187,6 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(message).not.toContain('Timeout'); } }); - it('should be able to close remote browser', async({server}) => { - const browserServer = await playwright.launchServer({...defaultBrowserOptions }); - const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - await Promise.all([ - new Promise(f => browserServer.once('close', f)), - remote.close(), - ]); - }); }); describe('Playwright.launch |webSocket| option', function() { @@ -219,25 +211,22 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); describe('Playwright.connect', function() { - it.skip(WEBKIT)('should be able to reconnect to a browser', async({server}) => { + it('should be able to reconnect to a browser', async({server}) => { const browserServer = await playwright.launchServer(defaultBrowserOptions); - const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - const browserContext = await browser.newContext(); - const page = await browserContext.newPage(); - await page.goto(server.PREFIX + '/frames/nested-frames.html'); - await browser.disconnect(); - - const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - const pages = await remote.pages(); - const restoredPage = pages.find(page => page.url() === server.PREFIX + '/frames/nested-frames.html'); - expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([ - 'http://localhost:/frames/nested-frames.html', - ' http://localhost:/frames/frame.html (aframe)', - ' http://localhost:/frames/two-frames.html (2frames)', - ' http://localhost:/frames/frame.html (dos)', - ' http://localhost:/frames/frame.html (uno)', - ]); - expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56); + { + const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); + const browserContext = await browser.newContext(); + const page = await browserContext.newPage(); + await page.goto(server.EMPTY_PAGE); + await browser.close(); + } + { + const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); + const browserContext = await browser.newContext(); + const page = await browserContext.newPage(); + await page.goto(server.EMPTY_PAGE); + await browser.close(); + } await browserServer.close(); }); }); diff --git a/test/multiclient.spec.js b/test/multiclient.spec.js index 67091b9e41..3a681dbfe0 100644 --- a/test/multiclient.spec.js +++ b/test/multiclient.spec.js @@ -25,13 +25,17 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p describe('BrowserContext', function() { it('should work across sessions', async () => { const browserServer = await playwright.launchServer(defaultBrowserOptions); - const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - expect(browser.browserContexts().length).toBe(0); - await browser.newContext(); - expect(browser.browserContexts().length).toBe(1); - const remoteBrowser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - const contexts = remoteBrowser.browserContexts(); - expect(contexts.length).toBe(1); + const browser1 = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); + expect(browser1.browserContexts().length).toBe(0); + await browser1.newContext(); + expect(browser1.browserContexts().length).toBe(1); + + const browser2 = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); + expect(browser2.browserContexts().length).toBe(0); + await browser2.newContext(); + expect(browser2.browserContexts().length).toBe(1); + + expect(browser1.browserContexts().length).toBe(1); await browserServer.close(); }); }); @@ -53,7 +57,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p await Promise.all([ utils.waitEvent(remoteBrowser2, 'disconnected'), - remoteBrowser2.disconnect(), + remoteBrowser2.close(), ]); expect(disconnectedOriginal).toBe(0); @@ -74,37 +78,29 @@ 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 browserServer = await playwright.launchServer({...defaultBrowserOptions }); - const local = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - const page = await remote.newPage(); - expect(await page.evaluate(() => 7 * 8)).toBe(56); - remote.disconnect(); - - const secondPage = await local.newPage(); - expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work'); - await browserServer.close(); - }); - it('should be able to close remote browser', async({server}) => { - const browserServer = await playwright.launchServer({...defaultBrowserOptions }); - const local = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - await Promise.all([ - utils.waitEvent(local, 'disconnected'), - remote.close(), - ]); - }); - // @see https://github.com/GoogleChrome/puppeteer/issues/4197#issuecomment-481793410 - it('should be able to connect to the same page simultaneously', async({server}) => { - const browserServer = await playwright.launchServer({...defaultBrowserOptions }); + const browserServer = await playwright.launchServer(defaultBrowserOptions); const browser1 = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - const page1 = await browser1.newPage(); - await page1.goto(server.EMPTY_PAGE); const browser2 = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); - const page2 = (await browser2.pages()).find(page => page.url() === server.EMPTY_PAGE); + const page1 = await browser1.newPage(); expect(await page1.evaluate(() => 7 * 8)).toBe(56); - expect(await page2.evaluate(() => 7 * 6)).toBe(42); + browser1.close(); + + const page2 = await browser2.newPage(); + expect(await page2.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work'); await browserServer.close(); }); + it('should not be able to close remote browser', async() => { + const browserServer = await playwright.launchServer(defaultBrowserOptions); + { + const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); + await remote.newContext(); + await remote.close(); + } + { + const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); + await remote.newContext(); + await remote.close(); + } + }); }); }; diff --git a/test/playwright.spec.js b/test/playwright.spec.js index 7af734b3e0..e761ec975e 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -200,6 +200,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { testRunner.loadTests(require('./fixtures.spec.js'), testOptions); testRunner.loadTests(require('./launcher.spec.js'), testOptions); testRunner.loadTests(require('./headful.spec.js'), testOptions); + testRunner.loadTests(require('./multiclient.spec.js'), testOptions); if (CHROMIUM) { testRunner.loadTests(require('./chromium/launcher.spec.js'), testOptions); @@ -208,9 +209,5 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { testRunner.loadTests(require('./chromium/tracing.spec.js'), testOptions); } - if (CHROMIUM || FFOX) { - testRunner.loadTests(require('./multiclient.spec.js'), testOptions); - } - testRunner.loadTests(require('./web.spec.js'), testOptions); };