Skip to content

Commit a2ab645

Browse files
authored
feat(launch): introduce client, server & persistent launch modes (2) (#840)
1 parent 0f1a42a commit a2ab645

31 files changed

+479
-422
lines changed

README.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ const playwright = require('playwright');
4040
(async () => {
4141
for (const browserType of ['chromium', 'firefox', 'webkit']) {
4242
const browser = await playwright[browserType].launch();
43-
const context = await browser.newContext();
44-
const page = await context.newPage('http://whatsmyuseragent.org/');
45-
43+
const page = await browser.newPage('http://whatsmyuseragent.org/');
4644
await page.screenshot({ path: `example-${browserType}.png` });
4745
await browser.close();
4846
}
@@ -59,14 +57,13 @@ const iPhone11 = devices['iPhone 11 Pro'];
5957

6058
(async () => {
6159
const browser = await webkit.launch();
62-
const context = await browser.newContext({
60+
const page = await browser.newPage('https://maps.google.com', {
6361
viewport: iPhone11.viewport,
6462
userAgent: iPhone11.userAgent,
6563
geolocation: { longitude: 12.492507, latitude: 41.889938 },
6664
permissions: { 'https://www.google.com': ['geolocation'] }
6765
});
6866

69-
const page = await context.newPage('https://maps.google.com');
7067
await page.click('text="Your location"');
7168
await page.waitForRequest(/.*preview\/pwa/);
7269
await page.screenshot({ path: 'colosseum-iphone.png' });
@@ -82,14 +79,12 @@ const pixel2 = devices['Pixel 2'];
8279

8380
(async () => {
8481
const browser = await chromium.launch();
85-
const context = await browser.newContext({
82+
const page = await browser.newPage('https://maps.google.com', {
8683
viewport: pixel2.viewport,
8784
userAgent: pixel2.userAgent,
8885
geolocation: { longitude: 12.492507, latitude: 41.889938 },
8986
permissions: { 'https://www.google.com': ['geolocation'] }
9087
});
91-
92-
const page = await context.newPage('https://maps.google.com');
9388
await page.click('text="Your location"');
9489
await page.waitForRequest(/.*pwa\/net.js.*/);
9590
await page.screenshot({ path: 'colosseum-android.png' });
@@ -106,10 +101,7 @@ const { firefox } = require('playwright');
106101

107102
(async () => {
108103
const browser = await firefox.launch();
109-
const context = await browser.newContext();
110-
const page = await context.newPage();
111-
112-
await page.goto('https://www.example.com/');
104+
const page = await browser.newPage('https://www.example.com/');
113105
const dimensions = await page.evaluate(() => {
114106
return {
115107
width: document.documentElement.clientWidth,

docs/api.md

Lines changed: 103 additions & 73 deletions
Large diffs are not rendered by default.

src/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ export { FFBrowser as FirefoxBrowser } from './firefox/ffBrowser';
3737
export { WKBrowser as WebKitBrowser } from './webkit/wkBrowser';
3838

3939
export { BrowserType } from './server/browserType';
40-
export { BrowserApp } from './server/browserApp';
40+
export { BrowserServer } from './server/browserServer';

src/browser.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
import { BrowserContext, BrowserContextOptions } from './browserContext';
1818
import * as platform from './platform';
19+
import { Page } from './page';
1920

2021
export interface Browser extends platform.EventEmitterType {
2122
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
2223
browserContexts(): BrowserContext[];
23-
defaultContext(): BrowserContext;
24-
24+
pages(): Promise<Page[]>;
25+
newPage(url?: string, options?: BrowserContextOptions): Promise<Page>;
2526
disconnect(): Promise<void>;
2627
isConnected(): boolean;
2728
close(): Promise<void>;
@@ -31,3 +32,15 @@ export type ConnectOptions = {
3132
slowMo?: number,
3233
wsEndpoint: string
3334
};
35+
36+
export async function collectPages(browser: Browser): Promise<Page[]> {
37+
const result: Promise<Page[]>[] = [];
38+
for (const browserContext of browser.browserContexts())
39+
result.push(browserContext.pages());
40+
const pages: Page[] = [];
41+
for (const group of await Promise.all(result))
42+
pages.push(...group);
43+
return pages;
44+
}
45+
46+
export type LaunchType = 'local' | 'server' | 'persistent';

src/chromium/crBrowser.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Page, Worker } from '../page';
2424
import { CRTarget } from './crTarget';
2525
import { Protocol } from './protocol';
2626
import { CRPage } from './crPage';
27-
import { Browser } from '../browser';
27+
import { Browser, collectPages } from '../browser';
2828
import * as network from '../network';
2929
import * as types from '../types';
3030
import * as platform from '../platform';
@@ -34,7 +34,7 @@ import { ConnectionTransport, SlowMoTransport } from '../transport';
3434
export class CRBrowser extends platform.EventEmitter implements Browser {
3535
_connection: CRConnection;
3636
_client: CRSession;
37-
private _defaultContext: BrowserContext;
37+
readonly _defaultContext: BrowserContext;
3838
private _contexts = new Map<string, BrowserContext>();
3939
_targets = new Map<string, CRTarget>();
4040

@@ -165,11 +165,16 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
165165
}
166166

167167
browserContexts(): BrowserContext[] {
168-
return [this._defaultContext, ...Array.from(this._contexts.values())];
168+
return Array.from(this._contexts.values());
169169
}
170170

171-
defaultContext(): BrowserContext {
172-
return this._defaultContext;
171+
async pages(): Promise<Page[]> {
172+
return collectPages(this);
173+
}
174+
175+
async newPage(url?: string, options?: BrowserContextOptions): Promise<Page> {
176+
const browserContext = await this.newContext(options);
177+
return browserContext.newPage(url);
173178
}
174179

175180
async _targetCreated(event: Protocol.Target.targetCreatedPayload) {

src/events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const Events = {
2020
Disconnected: 'disconnected'
2121
},
2222

23-
BrowserApp: {
23+
BrowserServer: {
2424
Close: 'close',
2525
},
2626

src/firefox/ffBrowser.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { Browser } from '../browser';
18+
import { Browser, collectPages } from '../browser';
1919
import { BrowserContext, BrowserContextOptions } from '../browserContext';
2020
import { Events } from '../events';
2121
import { assert, helper, RegisteredListener } from '../helper';
@@ -31,7 +31,7 @@ import { ConnectionTransport, SlowMoTransport } from '../transport';
3131
export class FFBrowser extends platform.EventEmitter implements Browser {
3232
_connection: FFConnection;
3333
_targets: Map<string, Target>;
34-
private _defaultContext: BrowserContext;
34+
readonly _defaultContext: BrowserContext;
3535
private _contexts: Map<string, BrowserContext>;
3636
private _eventListeners: RegisteredListener[];
3737

@@ -84,12 +84,17 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
8484
return context;
8585
}
8686

87-
browserContexts(): Array<BrowserContext> {
88-
return [this._defaultContext, ...Array.from(this._contexts.values())];
87+
browserContexts(): BrowserContext[] {
88+
return Array.from(this._contexts.values());
8989
}
9090

91-
defaultContext() {
92-
return this._defaultContext;
91+
async pages(): Promise<Page[]> {
92+
return collectPages(this);
93+
}
94+
95+
async newPage(url?: string, options?: BrowserContextOptions): Promise<Page> {
96+
const browserContext = await this.newContext(options);
97+
return browserContext.newPage(url);
9398
}
9499

95100
async _waitForTarget(predicate: (target: Target) => boolean, options: { timeout?: number; } = {}): Promise<Target> {

src/server/browserApp.ts renamed to src/server/browserServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { ChildProcess, execSync } from 'child_process';
1818
import * as platform from '../platform';
1919

20-
export class BrowserApp extends platform.EventEmitter {
20+
export class BrowserServer extends platform.EventEmitter {
2121
private _process: ChildProcess;
2222
private _gracefullyClose: () => Promise<void>;
2323
private _browserWSEndpoint: string | null = null;

src/server/browserType.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
import * as types from '../types';
1818
import { TimeoutError } from '../errors';
1919
import { Browser, ConnectOptions } from '../browser';
20-
import { BrowserApp } from './browserApp';
20+
import { BrowserContext } from '../browserContext';
21+
import { BrowserServer } from './browserServer';
2122

2223
export type BrowserArgOptions = {
2324
headless?: boolean,
2425
args?: string[],
25-
userDataDir?: string,
2626
devtools?: boolean,
2727
};
2828

@@ -40,8 +40,9 @@ export type LaunchOptions = BrowserArgOptions & {
4040
export interface BrowserType {
4141
executablePath(): string;
4242
name(): string;
43-
launchBrowserApp(options?: LaunchOptions): Promise<BrowserApp>;
4443
launch(options?: LaunchOptions & { slowMo?: number }): Promise<Browser>;
44+
launchServer(options?: LaunchOptions & { port?: number }): Promise<BrowserServer>;
45+
launchPersistent(options?: LaunchOptions & { userDataDir: string }): Promise<BrowserContext>;
4546
defaultArgs(options?: BrowserArgOptions): string[];
4647
connect(options: ConnectOptions): Promise<Browser>;
4748
devices: types.Devices;

src/server/chromium.ts

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ import { launchProcess, waitForLine } from '../server/processLauncher';
3030
import { kBrowserCloseMessageId } from '../chromium/crConnection';
3131
import { PipeTransport } from './pipeTransport';
3232
import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
33-
import { ConnectOptions } from '../browser';
34-
import { BrowserApp } from './browserApp';
33+
import { ConnectOptions, LaunchType } from '../browser';
34+
import { BrowserServer } from './browserServer';
3535
import { Events } from '../events';
3636
import { ConnectionTransport } from '../transport';
37+
import { BrowserContext } from '../browserContext';
3738

3839
export class Chromium implements BrowserType {
3940
private _projectRoot: string;
@@ -49,19 +50,28 @@ export class Chromium implements BrowserType {
4950
}
5051

5152
async launch(options?: LaunchOptions & { slowMo?: number }): Promise<CRBrowser> {
52-
const { browserApp, transport } = await this._launchBrowserApp(options, false);
53+
const { browserServer, transport } = await this._launchServer(options, 'local');
5354
const browser = await CRBrowser.connect(transport!, options && options.slowMo);
5455
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
55-
browser.close = () => browserApp.close();
56-
(browser as any)['__app__'] = browserApp;
56+
browser.close = () => browserServer.close();
57+
(browser as any)['__server__'] = browserServer;
5758
return browser;
5859
}
5960

60-
async launchBrowserApp(options?: LaunchOptions): Promise<BrowserApp> {
61-
return (await this._launchBrowserApp(options, true)).browserApp;
61+
async launchServer(options?: LaunchOptions & { port?: number }): Promise<BrowserServer> {
62+
return (await this._launchServer(options, 'server', undefined, options && options.port)).browserServer;
6263
}
6364

64-
async _launchBrowserApp(options: LaunchOptions = {}, isServer: boolean): Promise<{ browserApp: BrowserApp, transport?: ConnectionTransport }> {
65+
async launchPersistent(options?: LaunchOptions & { userDataDir?: string }): Promise<BrowserContext> {
66+
const { browserServer, transport } = await this._launchServer(options, 'persistent', options && options.userDataDir);
67+
const browser = await CRBrowser.connect(transport!);
68+
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
69+
const browserContext = browser._defaultContext;
70+
browserContext.close = () => browserServer.close();
71+
return browserContext;
72+
}
73+
74+
private async _launchServer(options: LaunchOptions = {}, launchType: LaunchType, userDataDir?: string, port?: number): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport }> {
6575
const {
6676
ignoreDefaultArgs = false,
6777
args = [],
@@ -82,14 +92,19 @@ export class Chromium implements BrowserType {
8292
else
8393
chromeArguments.push(...args);
8494

85-
let temporaryUserDataDir: string | null = null;
95+
const userDataDirArg = chromeArguments.find(arg => arg.startsWith('--user-data-dir='));
96+
if (userDataDirArg)
97+
throw new Error('Pass userDataDir parameter instead of specifying --user-data-dir argument');
98+
if (chromeArguments.find(arg => arg.startsWith('--remote-debugging-')))
99+
throw new Error('Can\' use --remote-debugging-* args. Playwright manages remote debugging connection itself');
86100

87-
if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
88-
chromeArguments.push(isServer ? '--remote-debugging-port=0' : '--remote-debugging-pipe');
89-
if (!chromeArguments.some(arg => arg.startsWith('--user-data-dir'))) {
101+
let temporaryUserDataDir: string | null = null;
102+
if (!userDataDir) {
103+
userDataDir = await mkdtempAsync(CHROMIUM_PROFILE_PATH);
90104
temporaryUserDataDir = await mkdtempAsync(CHROMIUM_PROFILE_PATH);
91-
chromeArguments.push(`--user-data-dir=${temporaryUserDataDir}`);
92105
}
106+
chromeArguments.push(`--user-data-dir=${userDataDir}`);
107+
chromeArguments.push(launchType === 'server' ? `--remote-debugging-port=${port || 0}` : '--remote-debugging-pipe');
93108

94109
let chromeExecutable = executablePath;
95110
if (!executablePath) {
@@ -99,11 +114,7 @@ export class Chromium implements BrowserType {
99114
chromeExecutable = executablePath;
100115
}
101116

102-
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
103-
if (usePipe && isServer)
104-
throw new Error(`Argument "--remote-debugging-pipe" is not compatible with the launchBrowserApp.`);
105-
106-
let browserApp: BrowserApp | undefined = undefined;
117+
let browserServer: BrowserServer | undefined = undefined;
107118
const { launchedProcess, gracefullyClose } = await launchProcess({
108119
executablePath: chromeExecutable!,
109120
args: chromeArguments,
@@ -112,10 +123,10 @@ export class Chromium implements BrowserType {
112123
handleSIGTERM,
113124
handleSIGHUP,
114125
dumpio,
115-
pipe: usePipe,
126+
pipe: launchType !== 'server',
116127
tempDir: temporaryUserDataDir || undefined,
117128
attemptToGracefullyClose: async () => {
118-
if (!browserApp)
129+
if (!browserServer)
119130
return Promise.reject();
120131
// We try to gracefully close to prevent crash reporting and core dumps.
121132
// Note that it's fine to reuse the pipe transport, since
@@ -125,23 +136,23 @@ export class Chromium implements BrowserType {
125136
t.send(JSON.stringify(message));
126137
},
127138
onkill: (exitCode, signal) => {
128-
if (browserApp)
129-
browserApp.emit(Events.BrowserApp.Close, exitCode, signal);
139+
if (browserServer)
140+
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
130141
},
131142
});
132143

133144
let transport: ConnectionTransport | undefined;
134145
let browserWSEndpoint: string | null;
135-
if (isServer) {
146+
if (launchType === 'server') {
136147
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chromium! The only Chromium revision guaranteed to work is r${this._revision}`);
137148
const match = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError);
138149
browserWSEndpoint = match[1];
139150
} else {
140151
transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
141152
browserWSEndpoint = null;
142153
}
143-
browserApp = new BrowserApp(launchedProcess, gracefullyClose, browserWSEndpoint);
144-
return { browserApp, transport };
154+
browserServer = new BrowserServer(launchedProcess, gracefullyClose, browserWSEndpoint);
155+
return { browserServer, transport };
145156
}
146157

147158
async connect(options: ConnectOptions): Promise<CRBrowser> {
@@ -166,11 +177,8 @@ export class Chromium implements BrowserType {
166177
devtools = false,
167178
headless = !devtools,
168179
args = [],
169-
userDataDir = null
170180
} = options;
171181
const chromeArguments = [...DEFAULT_ARGS];
172-
if (userDataDir)
173-
chromeArguments.push(`--user-data-dir=${userDataDir}`);
174182
if (devtools)
175183
chromeArguments.push('--auto-open-devtools-for-tabs');
176184
if (headless) {

0 commit comments

Comments
 (0)