chore: simplify client-side browserType.connect routine (#8596)
This commit is contained in:
parent
26e7c2825b
commit
0fd5078b2b
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2852,8 +2852,6 @@ JsonPipe:
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
|
||||||
opened:
|
|
||||||
|
|
||||||
message:
|
message:
|
||||||
parameters:
|
parameters:
|
||||||
message: json
|
message: json
|
||||||
|
|
|
||||||
|
|
@ -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}) => {
|
||||||
|
|
|
||||||
|
|
@ -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}) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue