feat(connect): allow multiple webkit connections over web socket (#863)

This commit is contained in:
Pavel Feldman 2020-02-06 12:41:43 -08:00 committed by GitHub
parent f49d63ff0c
commit a547aa7984
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 237 additions and 164 deletions

View File

@ -147,7 +147,6 @@ See [ChromiumBrowser], [FirefoxBrowser] and [WebKitBrowser] for browser-specific
- [event: 'disconnected'](#event-disconnected) - [event: 'disconnected'](#event-disconnected)
- [browser.browserContexts()](#browserbrowsercontexts) - [browser.browserContexts()](#browserbrowsercontexts)
- [browser.close()](#browserclose) - [browser.close()](#browserclose)
- [browser.disconnect()](#browserdisconnect)
- [browser.isConnected()](#browserisconnected) - [browser.isConnected()](#browserisconnected)
- [browser.newContext(options)](#browsernewcontextoptions) - [browser.newContext(options)](#browsernewcontextoptions)
- [browser.newPage(url, [options])](#browsernewpageurl-options) - [browser.newPage(url, [options])](#browsernewpageurl-options)
@ -168,12 +167,11 @@ a single instance of [BrowserContext].
#### browser.close() #### browser.close()
- returns: <[Promise]> - 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() In case this browser is obtained using [browserType.connect](#browsertypeconnectoptions), clears all created contexts belonging to this browser and disconnects from the browser server.
- returns: <[Promise]>
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() #### browser.isConnected()
@ -3468,12 +3466,11 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
<!-- GEN:toc --> <!-- GEN:toc -->
- [browserType.connect(options)](#browsertypeconnectoptions) - [browserType.connect(options)](#browsertypeconnectoptions)
- [browserType.defaultArgs([options])](#browsertypedefaultargsoptions)
- [browserType.devices](#browsertypedevices) - [browserType.devices](#browsertypedevices)
- [browserType.errors](#browsertypeerrors) - [browserType.errors](#browsertypeerrors)
- [browserType.executablePath()](#browsertypeexecutablepath) - [browserType.executablePath()](#browsertypeexecutablepath)
- [browserType.launch([options])](#browsertypelaunchoptions) - [browserType.launch([options])](#browsertypelaunchoptions)
- [browserType.launchPersistent([options])](#browsertypelaunchpersistentoptions) - [browserType.launchPersistent(userDataDir, [options])](#browsertypelaunchpersistentuserdatadir-options)
- [browserType.launchServer([options])](#browsertypelaunchserveroptions) - [browserType.launchServer([options])](#browsertypelaunchserveroptions)
- [browserType.name()](#browsertypename) - [browserType.name()](#browsertypename)
<!-- GEN:stop --> <!-- GEN:stop -->
@ -3648,7 +3645,6 @@ await browser.stopTracing();
- [event: 'disconnected'](#event-disconnected) - [event: 'disconnected'](#event-disconnected)
- [browser.browserContexts()](#browserbrowsercontexts) - [browser.browserContexts()](#browserbrowsercontexts)
- [browser.close()](#browserclose) - [browser.close()](#browserclose)
- [browser.disconnect()](#browserdisconnect)
- [browser.isConnected()](#browserisconnected) - [browser.isConnected()](#browserisconnected)
- [browser.newContext(options)](#browsernewcontextoptions) - [browser.newContext(options)](#browsernewcontextoptions)
- [browser.newPage(url, [options])](#browsernewpageurl-options) - [browser.newPage(url, [options])](#browsernewpageurl-options)
@ -3816,7 +3812,6 @@ Firefox browser instance does not expose Firefox-specific features.
- [event: 'disconnected'](#event-disconnected) - [event: 'disconnected'](#event-disconnected)
- [browser.browserContexts()](#browserbrowsercontexts) - [browser.browserContexts()](#browserbrowsercontexts)
- [browser.close()](#browserclose) - [browser.close()](#browserclose)
- [browser.disconnect()](#browserdisconnect)
- [browser.isConnected()](#browserisconnected) - [browser.isConnected()](#browserisconnected)
- [browser.newContext(options)](#browsernewcontextoptions) - [browser.newContext(options)](#browsernewcontextoptions)
- [browser.newPage(url, [options])](#browsernewpageurl-options) - [browser.newPage(url, [options])](#browsernewpageurl-options)
@ -3833,7 +3828,6 @@ WebKit browser instance does not expose WebKit-specific features.
- [event: 'disconnected'](#event-disconnected) - [event: 'disconnected'](#event-disconnected)
- [browser.browserContexts()](#browserbrowsercontexts) - [browser.browserContexts()](#browserbrowsercontexts)
- [browser.close()](#browserclose) - [browser.close()](#browserclose)
- [browser.disconnect()](#browserdisconnect)
- [browser.isConnected()](#browserisconnected) - [browser.isConnected()](#browserisconnected)
- [browser.newContext(options)](#browsernewcontextoptions) - [browser.newContext(options)](#browsernewcontextoptions)
- [browser.newPage(url, [options])](#browsernewpageurl-options) - [browser.newPage(url, [options])](#browsernewpageurl-options)

View File

@ -12,7 +12,7 @@ API consists of a single `connect` function, similar to [browserType.connect(opt
async function usePlaywright() { async function usePlaywright() {
const browser = await window.playwrightweb.chromium.connect(options); // or 'firefox', 'webkit' const browser = await window.playwrightweb.chromium.connect(options); // or 'firefox', 'webkit'
// ... drive automation ... // ... drive automation ...
await browser.disconnect(); await browser.close();
} }
</script> </script>
``` ```

View File

@ -23,7 +23,6 @@ export interface Browser extends platform.EventEmitterType {
browserContexts(): BrowserContext[]; browserContexts(): BrowserContext[];
pages(): Promise<Page[]>; pages(): Promise<Page[]>;
newPage(url?: string, options?: BrowserContextOptions): Promise<Page>; newPage(url?: string, options?: BrowserContextOptions): Promise<Page>;
disconnect(): Promise<void>;
isConnected(): boolean; isConnected(): boolean;
close(): Promise<void>; close(): Promise<void>;
} }

View File

@ -44,28 +44,23 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<CRBrowser> { static async connect(transport: ConnectionTransport, slowMo?: number): Promise<CRBrowser> {
const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo)); const connection = new CRConnection(SlowMoTransport.wrap(transport, slowMo));
const { browserContextIds } = await connection.rootSession.send('Target.getBrowserContexts'); const browser = new CRBrowser(connection);
const browser = new CRBrowser(connection, browserContextIds);
await connection.rootSession.send('Target.setDiscoverTargets', { discover: true }); await connection.rootSession.send('Target.setDiscoverTargets', { discover: true });
await browser.waitForTarget(t => t.type() === 'page'); await browser.waitForTarget(t => t.type() === 'page');
return browser; return browser;
} }
constructor(connection: CRConnection, contextIds: string[]) { constructor(connection: CRConnection) {
super(); super();
this._connection = connection; this._connection = connection;
this._client = connection.rootSession; this._client = connection.rootSession;
this._defaultContext = this._createBrowserContext(null, {}); 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._connection.on(ConnectionEvents.Disconnected, () => this.emit(CommonEvents.Browser.Disconnected));
this._client.on('Target.targetCreated', this._targetCreated.bind(this)); this._client.on('Target.targetCreated', this._targetCreated.bind(this));
this._client.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); this._client.on('Target.targetDestroyed', this._targetDestroyed.bind(this));
this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
} }
_createBrowserContext(contextId: string | null, options: BrowserContextOptions): BrowserContext { _createBrowserContext(contextId: string | null, options: BrowserContextOptions): BrowserContext {
const context = new BrowserContext({ const context = new BrowserContext({
pages: async (): Promise<Page[]> => { pages: async (): Promise<Page[]> => {
@ -245,7 +240,8 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
async close() { async close() {
const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f)); 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; await disconnected;
} }
@ -305,12 +301,6 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
return CRTarget.fromPage(page); return CRTarget.fromPage(page);
} }
async disconnect() {
const disconnected = new Promise(f => this.once(CommonEvents.Browser.Disconnected, f));
this._connection.close();
await disconnected;
}
isConnected(): boolean { isConnected(): boolean {
return !this._connection._closed; return !this._connection._closed;
} }

View File

@ -37,23 +37,19 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<FFBrowser> { static async connect(transport: ConnectionTransport, slowMo?: number): Promise<FFBrowser> {
const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo)); const connection = new FFConnection(SlowMoTransport.wrap(transport, slowMo));
const { browserContextIds } = await connection.send('Target.getBrowserContexts'); const browser = new FFBrowser(connection);
const browser = new FFBrowser(connection, browserContextIds);
await connection.send('Target.enable'); await connection.send('Target.enable');
await browser._waitForTarget(t => t.type() === 'page'); await browser._waitForTarget(t => t.type() === 'page');
return browser; return browser;
} }
constructor(connection: FFConnection, browserContextIds: Array<string>) { constructor(connection: FFConnection) {
super(); super();
this._connection = connection; this._connection = connection;
this._targets = new Map(); this._targets = new Map();
this._defaultContext = this._createBrowserContext(null, {}); this._defaultContext = this._createBrowserContext(null, {});
this._contexts = new Map(); 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._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected));
this._eventListeners = [ 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 { isConnected(): boolean {
return !this._connection._closed; return !this._connection._closed;
} }
@ -154,9 +144,10 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
} }
async close() { async close() {
await Promise.all(this.browserContexts().map(context => context.close()));
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f)); const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f));
await this._connection.send('Browser.close'); this._connection.close();
await disconnected; await disconnected;
} }

View File

@ -276,14 +276,22 @@ export function fetchUrl(url: string): Promise<string> {
}); });
} }
class WebSocketTransport implements ConnectionTransport { export class WebSocketTransport implements ConnectionTransport {
private _ws: WebSocket; private _ws: WebSocket;
onmessage?: (message: string) => void; onmessage?: (message: string) => void;
onclose?: () => void; onclose?: () => void;
private _connect: Promise<void>;
constructor(ws: WebSocket) { constructor(url: string) {
this._ws = ws; 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 => { this._ws.addEventListener('message', event => {
if (this.onmessage) if (this.onmessage)
this.onmessage.call(null, event.data); this.onmessage.call(null, event.data);
@ -296,7 +304,8 @@ class WebSocketTransport implements ConnectionTransport {
this._ws.addEventListener('error', () => {}); this._ws.addEventListener('error', () => {});
} }
send(message: string) { async send(message: string) {
await this._connect;
this._ws.send(message); this._ws.send(message);
} }
@ -304,14 +313,3 @@ class WebSocketTransport implements ConnectionTransport {
this._ws.close(); this._ws.close();
} }
} }
export function createWebSocketTransport(url: string): Promise<ConnectionTransport> {
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);
});
}

View File

@ -122,9 +122,9 @@ export class Chromium implements BrowserType {
// We try to gracefully close to prevent crash reporting and core dumps. // We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since // Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId. // 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 }; const message = { method: 'Browser.close', id: kBrowserCloseMessageId };
t.send(JSON.stringify(message)); await t.send(JSON.stringify(message));
}, },
onkill: (exitCode, signal) => { onkill: (exitCode, signal) => {
if (browserServer) if (browserServer)
@ -147,7 +147,7 @@ export class Chromium implements BrowserType {
} }
async connect(options: ConnectOptions): Promise<CRBrowser> { async connect(options: ConnectOptions): Promise<CRBrowser> {
const transport = await platform.createWebSocketTransport(options.wsEndpoint); const transport = new platform.WebSocketTransport(options.wsEndpoint);
return CRBrowser.connect(transport, options.slowMo); return CRBrowser.connect(transport, options.slowMo);
} }

View File

@ -70,7 +70,7 @@ export class Firefox implements BrowserType {
return browserContext; 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 { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
@ -128,9 +128,9 @@ export class Firefox implements BrowserType {
// We try to gracefully close to prevent crash reporting and core dumps. // We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since // Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId. // our connection ignores kBrowserCloseMessageId.
const transport = await platform.createWebSocketTransport(browserWSEndpoint); const transport = new platform.WebSocketTransport(browserWSEndpoint);
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId }; const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
transport.send(JSON.stringify(message)); await transport.send(JSON.stringify(message));
}, },
onkill: (exitCode, signal) => { onkill: (exitCode, signal) => {
if (browserServer) 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 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 match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
const browserWSEndpoint = match[1]; const browserWSEndpoint = match[1];
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? browserWSEndpoint : null); browserServer = new BrowserServer(launchedProcess, gracefullyClose, connectionType === 'server' ? browserWSEndpoint : null);
return { browserServer, transport: launchType === 'server' ? undefined : await platform.createWebSocketTransport(browserWSEndpoint) }; return { browserServer, transport: connectionType === 'server' ? undefined : new platform.WebSocketTransport(browserWSEndpoint) };
} }
async connect(options: ConnectOptions): Promise<FFBrowser> { async connect(options: ConnectOptions): Promise<FFBrowser> {
const transport = await platform.createWebSocketTransport(options.wsEndpoint); const transport = new platform.WebSocketTransport(options.wsEndpoint);
return FFBrowser.connect(transport, options.slowMo); return FFBrowser.connect(transport, options.slowMo);
} }

View File

@ -141,7 +141,7 @@ export class WebKit implements BrowserType {
} }
async connect(options: ConnectOptions): Promise<WKBrowser> { async connect(options: ConnectOptions): Promise<WKBrowser> {
const transport = await platform.createWebSocketTransport(options.wsEndpoint); const transport = new platform.WebSocketTransport(options.wsEndpoint);
return WKBrowser.connect(transport, options.slowMo); return WKBrowser.connect(transport, options.slowMo);
} }
@ -236,36 +236,160 @@ function getMacVersion(): string {
return cachedMacVersion; return cachedMacVersion;
} }
class SequenceNumberMixer<V> {
static _lastSequenceNumber = 1;
private _values = new Map<number, V>();
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) { function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number) {
const server = new ws.Server({ port }); const server = new ws.Server({ port });
let socket: ws | undefined;
const guid = uuidv4(); const guid = uuidv4();
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
const pendingBrowserContextCreations = new Set<number>();
const pendingBrowserContextDeletions = new Map<number, string>();
const browserContextIds = new Map<string, ws>();
const pageProxyIds = new Map<string, ws>();
const sockets = new Set<ws>();
server.on('connection', (s, req) => {
if (req.url !== '/' + guid) {
s.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 => { transport.onmessage = message => {
// We are not notified when socket starts closing, and sending messages to a closing const parsedMessage = JSON.parse(message);
// socket throws an error. if ('id' in parsedMessage) {
if (s.readyState !== ws.CLOSING) if (parsedMessage.id === -9999)
s.send(message); 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;
}
}; };
s.on('close', () => {
socket = undefined; server.on('connection', (socket: ws, req) => {
transport.onmessage = undefined; if (req.url !== '/' + guid) {
socket.close();
return;
}
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 = () => { transport.onclose = () => {
if (socket) for (const socket of sockets)
socket.close(undefined, 'Browser disconnected'); socket.close(undefined, 'Browser disconnected');
server.close(); server.close();
transport.onmessage = undefined; transport.onmessage = undefined;

View File

@ -22,19 +22,19 @@ import * as platform from './platform';
const connect = { const connect = {
chromium: { chromium: {
connect: async (url: string) => { connect: async (url: string) => {
const transport = await platform.createWebSocketTransport(url); const transport = new platform.WebSocketTransport(url);
return ChromiumBrowser.connect(transport); return ChromiumBrowser.connect(transport);
} }
}, },
webkit: { webkit: {
connect: async (url: string) => { connect: async (url: string) => {
const transport = await platform.createWebSocketTransport(url); const transport = new platform.WebSocketTransport(url);
return WebKitBrowser.connect(transport); return WebKitBrowser.connect(transport);
} }
}, },
firefox: { firefox: {
connect: async (url: string) => { connect: async (url: string) => {
const transport = await platform.createWebSocketTransport(url); const transport = new platform.WebSocketTransport(url);
return FirefoxBrowser.connect(transport); return FirefoxBrowser.connect(transport);
} }
} }

View File

@ -157,12 +157,6 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
pageProxy.handleProvisionalLoadFailed(event); pageProxy.handleProvisionalLoadFailed(event);
} }
async disconnect() {
const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f));
this._connection.close();
await disconnected;
}
isConnected(): boolean { isConnected(): boolean {
return !this._connection.isClosed(); return !this._connection.isClosed();
} }
@ -170,7 +164,8 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
async close() { async close() {
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
const disconnected = new Promise(f => this.once(Events.Browser.Disconnected, f)); 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; await disconnected;
} }

View File

@ -7,6 +7,6 @@ async function setup(product, wsEndpoint) {
} }
async function teardown() { async function teardown() {
await window.context.close(); await window.context.close();
await window.browser.disconnect(); await window.browser.close();
} }
</script> </script>

View File

@ -114,7 +114,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
const browserServer = await playwright.launchServer({...defaultBrowserOptions }); const browserServer = await playwright.launchServer({...defaultBrowserOptions });
const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
expect(remote.isConnected()).toBe(true); expect(remote.isConnected()).toBe(true);
await remote.disconnect(); await remote.close();
expect(remote.isConnected()).toBe(false); expect(remote.isConnected()).toBe(false);
await browserServer.close(); await browserServer.close();
}); });
@ -140,7 +140,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
const page = await remote.newPage(); const page = await remote.newPage();
const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e); const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e);
await server.waitForRequest('/one-style.css'); await server.waitForRequest('/one-style.css');
await remote.disconnect(); await remote.close();
const error = await navigationPromise; const error = await navigationPromise;
expect(error.message).toBe('Navigation failed because browser has disconnected!'); expect(error.message).toBe('Navigation failed because browser has disconnected!');
await browserServer.close(); 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. // Make sure the previous waitForSelector has time to make it to the browser before we disconnect.
await page.waitForSelector('body'); await page.waitForSelector('body');
await remote.disconnect(); await remote.close();
const error = await watchdog; const error = await watchdog;
expect(error.message).toContain('Protocol error'); expect(error.message).toContain('Protocol error');
await browserServer.close(); await browserServer.close();
@ -164,7 +164,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
const browserServer = await playwright.launchServer({...defaultBrowserOptions }); const browserServer = await playwright.launchServer({...defaultBrowserOptions });
const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
const page = await remote.newPage(); const page = await remote.newPage();
await remote.disconnect(); await remote.close();
const error = await page.evaluate('1 + 1').catch(e => e); const error = await page.evaluate('1 + 1').catch(e => e);
expect(error.message).toContain('has been closed'); expect(error.message).toContain('has been closed');
await browserServer.close(); await browserServer.close();
@ -187,14 +187,6 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
expect(message).not.toContain('Timeout'); 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() { describe('Playwright.launch |webSocket| option', function() {
@ -219,25 +211,22 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
}); });
describe('Playwright.connect', function() { 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 browserServer = await playwright.launchServer(defaultBrowserOptions);
{
const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
const browserContext = await browser.newContext(); const browserContext = await browser.newContext();
const page = await browserContext.newPage(); const page = await browserContext.newPage();
await page.goto(server.PREFIX + '/frames/nested-frames.html'); await page.goto(server.EMPTY_PAGE);
await browser.disconnect(); await browser.close();
}
const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); {
const pages = await remote.pages(); const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
const restoredPage = pages.find(page => page.url() === server.PREFIX + '/frames/nested-frames.html'); const browserContext = await browser.newContext();
expect(utils.dumpFrames(restoredPage.mainFrame())).toEqual([ const page = await browserContext.newPage();
'http://localhost:<PORT>/frames/nested-frames.html', await page.goto(server.EMPTY_PAGE);
' http://localhost:<PORT>/frames/frame.html (aframe)', await browser.close();
' http://localhost:<PORT>/frames/two-frames.html (2frames)', }
' http://localhost:<PORT>/frames/frame.html (dos)',
' http://localhost:<PORT>/frames/frame.html (uno)',
]);
expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56);
await browserServer.close(); await browserServer.close();
}); });
}); });

View File

@ -25,13 +25,17 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
describe('BrowserContext', function() { describe('BrowserContext', function() {
it('should work across sessions', async () => { it('should work across sessions', async () => {
const browserServer = await playwright.launchServer(defaultBrowserOptions); const browserServer = await playwright.launchServer(defaultBrowserOptions);
const browser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); const browser1 = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
expect(browser.browserContexts().length).toBe(0); expect(browser1.browserContexts().length).toBe(0);
await browser.newContext(); await browser1.newContext();
expect(browser.browserContexts().length).toBe(1); expect(browser1.browserContexts().length).toBe(1);
const remoteBrowser = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
const contexts = remoteBrowser.browserContexts(); const browser2 = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
expect(contexts.length).toBe(1); expect(browser2.browserContexts().length).toBe(0);
await browser2.newContext();
expect(browser2.browserContexts().length).toBe(1);
expect(browser1.browserContexts().length).toBe(1);
await browserServer.close(); await browserServer.close();
}); });
}); });
@ -53,7 +57,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
await Promise.all([ await Promise.all([
utils.waitEvent(remoteBrowser2, 'disconnected'), utils.waitEvent(remoteBrowser2, 'disconnected'),
remoteBrowser2.disconnect(), remoteBrowser2.close(),
]); ]);
expect(disconnectedOriginal).toBe(0); expect(disconnectedOriginal).toBe(0);
@ -74,37 +78,29 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
describe('Playwright.connect', function() { describe('Playwright.connect', function() {
it('should be able to connect multiple times to the same browser', async({server}) => { it('should be able to connect multiple times to the same browser', async({server}) => {
const browserServer = await playwright.launchServer({...defaultBrowserOptions }); 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 browser1 = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); 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 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 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(); 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();
}
});
}); });
}; };

View File

@ -200,6 +200,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
testRunner.loadTests(require('./fixtures.spec.js'), testOptions); testRunner.loadTests(require('./fixtures.spec.js'), testOptions);
testRunner.loadTests(require('./launcher.spec.js'), testOptions); testRunner.loadTests(require('./launcher.spec.js'), testOptions);
testRunner.loadTests(require('./headful.spec.js'), testOptions); testRunner.loadTests(require('./headful.spec.js'), testOptions);
testRunner.loadTests(require('./multiclient.spec.js'), testOptions);
if (CHROMIUM) { if (CHROMIUM) {
testRunner.loadTests(require('./chromium/launcher.spec.js'), testOptions); 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); 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); testRunner.loadTests(require('./web.spec.js'), testOptions);
}; };