chore: simplify client-side browserType.connect routine (#8596)

This commit is contained in:
Pavel Feldman 2021-08-31 15:56:55 -07:00 committed by GitHub
parent 26e7c2825b
commit 0fd5078b2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 65 additions and 63 deletions

View file

@ -23,10 +23,10 @@ import { Connection } from './connection';
import { Events } from './events'; import { Events } from './events';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { envObjectToArray } from './clientHelper'; import { envObjectToArray } from './clientHelper';
import { assert, headersObjectToArray, getUserAgent } from '../utils/utils'; import { assert, headersObjectToArray, getUserAgent, monotonicTime } from '../utils/utils';
import * as api from '../../types/types'; import * as api from '../../types/types';
import { kBrowserClosedError } from '../utils/errors'; import { kBrowserClosedError } from '../utils/errors';
import { ManualPromise } from '../utils/async'; import { raceAgainstDeadline } from '../utils/async';
export interface BrowserServerLauncher { export interface BrowserServerLauncher {
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>; launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
@ -128,12 +128,25 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
async _connect(wsEndpoint: string, params: Partial<ConnectOptions> = {}): Promise<Browser> { async _connect(wsEndpoint: string, params: Partial<ConnectOptions> = {}): Promise<Browser> {
const logger = params.logger; const logger = params.logger;
return await this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => { return await this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => {
const timeoutPromise = new ManualPromise<Browser>(); const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
const timer = params.timeout ? setTimeout(() => timeoutPromise.reject(new Error(`Timeout ${params.timeout}ms exceeded.`)), params.timeout) : undefined; let browser: Browser;
const { pipe } = await channel.connect({ wsEndpoint, headers: params.headers, timeout: params.timeout }); const { pipe } = await channel.connect({ wsEndpoint, headers: params.headers, timeout: params.timeout });
const connection = new Connection(() => pipe.close().catch(() => {})); const closePipe = () => pipe.close().catch(() => {});
connection.onmessage = message => pipe.send({ message }).catch(() => { }); const connection = new Connection(closePipe);
const onPipeClosed = () => {
// Emulate all pages, contexts and the browser closing upon disconnect.
for (const context of browser?.contexts() || []) {
for (const page of context.pages())
page._onClose();
context._onClose();
}
browser?._didClose();
connection.didDisconnect(kBrowserClosedError);
};
pipe.on('closed', onPipeClosed);
connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed);
pipe.on('message', ({ message }) => { pipe.on('message', ({ message }) => {
try { try {
if (!connection!.isDisconnected()) if (!connection!.isDisconnected())
@ -141,64 +154,42 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
} catch (e) { } catch (e) {
console.error(`Playwright: Connection dispatch error`); console.error(`Playwright: Connection dispatch error`);
console.error(e); console.error(e);
pipe.close().catch(() => {}); closePipe();
} }
}); });
const successPromise = new Promise<Browser>(async (fulfill, reject) => { const createBrowserPromise = new Promise<Browser>(async (fulfill, reject) => {
if ((params as any).__testHookBeforeCreateBrowser) { try {
try { // For tests.
if ((params as any).__testHookBeforeCreateBrowser)
await (params as any).__testHookBeforeCreateBrowser(); await (params as any).__testHookBeforeCreateBrowser();
} catch (e) {
reject(e);
}
}
const prematureCloseListener = (params: { error?: channels.SerializedError }) => {
reject(new Error(`WebSocket server disconnected ${params.error!.error?.message}`));
};
pipe.on('closed', prematureCloseListener);
pipe.on('opened', async () => {
const playwright = await connection!.initializePlaywright(); const playwright = await connection!.initializePlaywright();
if (!playwright._initializer.preLaunchedBrowser) { if (!playwright._initializer.preLaunchedBrowser) {
reject(new Error('Malformed endpoint. Did you use launchServer method?')); reject(new Error('Malformed endpoint. Did you use launchServer method?'));
pipe.close().catch(() => {}); closePipe();
return; return;
} }
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
const browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
browser._logger = logger; browser._logger = logger;
browser._remoteType = 'owns-connection'; browser._remoteType = 'owns-connection';
browser._setBrowserType((playwright as any)[browser._name]); browser._setBrowserType((playwright as any)[browser._name]);
const closeListener = (param: { error?: Error }) => {
// Emulate all pages, contexts and the browser closing upon disconnect.
for (const context of browser.contexts()) {
for (const page of context.pages())
page._onClose();
context._onClose();
}
browser._didClose();
connection.didDisconnect(kBrowserClosedError);
if (param.error)
reject(new Error(param.error + '. Most likely ws endpoint is incorrect'));
};
pipe.off('closed', prematureCloseListener);
pipe.on('closed', closeListener);
browser.on(Events.Browser.Disconnected, () => { browser.on(Events.Browser.Disconnected, () => {
playwright._cleanup(); playwright._cleanup();
pipe.off('closed', closeListener); closePipe();
pipe.close().catch(() => {});
}); });
fulfill(browser); fulfill(browser);
}); } catch (e) {
reject(e);
}
}); });
try { const result = await raceAgainstDeadline(createBrowserPromise, deadline);
return await Promise.race([successPromise, timeoutPromise]); if (result.result) {
} finally { return result.result;
if (timer) } else {
clearTimeout(timer); closePipe();
throw new Error(`Timeout ${params.timeout}ms exceeded`);
} }
}, logger); }, logger);
} }

View file

@ -23,6 +23,7 @@ import { CallMetadata } from '../server/instrumentation';
import WebSocket from 'ws'; import WebSocket from 'ws';
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher'; import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
import { getUserAgent, makeWaitForNextTask } from '../utils/utils'; import { getUserAgent, makeWaitForNextTask } from '../utils/utils';
import { ManualPromise } from '../utils/async';
export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.BrowserTypeInitializer, channels.BrowserTypeEvents> implements channels.BrowserTypeChannel { export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.BrowserTypeInitializer, channels.BrowserTypeEvents> implements channels.BrowserTypeChannel {
constructor(scope: DispatcherScope, browserType: BrowserType) { constructor(scope: DispatcherScope, browserType: BrowserType) {
@ -59,13 +60,21 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
const ws = new WebSocket(params.wsEndpoint, [], { const ws = new WebSocket(params.wsEndpoint, [], {
perMessageDeflate: false, perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb, maxPayload: 256 * 1024 * 1024, // 256Mb,
handshakeTimeout: params.timeout || 30000, handshakeTimeout: params.timeout,
headers: paramsHeaders, headers: paramsHeaders,
}); });
const pipe = new JsonPipeDispatcher(this._scope); const pipe = new JsonPipeDispatcher(this._scope);
ws.on('open', () => pipe.wasOpened()); const openPromise = new ManualPromise<{ pipe: JsonPipeDispatcher }>();
ws.on('open', () => openPromise.resolve({ pipe }));
ws.on('close', () => pipe.wasClosed()); ws.on('close', () => pipe.wasClosed());
ws.on('error', error => pipe.wasClosed(error)); ws.on('error', error => {
if (openPromise.isDone()) {
pipe.wasClosed(error);
} else {
pipe.dispose();
openPromise.reject(error);
}
});
pipe.on('close', () => ws.close()); pipe.on('close', () => ws.close());
pipe.on('message', message => ws.send(JSON.stringify(message))); pipe.on('message', message => ws.send(JSON.stringify(message)));
ws.addEventListener('message', event => { ws.addEventListener('message', event => {
@ -77,6 +86,6 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
} }
}); });
}); });
return { pipe }; return openPromise;
} }
} }

View file

@ -36,11 +36,6 @@ export class JsonPipeDispatcher extends Dispatcher<{ guid: string }, channels.Js
} }
} }
wasOpened(): void {
if (!this._disposed)
this._dispatchEvent('opened');
}
dispatch(message: Object) { dispatch(message: Object) {
if (!this._disposed) if (!this._disposed)
this._dispatchEvent('message', { message }); this._dispatchEvent('message', { message });
@ -53,4 +48,8 @@ export class JsonPipeDispatcher extends Dispatcher<{ guid: string }, channels.Js
this._dispose(); this._dispose();
} }
} }
dispose() {
this._dispose();
}
} }

View file

@ -3524,13 +3524,11 @@ export type AndroidElementInfo = {
// ----------- JsonPipe ----------- // ----------- JsonPipe -----------
export type JsonPipeInitializer = {}; export type JsonPipeInitializer = {};
export interface JsonPipeChannel extends Channel { export interface JsonPipeChannel extends Channel {
on(event: 'opened', callback: (params: JsonPipeOpenedEvent) => void): this;
on(event: 'message', callback: (params: JsonPipeMessageEvent) => void): this; on(event: 'message', callback: (params: JsonPipeMessageEvent) => void): this;
on(event: 'closed', callback: (params: JsonPipeClosedEvent) => void): this; on(event: 'closed', callback: (params: JsonPipeClosedEvent) => void): this;
send(params: JsonPipeSendParams, metadata?: Metadata): Promise<JsonPipeSendResult>; send(params: JsonPipeSendParams, metadata?: Metadata): Promise<JsonPipeSendResult>;
close(params?: JsonPipeCloseParams, metadata?: Metadata): Promise<JsonPipeCloseResult>; close(params?: JsonPipeCloseParams, metadata?: Metadata): Promise<JsonPipeCloseResult>;
} }
export type JsonPipeOpenedEvent = {};
export type JsonPipeMessageEvent = { export type JsonPipeMessageEvent = {
message: any, message: any,
}; };
@ -3547,7 +3545,6 @@ export type JsonPipeCloseOptions = {};
export type JsonPipeCloseResult = void; export type JsonPipeCloseResult = void;
export interface JsonPipeEvents { export interface JsonPipeEvents {
'opened': JsonPipeOpenedEvent;
'message': JsonPipeMessageEvent; 'message': JsonPipeMessageEvent;
'closed': JsonPipeClosedEvent; 'closed': JsonPipeClosedEvent;
} }

View file

@ -2852,8 +2852,6 @@ JsonPipe:
events: events:
opened:
message: message:
parameters: parameters:
message: json message: json

View file

@ -64,12 +64,20 @@ test('should be able to connect two browsers at the same time', async ({browserT
await browser2.close(); await browser2.close();
}); });
test('should timeout while connecting', async ({browserType, startRemoteServer, server}) => { test('should timeout in socket while connecting', async ({browserType, startRemoteServer, server}) => {
const e = await browserType.connect({
wsEndpoint: `ws://localhost:${server.PORT}/ws`,
timeout: 1,
}).catch(e => e);
expect(e.message).toContain('browserType.connect: Opening handshake has timed out');
});
test('should timeout in connect while connecting', async ({browserType, startRemoteServer, server}) => {
const e = await browserType.connect({ const e = await browserType.connect({
wsEndpoint: `ws://localhost:${server.PORT}/ws`, wsEndpoint: `ws://localhost:${server.PORT}/ws`,
timeout: 100, timeout: 100,
}).catch(e => e); }).catch(e => e);
expect(e.message).toContain('browserType.connect: Timeout 100ms exceeded.'); expect(e.message).toContain('browserType.connect: Timeout 100ms exceeded');
}); });
test('should send extra headers with connect request', async ({browserType, startRemoteServer, server}) => { test('should send extra headers with connect request', async ({browserType, startRemoteServer, server}) => {

View file

@ -57,7 +57,7 @@ it.describe('launch server', () => {
const browserServer = await browserType.launchServer(browserOptions); const browserServer = await browserType.launchServer(browserOptions);
const error = await browserType.connect({ wsEndpoint: browserServer.wsEndpoint() + '-foo' }).catch(e => e); const error = await browserType.connect({ wsEndpoint: browserServer.wsEndpoint() + '-foo' }).catch(e => e);
await browserServer.close(); await browserServer.close();
expect(error.message).toContain('WebSocket server disconnected'); expect(error.message).toContain('Unexpected server response: 400');
}); });
it('should fire "close" event during kill', async ({browserType, browserOptions}) => { it('should fire "close" event during kill', async ({browserType, browserOptions}) => {