Skip to content

Commit 27d30fe

Browse files
authored
chore: encapsulate more launching logic in BrowserType (#2339)
1 parent aac5bf2 commit 27d30fe

File tree

7 files changed

+87
-103
lines changed

7 files changed

+87
-103
lines changed

src/server/browserServer.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@
1717
import { ChildProcess } from 'child_process';
1818
import { EventEmitter } from 'events';
1919
import { helper } from '../helper';
20-
import { RootLogger } from '../logger';
21-
import { LaunchOptions, processBrowserArgOptions } from './browserType';
22-
import { ConnectionTransport } from '../transport';
2320

2421
export class WebSocketWrapper {
2522
readonly wsEndpoint: string;
2623
private _bindings: (Map<any, any> | Set<any>)[];
24+
2725
constructor(wsEndpoint: string, bindings: (Map<any, any>|Set<any>)[]) {
2826
this.wsEndpoint = wsEndpoint;
2927
this._bindings = bindings;
@@ -53,24 +51,12 @@ export class WebSocketWrapper {
5351
export class BrowserServer extends EventEmitter {
5452
private _process: ChildProcess;
5553
private _gracefullyClose: (() => Promise<void>);
56-
private _webSocketWrapper: WebSocketWrapper | null = null;
57-
readonly _launchOptions: LaunchOptions;
58-
readonly _logger: RootLogger;
59-
readonly _downloadsPath: string | undefined;
60-
readonly _transport: ConnectionTransport;
61-
readonly _headful: boolean;
54+
_webSocketWrapper: WebSocketWrapper | null = null;
6255

63-
constructor(options: LaunchOptions, process: ChildProcess, gracefullyClose: () => Promise<void>, transport: ConnectionTransport, downloadsPath: string | undefined, webSocketWrapper: WebSocketWrapper | null) {
56+
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>) {
6457
super();
65-
this._launchOptions = options;
66-
this._headful = !processBrowserArgOptions(options).headless;
67-
this._logger = new RootLogger(options.logger);
68-
6958
this._process = process;
7059
this._gracefullyClose = gracefullyClose;
71-
this._transport = transport;
72-
this._downloadsPath = downloadsPath;
73-
this._webSocketWrapper = webSocketWrapper;
7460
}
7561

7662
process(): ChildProcess {

src/server/browserType.ts

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ type ConnectOptions = {
5959
logger?: Logger,
6060
timeout?: number,
6161
};
62-
export type LaunchType = 'local' | 'server' | 'persistent';
6362
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
6463
type LaunchServerOptions = LaunchOptionsBase & { port?: number };
6564

@@ -73,6 +72,7 @@ export interface BrowserType {
7372
}
7473

7574
const mkdtempAsync = util.promisify(fs.mkdtemp);
75+
const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-');
7676

7777
export abstract class BrowserTypeBase implements BrowserType {
7878
private _name: string;
@@ -100,24 +100,37 @@ export abstract class BrowserTypeBase implements BrowserType {
100100

101101
async launch(options: LaunchOptions = {}): Promise<Browser> {
102102
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
103-
return this._innerLaunch('local', options, undefined);
103+
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
104+
return this._innerLaunch(options, undefined);
104105
}
105106

106107
async launchPersistentContext(userDataDir: string, options: LaunchOptions & PersistentContextOptions = {}): Promise<BrowserContext> {
108+
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
107109
const persistent = validatePersistentContextOptions(options);
108-
const browser = await this._innerLaunch('persistent', options, persistent, userDataDir);
110+
const browser = await this._innerLaunch(options, persistent, userDataDir);
109111
return browser._defaultContext!;
110112
}
111113

112-
async _innerLaunch(launchType: LaunchType, options: LaunchOptions, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
114+
async _innerLaunch(options: LaunchOptions, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
113115
const deadline = TimeoutSettings.computeDeadline(options.timeout, 30000);
114116
const logger = new RootLogger(options.logger);
115117
logger.startLaunchRecording();
116118

117119
let browserServer: BrowserServer | undefined;
118120
try {
119-
browserServer = await this._launchServer(options, launchType, logger, deadline, userDataDir);
120-
const promise = this._innerLaunchPromise(browserServer, options, persistent);
121+
const launched = await this._launchServer(options, !!persistent, logger, deadline, userDataDir);
122+
browserServer = launched.browserServer;
123+
const browserOptions: BrowserOptions = {
124+
slowMo: options.slowMo,
125+
persistent,
126+
headful: !processBrowserArgOptions(options).headless,
127+
logger,
128+
downloadsPath: launched.downloadsPath,
129+
ownedServer: browserServer,
130+
};
131+
copyTestHooks(options, browserOptions);
132+
const hasCustomArguments = !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs);
133+
const promise = this._innerCreateBrowser(launched.transport, browserOptions, hasCustomArguments);
121134
const browser = await helper.waitWithDeadline(promise, 'the browser to launch', deadline, 'pw:browser*');
122135
return browser;
123136
} catch (e) {
@@ -132,34 +145,23 @@ export abstract class BrowserTypeBase implements BrowserType {
132145
}
133146
}
134147

135-
async _innerLaunchPromise(browserServer: BrowserServer, options: LaunchOptions, persistent: PersistentContextOptions | undefined): Promise<BrowserBase> {
136-
if ((options as any).__testHookBeforeCreateBrowser)
137-
await (options as any).__testHookBeforeCreateBrowser();
138-
139-
const browserOptions: BrowserOptions = {
140-
slowMo: options.slowMo,
141-
persistent,
142-
headful: browserServer._headful,
143-
logger: browserServer._logger,
144-
downloadsPath: browserServer._downloadsPath,
145-
ownedServer: browserServer,
146-
};
147-
for (const [key, value] of Object.entries(options)) {
148-
if (key.startsWith('__testHook'))
149-
(browserOptions as any)[key] = value;
150-
}
151-
152-
const browser = await this._connectToTransport(browserServer._transport, browserOptions);
153-
if (persistent && (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))) {
154-
const context = browser._defaultContext!;
155-
await context._loadDefaultContext();
156-
}
148+
async _innerCreateBrowser(transport: ConnectionTransport, browserOptions: BrowserOptions, hasCustomArguments: boolean): Promise<BrowserBase> {
149+
if ((browserOptions as any).__testHookBeforeCreateBrowser)
150+
await (browserOptions as any).__testHookBeforeCreateBrowser();
151+
const browser = await this._connectToTransport(transport, browserOptions);
152+
// We assume no control when using custom arguments, and do not prepare the default context in that case.
153+
if (browserOptions.persistent && !hasCustomArguments)
154+
await browser._defaultContext!._loadDefaultContext();
157155
return browser;
158156
}
159157

160158
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
159+
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launchServer`. Use `browserType.launchPersistentContext` instead');
160+
const { port = 0 } = options;
161161
const logger = new RootLogger(options.logger);
162-
return this._launchServer(options, 'server', logger, TimeoutSettings.computeDeadline(options.timeout, 30000));
162+
const { browserServer, transport } = await this._launchServer(options, false, logger, TimeoutSettings.computeDeadline(options.timeout, 30000));
163+
browserServer._webSocketWrapper = this._wrapTransportWithWebSocket(transport, logger, port);
164+
return browserServer;
163165
}
164166

165167
async connect(options: ConnectOptions): Promise<Browser> {
@@ -170,7 +172,12 @@ export abstract class BrowserTypeBase implements BrowserType {
170172
let transport: ConnectionTransport | undefined;
171173
try {
172174
transport = await WebSocketTransport.connect(options.wsEndpoint, logger, deadline);
173-
const promise = this._innerConnectPromise(transport, options, logger);
175+
const browserOptions: BrowserOptions = {
176+
slowMo: options.slowMo,
177+
logger,
178+
};
179+
copyTestHooks(options, browserOptions);
180+
const promise = this._innerCreateBrowser(transport, browserOptions, false);
174181
const browser = await helper.waitWithDeadline(promise, 'connect to browser', deadline, 'pw:browser*');
175182
logger.stopLaunchRecording();
176183
return browser;
@@ -189,13 +196,7 @@ export abstract class BrowserTypeBase implements BrowserType {
189196
}
190197
}
191198

192-
async _innerConnectPromise(transport: ConnectionTransport, options: ConnectOptions, logger: RootLogger): Promise<Browser> {
193-
if ((options as any).__testHookBeforeCreateBrowser)
194-
await (options as any).__testHookBeforeCreateBrowser();
195-
return this._connectToTransport(transport, { slowMo: options.slowMo, logger });
196-
}
197-
198-
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer> {
199+
private async _launchServer(options: LaunchServerOptions, isPersistent: boolean, logger: RootLogger, deadline: number, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, transport: ConnectionTransport }> {
199200
const {
200201
ignoreDefaultArgs = false,
201202
args = [],
@@ -204,21 +205,20 @@ export abstract class BrowserTypeBase implements BrowserType {
204205
handleSIGINT = true,
205206
handleSIGTERM = true,
206207
handleSIGHUP = true,
207-
port = 0,
208208
} = options;
209-
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
210209

211-
let temporaryUserDataDir: string | null = null;
210+
const downloadsPath = await mkdtempAsync(DOWNLOADS_FOLDER);
211+
const tempDirectories = [downloadsPath];
212212
if (!userDataDir) {
213213
userDataDir = await mkdtempAsync(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
214-
temporaryUserDataDir = userDataDir;
214+
tempDirectories.push(userDataDir);
215215
}
216216

217217
const browserArguments = [];
218218
if (!ignoreDefaultArgs)
219-
browserArguments.push(...this._defaultArgs(options, launchType, userDataDir));
219+
browserArguments.push(...this._defaultArgs(options, isPersistent, userDataDir));
220220
else if (Array.isArray(ignoreDefaultArgs))
221-
browserArguments.push(...this._defaultArgs(options, launchType, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
221+
browserArguments.push(...this._defaultArgs(options, isPersistent, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
222222
else
223223
browserArguments.push(...args);
224224

@@ -230,7 +230,7 @@ export abstract class BrowserTypeBase implements BrowserType {
230230
// "Cannot access 'browserServer' before initialization" if something went wrong.
231231
let transport: ConnectionTransport | undefined = undefined;
232232
let browserServer: BrowserServer | undefined = undefined;
233-
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
233+
const { launchedProcess, gracefullyClose } = await launchProcess({
234234
executablePath: executable,
235235
args: browserArguments,
236236
env: this._amendEnvironment(env, userDataDir, executable, browserArguments),
@@ -239,7 +239,7 @@ export abstract class BrowserTypeBase implements BrowserType {
239239
handleSIGHUP,
240240
logger,
241241
pipe: !this._webSocketRegexNotPipe,
242-
tempDir: temporaryUserDataDir || undefined,
242+
tempDirectories,
243243
attemptToGracefullyClose: async () => {
244244
if ((options as any).__testHookGracefullyClose)
245245
await (options as any).__testHookGracefullyClose();
@@ -269,14 +269,20 @@ export abstract class BrowserTypeBase implements BrowserType {
269269
helper.killProcess(launchedProcess);
270270
throw e;
271271
}
272-
browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, transport, downloadsPath,
273-
launchType === 'server' ? this._wrapTransportWithWebSocket(transport, logger, port) : null);
274-
return browserServer;
272+
browserServer = new BrowserServer(launchedProcess, gracefullyClose);
273+
return { browserServer, downloadsPath, transport };
275274
}
276275

277-
abstract _defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[];
276+
abstract _defaultArgs(options: BrowserArgOptions, isPersistent: boolean, userDataDir: string): string[];
278277
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
279278
abstract _wrapTransportWithWebSocket(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketWrapper;
280279
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
281280
abstract _attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void;
282281
}
282+
283+
function copyTestHooks(from: object, to: object) {
284+
for (const [key, value] of Object.entries(from)) {
285+
if (key.startsWith('__testHook'))
286+
(to as any)[key] = value;
287+
}
288+
}

src/server/chromium.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { CRBrowser } from '../chromium/crBrowser';
2121
import * as ws from 'ws';
2222
import { Env } from './processLauncher';
2323
import { kBrowserCloseMessageId } from '../chromium/crConnection';
24-
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
24+
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions } from './browserType';
2525
import { WebSocketWrapper } from './browserServer';
2626
import { ConnectionTransport, ProtocolRequest } from '../transport';
2727
import { InnerLogger, logError } from '../logger';
@@ -66,7 +66,7 @@ export class Chromium extends BrowserTypeBase {
6666
return wrapTransportWithWebSocket(transport, logger, port);
6767
}
6868

69-
_defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[] {
69+
_defaultArgs(options: BrowserArgOptions, isPersistent: boolean, userDataDir: string): string[] {
7070
const { devtools, headless } = processBrowserArgOptions(options);
7171
const { args = [] } = options;
7272
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
@@ -89,7 +89,7 @@ export class Chromium extends BrowserTypeBase {
8989
);
9090
}
9191
chromeArguments.push(...args);
92-
if (launchType === 'persistent')
92+
if (isPersistent)
9393
chromeArguments.push('about:blank');
9494
else
9595
chromeArguments.push('--no-startup-window');

src/server/electron.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export class Electron {
183183
logger,
184184
pipe: true,
185185
cwd: options.cwd,
186-
omitDownloads: true,
186+
tempDirectories: [],
187187
attemptToGracefullyClose: () => app!.close(),
188188
onkill: (exitCode, signal) => {
189189
if (app)
@@ -198,7 +198,7 @@ export class Electron {
198198

199199
const chromeMatch = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, helper.timeUntilDeadline(deadline), timeoutError);
200200
const chromeTransport = await WebSocketTransport.connect(chromeMatch[1], logger, deadline);
201-
const browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, chromeTransport, undefined, null);
201+
const browserServer = new BrowserServer(launchedProcess, gracefullyClose);
202202
const browser = await CRBrowser.connect(chromeTransport, { headful: true, logger, persistent: { viewport: null }, ownedServer: browserServer });
203203
app = new ElectronApplication(logger, browser, nodeConnection);
204204
await app._init();

src/server/firefox.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { FFBrowser } from '../firefox/ffBrowser';
2222
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
2323
import { helper } from '../helper';
2424
import { WebSocketWrapper } from './browserServer';
25-
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions, LaunchType } from './browserType';
25+
import { BrowserArgOptions, BrowserTypeBase, processBrowserArgOptions } from './browserType';
2626
import { Env } from './processLauncher';
2727
import { ConnectionTransport, SequenceNumberMixer } from '../transport';
2828
import { InnerLogger, logError } from '../logger';
@@ -56,7 +56,7 @@ export class Firefox extends BrowserTypeBase {
5656
return wrapTransportWithWebSocket(transport, logger, port);
5757
}
5858

59-
_defaultArgs(options: BrowserArgOptions, launchType: LaunchType, userDataDir: string): string[] {
59+
_defaultArgs(options: BrowserArgOptions, isPersistent: boolean, userDataDir: string): string[] {
6060
const { devtools, headless } = processBrowserArgOptions(options);
6161
const { args = [] } = options;
6262
if (devtools)
@@ -77,7 +77,7 @@ export class Firefox extends BrowserTypeBase {
7777
firefoxArguments.push(`-profile`, userDataDir);
7878
firefoxArguments.push('-juggler', '0');
7979
firefoxArguments.push(...args);
80-
if (launchType === 'persistent')
80+
if (isPersistent)
8181
firefoxArguments.push('about:blank');
8282
else
8383
firefoxArguments.push('-silent');

0 commit comments

Comments
 (0)