mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(connect): allow multiple webkit connections over web socket (#863)
This commit is contained in:
parent
f49d63ff0c
commit
a547aa7984
14
docs/api.md
14
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'.
|
||||
|
||||
<!-- GEN:toc -->
|
||||
- [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)
|
||||
<!-- GEN:stop -->
|
||||
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
@ -23,7 +23,6 @@ export interface Browser extends platform.EventEmitterType {
|
||||
browserContexts(): BrowserContext[];
|
||||
pages(): Promise<Page[]>;
|
||||
newPage(url?: string, options?: BrowserContextOptions): Promise<Page>;
|
||||
disconnect(): Promise<void>;
|
||||
isConnected(): boolean;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
@ -44,28 +44,23 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
|
||||
|
||||
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<CRBrowser> {
|
||||
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<Page[]> => {
|
||||
@ -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;
|
||||
}
|
||||
|
@ -37,23 +37,19 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
|
||||
|
||||
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<FFBrowser> {
|
||||
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<string>) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -276,14 +276,22 @@ export function fetchUrl(url: string): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
class WebSocketTransport implements ConnectionTransport {
|
||||
export class WebSocketTransport implements ConnectionTransport {
|
||||
private _ws: WebSocket;
|
||||
|
||||
onmessage?: (message: string) => void;
|
||||
onclose?: () => void;
|
||||
private _connect: Promise<void>;
|
||||
|
||||
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<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);
|
||||
});
|
||||
}
|
||||
|
@ -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<CRBrowser> {
|
||||
const transport = await platform.createWebSocketTransport(options.wsEndpoint);
|
||||
const transport = new platform.WebSocketTransport(options.wsEndpoint);
|
||||
return CRBrowser.connect(transport, options.slowMo);
|
||||
}
|
||||
|
||||
|
@ -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<FFBrowser> {
|
||||
const transport = await platform.createWebSocketTransport(options.wsEndpoint);
|
||||
const transport = new platform.WebSocketTransport(options.wsEndpoint);
|
||||
return FFBrowser.connect(transport, options.slowMo);
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,7 @@ export class WebKit implements BrowserType {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -236,36 +236,160 @@ function getMacVersion(): string {
|
||||
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) {
|
||||
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<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) => {
|
||||
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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,6 @@ async function setup(product, wsEndpoint) {
|
||||
}
|
||||
async function teardown() {
|
||||
await window.context.close();
|
||||
await window.browser.disconnect();
|
||||
await window.browser.close();
|
||||
}
|
||||
</script>
|
@ -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:<PORT>/frames/nested-frames.html',
|
||||
' http://localhost:<PORT>/frames/frame.html (aframe)',
|
||||
' 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);
|
||||
{
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user