2020-06-26 01:05:36 +02:00
|
|
|
/**
|
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-08-25 02:05:16 +02:00
|
|
|
import * as channels from '../protocol/channels';
|
2020-06-26 01:05:36 +02:00
|
|
|
import { Browser } from './browser';
|
2021-02-12 02:46:54 +01:00
|
|
|
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
2020-06-26 01:05:36 +02:00
|
|
|
import { ChannelOwner } from './channelOwner';
|
2020-08-01 02:00:36 +02:00
|
|
|
import { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions } from './types';
|
2021-02-11 15:36:15 +01:00
|
|
|
import WebSocket from 'ws';
|
2020-08-13 22:24:49 +02:00
|
|
|
import { Connection } from './connection';
|
|
|
|
|
import { Events } from './events';
|
2020-08-23 00:13:51 +02:00
|
|
|
import { TimeoutSettings } from '../utils/timeoutSettings';
|
2020-08-13 22:24:49 +02:00
|
|
|
import { ChildProcess } from 'child_process';
|
2020-08-18 18:37:40 +02:00
|
|
|
import { envObjectToArray } from './clientHelper';
|
2021-06-02 20:36:58 +02:00
|
|
|
import { assert, headersObjectToArray, makeWaitForNextTask, getUserAgent } from '../utils/utils';
|
2020-10-01 06:17:30 +02:00
|
|
|
import { kBrowserClosedError } from '../utils/errors';
|
2020-12-27 02:05:57 +01:00
|
|
|
import * as api from '../../types/types';
|
2021-04-12 20:14:54 +02:00
|
|
|
import type { Playwright } from './playwright';
|
2020-08-13 22:24:49 +02:00
|
|
|
|
|
|
|
|
export interface BrowserServerLauncher {
|
2020-12-27 02:05:57 +01:00
|
|
|
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
|
2020-08-13 22:24:49 +02:00
|
|
|
}
|
|
|
|
|
|
2020-12-27 02:05:57 +01:00
|
|
|
// This is here just for api generation and checking.
|
|
|
|
|
export interface BrowserServer extends api.BrowserServer {
|
2020-08-13 22:24:49 +02:00
|
|
|
process(): ChildProcess;
|
|
|
|
|
wsEndpoint(): string;
|
|
|
|
|
close(): Promise<void>;
|
|
|
|
|
kill(): Promise<void>;
|
|
|
|
|
}
|
2020-06-26 01:05:36 +02:00
|
|
|
|
2021-04-08 19:27:24 +02:00
|
|
|
export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, channels.BrowserTypeInitializer> implements api.BrowserType {
|
2020-08-13 22:24:49 +02:00
|
|
|
private _timeoutSettings = new TimeoutSettings();
|
|
|
|
|
_serverLauncher?: BrowserServerLauncher;
|
2020-07-09 03:42:04 +02:00
|
|
|
|
2020-08-25 02:05:16 +02:00
|
|
|
static from(browserType: channels.BrowserTypeChannel): BrowserType {
|
2020-07-11 03:00:10 +02:00
|
|
|
return (browserType as any)._object;
|
2020-07-09 03:42:04 +02:00
|
|
|
}
|
|
|
|
|
|
2020-08-25 02:05:16 +02:00
|
|
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserTypeInitializer) {
|
2020-07-27 19:21:39 +02:00
|
|
|
super(parent, type, guid, initializer);
|
2020-06-26 01:05:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
executablePath(): string {
|
2020-09-22 00:51:27 +02:00
|
|
|
if (!this._initializer.executablePath)
|
|
|
|
|
throw new Error('Browser is not supported on current platform');
|
2020-06-26 21:28:27 +02:00
|
|
|
return this._initializer.executablePath;
|
2020-06-26 01:05:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name(): string {
|
2020-06-26 21:28:27 +02:00
|
|
|
return this._initializer.name;
|
2020-06-26 01:05:36 +02:00
|
|
|
}
|
|
|
|
|
|
2020-07-30 02:26:59 +02:00
|
|
|
async launch(options: LaunchOptions = {}): Promise<Browser> {
|
2020-07-11 03:00:10 +02:00
|
|
|
const logger = options.logger;
|
2021-06-28 22:27:38 +02:00
|
|
|
return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => {
|
2020-07-23 03:05:07 +02:00
|
|
|
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
|
|
|
|
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
2020-08-25 02:05:16 +02:00
|
|
|
const launchOptions: channels.BrowserTypeLaunchParams = {
|
2020-07-17 18:32:27 +02:00
|
|
|
...options,
|
|
|
|
|
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
|
|
|
|
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
|
|
|
|
env: options.env ? envObjectToArray(options.env) : undefined,
|
|
|
|
|
};
|
2021-02-20 01:21:39 +01:00
|
|
|
const browser = Browser.from((await channel.launch(launchOptions)).browser);
|
2020-07-15 23:04:39 +02:00
|
|
|
browser._logger = logger;
|
|
|
|
|
return browser;
|
|
|
|
|
}, logger);
|
2020-06-26 01:05:36 +02:00
|
|
|
}
|
|
|
|
|
|
2020-12-27 02:05:57 +01:00
|
|
|
async launchServer(options: LaunchServerOptions = {}): Promise<api.BrowserServer> {
|
2020-08-13 22:24:49 +02:00
|
|
|
if (!this._serverLauncher)
|
|
|
|
|
throw new Error('Launching server is not supported');
|
|
|
|
|
return this._serverLauncher.launchServer(options);
|
2020-06-30 19:55:11 +02:00
|
|
|
}
|
|
|
|
|
|
2020-08-01 02:00:36 +02:00
|
|
|
async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> {
|
2021-06-28 22:27:38 +02:00
|
|
|
return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => {
|
2020-09-14 23:43:39 +02:00
|
|
|
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
2021-02-12 02:46:54 +01:00
|
|
|
const contextParams = await prepareBrowserContextParams(options);
|
|
|
|
|
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
|
|
|
|
...contextParams,
|
2020-07-17 18:32:27 +02:00
|
|
|
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
|
|
|
|
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
|
|
|
|
env: options.env ? envObjectToArray(options.env) : undefined,
|
2021-03-15 16:07:57 +01:00
|
|
|
channel: options.channel,
|
2020-07-16 22:36:22 +02:00
|
|
|
userDataDir,
|
|
|
|
|
};
|
2021-02-20 01:21:39 +01:00
|
|
|
const result = await channel.launchPersistentContext(persistentParams);
|
2020-07-15 23:04:39 +02:00
|
|
|
const context = BrowserContext.from(result.context);
|
2021-02-12 02:46:54 +01:00
|
|
|
context._options = contextParams;
|
2020-11-03 04:42:05 +01:00
|
|
|
context._logger = options.logger;
|
2020-07-15 23:04:39 +02:00
|
|
|
return context;
|
2020-11-03 04:42:05 +01:00
|
|
|
}, options.logger);
|
2020-06-26 01:05:36 +02:00
|
|
|
}
|
|
|
|
|
|
2021-07-22 16:55:23 +02:00
|
|
|
connect(options: api.ConnectOptions & { wsEndpoint?: string }): Promise<api.Browser>;
|
|
|
|
|
connect(wsEndpoint: string, options?: api.ConnectOptions): Promise<api.Browser>;
|
|
|
|
|
async connect(optionsOrWsEndpoint: string|(api.ConnectOptions & { wsEndpoint?: string }), options?: api.ConnectOptions): Promise<Browser>{
|
|
|
|
|
if (typeof optionsOrWsEndpoint === 'string')
|
|
|
|
|
return this._connect(optionsOrWsEndpoint, options);
|
|
|
|
|
assert(optionsOrWsEndpoint.wsEndpoint, 'options.wsEndpoint is required');
|
|
|
|
|
return this._connect(optionsOrWsEndpoint.wsEndpoint, optionsOrWsEndpoint);
|
|
|
|
|
}
|
|
|
|
|
async _connect(wsEndpoint: string, params: Partial<ConnectOptions> = {}): Promise<Browser> {
|
2020-12-04 07:28:11 +01:00
|
|
|
const logger = params.logger;
|
2021-06-02 20:36:58 +02:00
|
|
|
const paramsHeaders = Object.assign({'User-Agent': getUserAgent()}, params.headers);
|
2021-06-28 22:27:38 +02:00
|
|
|
return this._wrapApiCall(async () => {
|
2021-07-22 16:55:23 +02:00
|
|
|
const ws = new WebSocket(wsEndpoint, [], {
|
2020-08-13 22:24:49 +02:00
|
|
|
perMessageDeflate: false,
|
|
|
|
|
maxPayload: 256 * 1024 * 1024, // 256Mb,
|
2020-12-04 07:28:11 +01:00
|
|
|
handshakeTimeout: this._timeoutSettings.timeout(params),
|
2021-06-02 20:36:58 +02:00
|
|
|
headers: paramsHeaders,
|
2020-08-13 22:24:49 +02:00
|
|
|
});
|
2021-05-06 18:34:06 +02:00
|
|
|
const connection = new Connection(() => ws.close());
|
2020-08-13 22:24:49 +02:00
|
|
|
|
|
|
|
|
// The 'ws' module in node sometimes sends us multiple messages in a single task.
|
2020-12-04 07:28:11 +01:00
|
|
|
const waitForNextTask = params.slowMo
|
|
|
|
|
? (cb: () => any) => setTimeout(cb, params.slowMo)
|
2020-08-22 16:07:13 +02:00
|
|
|
: makeWaitForNextTask();
|
2020-08-13 22:24:49 +02:00
|
|
|
connection.onmessage = message => {
|
2021-05-06 18:34:06 +02:00
|
|
|
// Connection should handle all outgoing message in disconnected().
|
|
|
|
|
if (ws.readyState !== WebSocket.OPEN)
|
2020-08-13 22:24:49 +02:00
|
|
|
return;
|
|
|
|
|
ws.send(JSON.stringify(message));
|
|
|
|
|
};
|
|
|
|
|
ws.addEventListener('message', event => {
|
2021-04-23 23:52:27 +02:00
|
|
|
waitForNextTask(() => {
|
|
|
|
|
try {
|
2021-05-06 18:34:06 +02:00
|
|
|
// Since we may slow down the messages, but disconnect
|
|
|
|
|
// synchronously, we might come here with a message
|
|
|
|
|
// after disconnect.
|
|
|
|
|
if (!connection.isDisconnected())
|
|
|
|
|
connection.dispatch(JSON.parse(event.data));
|
2021-04-23 23:52:27 +02:00
|
|
|
} catch (e) {
|
2021-05-25 17:11:32 +02:00
|
|
|
console.error(`Playwright: Connection dispatch error`);
|
|
|
|
|
console.error(e);
|
2021-04-23 23:52:27 +02:00
|
|
|
ws.close();
|
|
|
|
|
}
|
|
|
|
|
});
|
2020-08-13 22:24:49 +02:00
|
|
|
});
|
2021-05-27 00:18:52 +02:00
|
|
|
|
|
|
|
|
let timeoutCallback = (e: Error) => {};
|
|
|
|
|
const timeoutPromise = new Promise<Browser>((f, r) => timeoutCallback = r);
|
|
|
|
|
const timer = params.timeout ? setTimeout(() => timeoutCallback(new Error(`Timeout ${params.timeout}ms exceeded.`)), params.timeout) : undefined;
|
|
|
|
|
|
|
|
|
|
const successPromise = new Promise<Browser>(async (fulfill, reject) => {
|
2020-12-04 07:28:11 +01:00
|
|
|
if ((params as any).__testHookBeforeCreateBrowser) {
|
2020-08-13 22:24:49 +02:00
|
|
|
try {
|
2020-12-04 07:28:11 +01:00
|
|
|
await (params as any).__testHookBeforeCreateBrowser();
|
2020-08-13 22:24:49 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
reject(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ws.addEventListener('open', async () => {
|
2021-05-25 17:11:32 +02:00
|
|
|
const prematureCloseListener = (event: { code: number, reason: string }) => {
|
|
|
|
|
reject(new Error(`WebSocket server disconnected (${event.code}) ${event.reason}`));
|
2021-01-12 00:53:45 +01:00
|
|
|
};
|
|
|
|
|
ws.addEventListener('close', prematureCloseListener);
|
2021-04-12 20:14:54 +02:00
|
|
|
const playwright = await connection.waitForObjectWithKnownName('Playwright') as Playwright;
|
2020-09-03 01:15:43 +02:00
|
|
|
|
2021-04-12 20:14:54 +02:00
|
|
|
if (!playwright._initializer.preLaunchedBrowser) {
|
|
|
|
|
reject(new Error('Malformed endpoint. Did you use launchServer method?'));
|
|
|
|
|
ws.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-09-03 01:15:43 +02:00
|
|
|
|
2021-04-12 20:14:54 +02:00
|
|
|
const browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
|
2020-08-13 22:24:49 +02:00
|
|
|
browser._logger = logger;
|
2021-05-06 18:34:06 +02:00
|
|
|
browser._remoteType = 'owns-connection';
|
2020-08-13 22:24:49 +02:00
|
|
|
const closeListener = () => {
|
|
|
|
|
// 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();
|
2021-05-06 18:34:06 +02:00
|
|
|
connection.didDisconnect(kBrowserClosedError);
|
2020-08-13 22:24:49 +02:00
|
|
|
};
|
2021-01-12 00:53:45 +01:00
|
|
|
ws.removeEventListener('close', prematureCloseListener);
|
2020-08-13 22:24:49 +02:00
|
|
|
ws.addEventListener('close', closeListener);
|
|
|
|
|
browser.on(Events.Browser.Disconnected, () => {
|
2021-04-12 20:14:54 +02:00
|
|
|
playwright._cleanup();
|
2020-08-13 22:24:49 +02:00
|
|
|
ws.removeEventListener('close', closeListener);
|
|
|
|
|
ws.close();
|
|
|
|
|
});
|
2021-05-25 17:11:32 +02:00
|
|
|
if (params._forwardPorts) {
|
|
|
|
|
try {
|
2021-06-02 23:35:17 +02:00
|
|
|
await playwright._enablePortForwarding(params._forwardPorts);
|
2021-05-25 17:11:32 +02:00
|
|
|
} catch (err) {
|
|
|
|
|
reject(err);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-13 22:24:49 +02:00
|
|
|
fulfill(browser);
|
|
|
|
|
});
|
|
|
|
|
ws.addEventListener('error', event => {
|
|
|
|
|
ws.close();
|
2021-01-12 00:53:45 +01:00
|
|
|
reject(new Error(event.message + '. Most likely ws endpoint is incorrect'));
|
2020-08-13 22:24:49 +02:00
|
|
|
});
|
|
|
|
|
});
|
2021-05-27 00:18:52 +02:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return await Promise.race([successPromise, timeoutPromise]);
|
|
|
|
|
} finally {
|
|
|
|
|
if (timer)
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
}
|
2020-07-15 23:04:39 +02:00
|
|
|
}, logger);
|
2020-06-26 01:05:36 +02:00
|
|
|
}
|
2021-02-10 23:00:02 +01:00
|
|
|
|
2021-07-22 16:55:23 +02:00
|
|
|
connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<api.Browser>;
|
|
|
|
|
connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise<api.Browser>;
|
|
|
|
|
connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string, options?: api.ConnectOverCDPOptions) {
|
|
|
|
|
if (typeof endpointURLOrOptions === 'string')
|
|
|
|
|
return this._connectOverCDP(endpointURLOrOptions, options);
|
|
|
|
|
const endpointURL = 'endpointURL' in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint;
|
|
|
|
|
assert(endpointURL, 'Cannot connect over CDP without wsEndpoint.');
|
|
|
|
|
return this.connectOverCDP(endpointURL, endpointURLOrOptions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async _connectOverCDP(endpointURL: string, params: api.ConnectOverCDPOptions = {}): Promise<Browser> {
|
2021-02-10 23:00:02 +01:00
|
|
|
if (this.name() !== 'chromium')
|
|
|
|
|
throw new Error('Connecting over CDP is only supported in Chromium.');
|
|
|
|
|
const logger = params.logger;
|
2021-06-28 22:27:38 +02:00
|
|
|
return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => {
|
2021-06-02 20:36:58 +02:00
|
|
|
const paramsHeaders = Object.assign({'User-Agent': getUserAgent()}, params.headers);
|
|
|
|
|
const headers = paramsHeaders ? headersObjectToArray(paramsHeaders) : undefined;
|
2021-02-20 01:21:39 +01:00
|
|
|
const result = await channel.connectOverCDP({
|
2021-02-12 02:46:54 +01:00
|
|
|
sdkLanguage: 'javascript',
|
2021-07-22 16:55:23 +02:00
|
|
|
endpointURL,
|
2021-04-23 23:52:27 +02:00
|
|
|
headers,
|
2021-02-10 23:00:02 +01:00
|
|
|
slowMo: params.slowMo,
|
|
|
|
|
timeout: params.timeout
|
|
|
|
|
});
|
|
|
|
|
const browser = Browser.from(result.browser);
|
|
|
|
|
if (result.defaultContext)
|
|
|
|
|
browser._contexts.add(BrowserContext.from(result.defaultContext));
|
2021-05-06 18:34:06 +02:00
|
|
|
browser._remoteType = 'uses-connection';
|
2021-02-10 23:00:02 +01:00
|
|
|
browser._logger = logger;
|
|
|
|
|
return browser;
|
|
|
|
|
}, logger);
|
|
|
|
|
}
|
2020-06-26 01:05:36 +02:00
|
|
|
}
|