Skip to content

Commit aae3f1e

Browse files
authored
feat(default context): support selected options for default context (#2177)
1 parent 2f99301 commit aae3f1e

17 files changed

+416
-196
lines changed

browsers.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
{
88
"name": "firefox",
9-
"revision": "1094"
9+
"revision": "1097"
1010
},
1111
{
1212
"name": "webkit",

docs/api.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4031,9 +4031,32 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
40314031
- `env` <[Object]<[string], [string]|[number]|[boolean]>> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
40324032
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
40334033
- `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. Defaults to 0.
4034-
- returns: <[Promise]<[BrowserContext]>> Promise which resolves to the browser app instance.
4034+
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
4035+
- `bypassCSP` <[boolean]> Toggles bypassing page's Content-Security-Policy.
4036+
- `viewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport.
4037+
- `width` <[number]> page width in pixels.
4038+
- `height` <[number]> page height in pixels.
4039+
- `userAgent` <[string]> Specific user agent to use in this context.
4040+
- `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`.
4041+
- `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account and touch events are enabled. Defaults to `false`. Not supported in Firefox.
4042+
- `hasTouch` <[boolean]> Specifies if viewport supports touch events. Defaults to false.
4043+
- `javaScriptEnabled` <[boolean]> Whether or not to enable JavaScript in the context. Defaults to true.
4044+
- `timezoneId` <[string]> Changes the timezone of the context. See [ICU’s `metaZones.txt`](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) for a list of supported timezone IDs.
4045+
- `geolocation` <[Object]>
4046+
- `latitude` <[number]> Latitude between -90 and 90.
4047+
- `longitude` <[number]> Longitude between -180 and 180.
4048+
- `accuracy` <[number]> Non-negative accuracy value. Defaults to `0`.
4049+
- `locale` <[string]> Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules.
4050+
- `permissions` <[Array]<[string]>> A list of permissions to grant to all pages in this context. See [browserContext.grantPermissions](#browsercontextgrantpermissionspermissions-options) for more details.
4051+
- `extraHTTPHeaders` <[Object]<[string], [string]>> An object containing additional HTTP headers to be sent with every request. All header values must be strings.
4052+
- `offline` <[boolean]> Whether to emulate network being offline. Defaults to `false`.
4053+
- `httpCredentials` <[Object]> Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
4054+
- `username` <[string]>
4055+
- `password` <[string]>
4056+
- `colorScheme` <"dark"|"light"|"no-preference"> Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See [page.emulateMedia(options)](#pageemulatemediaoptions) for more details. Defaults to '`light`'.
4057+
- returns: <[Promise]<[BrowserContext]>> Promise that resolves to the persistent browser context instance.
40354058

4036-
Launches browser instance that uses persistent storage located at `userDataDir`.
4059+
Launches browser that uses persistent storage located at `userDataDir` and returns the only context. Closing this context will automatically close the browser.
40374060

40384061
#### browserType.launchServer([options])
40394062
- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields:

src/browser.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,20 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { BrowserContext, BrowserContextOptions, BrowserContextBase } from './browserContext';
17+
import { BrowserContext, BrowserContextOptions, BrowserContextBase, PersistentContextOptions } from './browserContext';
1818
import { Page } from './page';
1919
import { EventEmitter } from 'events';
2020
import { Download } from './download';
2121
import type { BrowserServer } from './server/browserServer';
2222
import { Events } from './events';
2323
import { InnerLogger, Log } from './logger';
24-
import * as types from './types';
2524

2625
export type BrowserOptions = {
2726
logger: InnerLogger,
28-
downloadsPath: string,
27+
downloadsPath?: string,
2928
headful?: boolean,
30-
persistent?: boolean,
29+
persistent?: PersistentContextOptions, // Undefined means no persistent context.
3130
slowMo?: number,
32-
viewport?: types.Size | null,
3331
ownedServer?: BrowserServer,
3432
};
3533

@@ -64,7 +62,7 @@ export abstract class BrowserBase extends EventEmitter implements Browser, Inner
6462
}
6563

6664
_downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) {
67-
const download = new Download(page, this._options.downloadsPath, uuid, url, suggestedFilename);
65+
const download = new Download(page, this._options.downloadsPath || '', uuid, url, suggestedFilename);
6866
this._downloads.set(uuid, download);
6967
}
7068

src/browserContext.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { BrowserBase } from './browser';
2727
import { Log, InnerLogger, Logger, RootLogger } from './logger';
2828
import { FunctionWithSource } from './frames';
2929

30-
export type BrowserContextOptions = {
30+
export type PersistentContextOptions = {
3131
viewport?: types.Size | null,
3232
ignoreHTTPSErrors?: boolean,
3333
javaScriptEnabled?: boolean,
@@ -44,6 +44,9 @@ export type BrowserContextOptions = {
4444
isMobile?: boolean,
4545
hasTouch?: boolean,
4646
colorScheme?: types.ColorScheme,
47+
};
48+
49+
export type BrowserContextOptions = PersistentContextOptions & {
4750
acceptDownloads?: boolean,
4851
logger?: Logger,
4952
};
@@ -188,9 +191,15 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
188191
await this.waitForEvent('page');
189192
const pages = this.pages();
190193
await pages[0].waitForLoadState();
191-
if (pages.length !== 1 || pages[0].url() !== 'about:blank') {
192-
await this.close().catch(e => null);
194+
if (pages.length !== 1 || pages[0].url() !== 'about:blank')
193195
throw new Error(`Arguments can not specify page to be opened (first url is ${pages[0].url()})`);
196+
if (this._options.isMobile || this._options.locale) {
197+
// Workaround for:
198+
// - chromium fails to change isMobile for existing page;
199+
// - webkit fails to change locale for existing page.
200+
const oldPage = pages[0];
201+
await this.newPage();
202+
await oldPage.close();
194203
}
195204
}
196205
}
@@ -203,7 +212,28 @@ export function assertBrowserContextIsNotOwned(context: BrowserContextBase) {
203212
}
204213

205214
export function validateBrowserContextOptions(options: BrowserContextOptions): BrowserContextOptions {
206-
const result = { ...options };
215+
// Copy all fields manually to strip any extra junk.
216+
// Especially useful when we share context and launch options for launchPersistent.
217+
const result: BrowserContextOptions = {
218+
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
219+
bypassCSP: options.bypassCSP,
220+
locale: options.locale,
221+
timezoneId: options.timezoneId,
222+
offline: options.offline,
223+
colorScheme: options.colorScheme,
224+
acceptDownloads: options.acceptDownloads,
225+
viewport: options.viewport,
226+
javaScriptEnabled: options.javaScriptEnabled,
227+
userAgent: options.userAgent,
228+
geolocation: options.geolocation,
229+
permissions: options.permissions,
230+
extraHTTPHeaders: options.extraHTTPHeaders,
231+
httpCredentials: options.httpCredentials,
232+
deviceScaleFactor: options.deviceScaleFactor,
233+
isMobile: options.isMobile,
234+
hasTouch: options.hasTouch,
235+
logger: options.logger,
236+
};
207237
if (result.viewport === null && result.deviceScaleFactor !== undefined)
208238
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
209239
if (result.viewport === null && result.isMobile !== undefined)
@@ -219,6 +249,12 @@ export function validateBrowserContextOptions(options: BrowserContextOptions): B
219249
return result;
220250
}
221251

252+
export function validatePersistentContextOptions(options: PersistentContextOptions): PersistentContextOptions {
253+
if ((options as any).acceptDownloads !== undefined)
254+
throw new Error(`Option "acceptDownloads" is not supported for persistent context`);
255+
return validateBrowserContextOptions(options);
256+
}
257+
222258
export function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation {
223259
const result = { ...geolocation };
224260
result.accuracy = result.accuracy || 0;

src/chromium/crBrowser.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export class CRBrowser extends BrowserBase {
5656
return browser;
5757
}
5858

59+
browser._defaultContext = new CRBrowserContext(browser, null, options.persistent);
60+
5961
const existingTargetAttachPromises: Promise<any>[] = [];
6062
// First page, background pages and their service workers in the persistent context
6163
// are created automatically and may be initialized before we enable auto-attach.
@@ -77,6 +79,7 @@ export class CRBrowser extends BrowserBase {
7779
await Promise.all([
7880
startDiscover,
7981
autoAttachAndStopDiscover,
82+
(browser._defaultContext as CRBrowserContext)._initialize(),
8083
]);
8184

8285
// Wait for initial targets to arrive.
@@ -88,9 +91,6 @@ export class CRBrowser extends BrowserBase {
8891
super(options);
8992
this._connection = connection;
9093
this._session = this._connection.rootSession;
91-
92-
if (options.persistent)
93-
this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({ viewport: options.viewport }));
9494
this._connection.on(ConnectionEvents.Disconnected, () => {
9595
for (const context of this._contexts.values())
9696
context._browserClosed();
@@ -290,19 +290,17 @@ export class CRBrowserContext extends BrowserContextBase {
290290
}
291291

292292
async _initialize() {
293-
const promises: Promise<any>[] = [
294-
this._browser._session.send('Browser.setDownloadBehavior', {
293+
assert(!Array.from(this._browser._crPages.values()).some(page => page._browserContext === this));
294+
const promises: Promise<any>[] = [];
295+
if (this._browser._options.downloadsPath) {
296+
promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
295297
behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny',
296298
browserContextId: this._browserContextId || undefined,
297299
downloadPath: this._browser._options.downloadsPath
298-
})
299-
];
300+
}));
301+
}
300302
if (this._options.permissions)
301303
promises.push(this.grantPermissions(this._options.permissions));
302-
if (this._options.offline)
303-
promises.push(this.setOffline(this._options.offline));
304-
if (this._options.httpCredentials)
305-
promises.push(this.setHTTPCredentials(this._options.httpCredentials));
306304
await Promise.all(promises);
307305
}
308306

src/firefox/ffBrowser.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,21 @@ export class FFBrowser extends BrowserBase {
3737
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
3838
const connection = new FFConnection(SlowMoTransport.wrap(transport, options.slowMo), options.logger);
3939
const browser = new FFBrowser(connection, options);
40-
await connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent });
40+
const promises: Promise<any>[] = [
41+
connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent }),
42+
];
43+
if (options.persistent) {
44+
browser._defaultContext = new FFBrowserContext(browser, null, options.persistent);
45+
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
46+
}
47+
await Promise.all(promises);
4148
return browser;
4249
}
4350

4451
constructor(connection: FFConnection, options: BrowserOptions) {
4552
super(options);
4653
this._connection = connection;
4754
this._ffPages = new Map();
48-
49-
if (options.persistent)
50-
this._defaultContext = new FFBrowserContext(this, null, validateBrowserContextOptions({}));
5155
this._contexts = new Map();
5256
this._connection.on(ConnectionEvents.Disconnected, () => {
5357
for (const context of this._contexts.values())
@@ -151,16 +155,18 @@ export class FFBrowserContext extends BrowserContextBase {
151155
}
152156

153157
async _initialize() {
158+
assert(!this._ffPages().length);
154159
const browserContextId = this._browserContextId || undefined;
155-
const promises: Promise<any>[] = [
156-
this._browser._connection.send('Browser.setDownloadOptions', {
160+
const promises: Promise<any>[] = [];
161+
if (this._browser._options.downloadsPath) {
162+
promises.push(this._browser._connection.send('Browser.setDownloadOptions', {
157163
browserContextId,
158164
downloadOptions: {
159165
behavior: this._options.acceptDownloads ? 'saveToDisk' : 'cancel',
160166
downloadsDir: this._browser._options.downloadsPath,
161167
},
162-
}),
163-
];
168+
}));
169+
}
164170
if (this._options.viewport) {
165171
const viewport = {
166172
viewportSize: { width: this._options.viewport.width, height: this._options.viewport.height },

src/server/browserServer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ export class BrowserServer extends EventEmitter {
5656
private _webSocketWrapper: WebSocketWrapper | null = null;
5757
readonly _launchOptions: LaunchOptions;
5858
readonly _logger: RootLogger;
59-
readonly _downloadsPath: string;
59+
readonly _downloadsPath: string | undefined;
6060
readonly _transport: ConnectionTransport;
6161
readonly _headful: boolean;
6262

63-
constructor(options: LaunchOptions, process: ChildProcess, gracefullyClose: () => Promise<void>, transport: ConnectionTransport, downloadsPath: string, webSocketWrapper: WebSocketWrapper | null) {
63+
constructor(options: LaunchOptions, process: ChildProcess, gracefullyClose: () => Promise<void>, transport: ConnectionTransport, downloadsPath: string | undefined, webSocketWrapper: WebSocketWrapper | null) {
6464
super();
6565
this._launchOptions = options;
6666
this._headful = !processBrowserArgOptions(options).headless;

src/server/browserType.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { BrowserContext } from '../browserContext';
17+
import { BrowserContext, PersistentContextOptions, validatePersistentContextOptions } from '../browserContext';
1818
import { BrowserServer } from './browserServer';
1919
import * as browserPaths from '../install/browserPaths';
2020
import { Logger, RootLogger } from '../logger';
@@ -60,7 +60,7 @@ export interface BrowserType {
6060
name(): string;
6161
launch(options?: LaunchOptions): Promise<Browser>;
6262
launchServer(options?: LaunchServerOptions): Promise<BrowserServer>;
63-
launchPersistentContext(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext>;
63+
launchPersistentContext(userDataDir: string, options?: LaunchOptions & PersistentContextOptions): Promise<BrowserContext>;
6464
connect(options: ConnectOptions): Promise<Browser>;
6565
}
6666

@@ -88,23 +88,24 @@ export abstract class BrowserTypeBase implements BrowserType {
8888

8989
async launch(options: LaunchOptions = {}): Promise<Browser> {
9090
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
91-
return this._innerLaunch('local', options);
91+
return this._innerLaunch('local', options, undefined);
9292
}
9393

94-
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
95-
const browser = await this._innerLaunch('persistent', options, userDataDir);
94+
async launchPersistentContext(userDataDir: string, options: LaunchOptions & PersistentContextOptions = {}): Promise<BrowserContext> {
95+
const persistent = validatePersistentContextOptions(options);
96+
const browser = await this._innerLaunch('persistent', options, persistent, userDataDir);
9697
return browser._defaultContext!;
9798
}
9899

99-
async _innerLaunch(launchType: LaunchType, options: LaunchOptions, userDataDir?: string): Promise<BrowserBase> {
100+
async _innerLaunch(launchType: LaunchType, options: LaunchOptions, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
100101
const deadline = TimeoutSettings.computeDeadline(options.timeout, 30000);
101102
const logger = new RootLogger(options.logger);
102103
logger.startLaunchRecording();
103104

104105
let browserServer: BrowserServer | undefined;
105106
try {
106107
browserServer = await this._launchServer(options, launchType, logger, deadline, userDataDir);
107-
const promise = this._innerLaunchPromise(browserServer, launchType, options);
108+
const promise = this._innerLaunchPromise(browserServer, options, persistent);
108109
const browser = await helper.waitWithDeadline(promise, 'the browser to launch', deadline, 'pw:browser*');
109110
return browser;
110111
} catch (e) {
@@ -119,12 +120,12 @@ export abstract class BrowserTypeBase implements BrowserType {
119120
}
120121
}
121122

122-
async _innerLaunchPromise(browserServer: BrowserServer, launchType: LaunchType, options: LaunchOptions): Promise<BrowserBase> {
123+
async _innerLaunchPromise(browserServer: BrowserServer, options: LaunchOptions, persistent: PersistentContextOptions | undefined): Promise<BrowserBase> {
123124
if ((options as any).__testHookBeforeCreateBrowser)
124125
await (options as any).__testHookBeforeCreateBrowser();
125126

126-
const browser = await this._connectToServer(browserServer, launchType === 'persistent');
127-
if (launchType === 'persistent' && (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))) {
127+
const browser = await this._connectToServer(browserServer, persistent);
128+
if (persistent && (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))) {
128129
const context = browser._defaultContext!;
129130
await context._loadDefaultContext();
130131
}
@@ -166,10 +167,10 @@ export abstract class BrowserTypeBase implements BrowserType {
166167
async _innerConnectPromise(transport: ConnectionTransport, options: ConnectOptions, logger: RootLogger): Promise<Browser> {
167168
if ((options as any).__testHookBeforeCreateBrowser)
168169
await (options as any).__testHookBeforeCreateBrowser();
169-
return this._connectToTransport(transport, { slowMo: options.slowMo, logger, downloadsPath: '' });
170+
return this._connectToTransport(transport, { slowMo: options.slowMo, logger });
170171
}
171172

172173
abstract _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer>;
173-
abstract _connectToServer(browserServer: BrowserServer, persistent: boolean): Promise<BrowserBase>;
174+
abstract _connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise<BrowserBase>;
174175
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
175176
}

src/server/chromium.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { InnerLogger, logError, RootLogger } from '../logger';
3333
import { BrowserDescriptor } from '../install/browserPaths';
3434
import { CRDevTools } from '../chromium/crDevTools';
3535
import { BrowserBase, BrowserOptions } from '../browser';
36+
import { PersistentContextOptions } from '../browserContext';
3637

3738
export class Chromium extends BrowserTypeBase {
3839
private _devtools: CRDevTools | undefined;
@@ -47,7 +48,7 @@ export class Chromium extends BrowserTypeBase {
4748
return new CRDevTools(path.join(this._browserPath, 'devtools-preferences.json'));
4849
}
4950

50-
async _connectToServer(browserServer: BrowserServer, persistent: boolean): Promise<BrowserBase> {
51+
async _connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise<BrowserBase> {
5152
const options = browserServer._launchOptions;
5253
let devtools = this._devtools;
5354
if ((options as any).__testHookForDevTools) {

0 commit comments

Comments
 (0)