feat(default context): support selected options for default context (#2177)

This commit is contained in:
Dmitry Gozman 2020-05-21 15:13:16 -07:00 committed by GitHub
parent 2f993018ea
commit aae3f1e75d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 416 additions and 196 deletions

View file

@ -6,7 +6,7 @@
},
{
"name": "firefox",
"revision": "1094"
"revision": "1097"
},
{
"name": "webkit",

View file

@ -4031,9 +4031,32 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
- `env` <[Object]<[string], [string]|[number]|[boolean]>> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `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`.
- `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.
- returns: <[Promise]<[BrowserContext]>> Promise which resolves to the browser app instance.
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `bypassCSP` <[boolean]> Toggles bypassing page's Content-Security-Policy.
- `viewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport.
- `width` <[number]> page width in pixels.
- `height` <[number]> page height in pixels.
- `userAgent` <[string]> Specific user agent to use in this context.
- `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`.
- `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account and touch events are enabled. Defaults to `false`. Not supported in Firefox.
- `hasTouch` <[boolean]> Specifies if viewport supports touch events. Defaults to false.
- `javaScriptEnabled` <[boolean]> Whether or not to enable JavaScript in the context. Defaults to true.
- `timezoneId` <[string]> Changes the timezone of the context. See [ICUs `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.
- `geolocation` <[Object]>
- `latitude` <[number]> Latitude between -90 and 90.
- `longitude` <[number]> Longitude between -180 and 180.
- `accuracy` <[number]> Non-negative accuracy value. Defaults to `0`.
- `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.
- `permissions` <[Array]<[string]>> A list of permissions to grant to all pages in this context. See [browserContext.grantPermissions](#browsercontextgrantpermissionspermissions-options) for more details.
- `extraHTTPHeaders` <[Object]<[string], [string]>> An object containing additional HTTP headers to be sent with every request. All header values must be strings.
- `offline` <[boolean]> Whether to emulate network being offline. Defaults to `false`.
- `httpCredentials` <[Object]> Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
- `username` <[string]>
- `password` <[string]>
- `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`'.
- returns: <[Promise]<[BrowserContext]>> Promise that resolves to the persistent browser context instance.
Launches browser instance that uses persistent storage located at `userDataDir`.
Launches browser that uses persistent storage located at `userDataDir` and returns the only context. Closing this context will automatically close the browser.
#### browserType.launchServer([options])
- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields:

View file

@ -14,22 +14,20 @@
* limitations under the License.
*/
import { BrowserContext, BrowserContextOptions, BrowserContextBase } from './browserContext';
import { BrowserContext, BrowserContextOptions, BrowserContextBase, PersistentContextOptions } from './browserContext';
import { Page } from './page';
import { EventEmitter } from 'events';
import { Download } from './download';
import type { BrowserServer } from './server/browserServer';
import { Events } from './events';
import { InnerLogger, Log } from './logger';
import * as types from './types';
export type BrowserOptions = {
logger: InnerLogger,
downloadsPath: string,
downloadsPath?: string,
headful?: boolean,
persistent?: boolean,
persistent?: PersistentContextOptions, // Undefined means no persistent context.
slowMo?: number,
viewport?: types.Size | null,
ownedServer?: BrowserServer,
};
@ -64,7 +62,7 @@ export abstract class BrowserBase extends EventEmitter implements Browser, Inner
}
_downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) {
const download = new Download(page, this._options.downloadsPath, uuid, url, suggestedFilename);
const download = new Download(page, this._options.downloadsPath || '', uuid, url, suggestedFilename);
this._downloads.set(uuid, download);
}

View file

@ -27,7 +27,7 @@ import { BrowserBase } from './browser';
import { Log, InnerLogger, Logger, RootLogger } from './logger';
import { FunctionWithSource } from './frames';
export type BrowserContextOptions = {
export type PersistentContextOptions = {
viewport?: types.Size | null,
ignoreHTTPSErrors?: boolean,
javaScriptEnabled?: boolean,
@ -44,6 +44,9 @@ export type BrowserContextOptions = {
isMobile?: boolean,
hasTouch?: boolean,
colorScheme?: types.ColorScheme,
};
export type BrowserContextOptions = PersistentContextOptions & {
acceptDownloads?: boolean,
logger?: Logger,
};
@ -188,9 +191,15 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
await this.waitForEvent('page');
const pages = this.pages();
await pages[0].waitForLoadState();
if (pages.length !== 1 || pages[0].url() !== 'about:blank') {
await this.close().catch(e => null);
if (pages.length !== 1 || pages[0].url() !== 'about:blank')
throw new Error(`Arguments can not specify page to be opened (first url is ${pages[0].url()})`);
if (this._options.isMobile || this._options.locale) {
// Workaround for:
// - chromium fails to change isMobile for existing page;
// - webkit fails to change locale for existing page.
const oldPage = pages[0];
await this.newPage();
await oldPage.close();
}
}
}
@ -203,7 +212,28 @@ export function assertBrowserContextIsNotOwned(context: BrowserContextBase) {
}
export function validateBrowserContextOptions(options: BrowserContextOptions): BrowserContextOptions {
const result = { ...options };
// Copy all fields manually to strip any extra junk.
// Especially useful when we share context and launch options for launchPersistent.
const result: BrowserContextOptions = {
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
bypassCSP: options.bypassCSP,
locale: options.locale,
timezoneId: options.timezoneId,
offline: options.offline,
colorScheme: options.colorScheme,
acceptDownloads: options.acceptDownloads,
viewport: options.viewport,
javaScriptEnabled: options.javaScriptEnabled,
userAgent: options.userAgent,
geolocation: options.geolocation,
permissions: options.permissions,
extraHTTPHeaders: options.extraHTTPHeaders,
httpCredentials: options.httpCredentials,
deviceScaleFactor: options.deviceScaleFactor,
isMobile: options.isMobile,
hasTouch: options.hasTouch,
logger: options.logger,
};
if (result.viewport === null && result.deviceScaleFactor !== undefined)
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
if (result.viewport === null && result.isMobile !== undefined)
@ -219,6 +249,12 @@ export function validateBrowserContextOptions(options: BrowserContextOptions): B
return result;
}
export function validatePersistentContextOptions(options: PersistentContextOptions): PersistentContextOptions {
if ((options as any).acceptDownloads !== undefined)
throw new Error(`Option "acceptDownloads" is not supported for persistent context`);
return validateBrowserContextOptions(options);
}
export function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation {
const result = { ...geolocation };
result.accuracy = result.accuracy || 0;

View file

@ -56,6 +56,8 @@ export class CRBrowser extends BrowserBase {
return browser;
}
browser._defaultContext = new CRBrowserContext(browser, null, options.persistent);
const existingTargetAttachPromises: Promise<any>[] = [];
// First page, background pages and their service workers in the persistent context
// are created automatically and may be initialized before we enable auto-attach.
@ -77,6 +79,7 @@ export class CRBrowser extends BrowserBase {
await Promise.all([
startDiscover,
autoAttachAndStopDiscover,
(browser._defaultContext as CRBrowserContext)._initialize(),
]);
// Wait for initial targets to arrive.
@ -88,9 +91,6 @@ export class CRBrowser extends BrowserBase {
super(options);
this._connection = connection;
this._session = this._connection.rootSession;
if (options.persistent)
this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({ viewport: options.viewport }));
this._connection.on(ConnectionEvents.Disconnected, () => {
for (const context of this._contexts.values())
context._browserClosed();
@ -290,19 +290,17 @@ export class CRBrowserContext extends BrowserContextBase {
}
async _initialize() {
const promises: Promise<any>[] = [
this._browser._session.send('Browser.setDownloadBehavior', {
assert(!Array.from(this._browser._crPages.values()).some(page => page._browserContext === this));
const promises: Promise<any>[] = [];
if (this._browser._options.downloadsPath) {
promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny',
browserContextId: this._browserContextId || undefined,
downloadPath: this._browser._options.downloadsPath
})
];
}));
}
if (this._options.permissions)
promises.push(this.grantPermissions(this._options.permissions));
if (this._options.offline)
promises.push(this.setOffline(this._options.offline));
if (this._options.httpCredentials)
promises.push(this.setHTTPCredentials(this._options.httpCredentials));
await Promise.all(promises);
}

View file

@ -37,7 +37,14 @@ export class FFBrowser extends BrowserBase {
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
const connection = new FFConnection(SlowMoTransport.wrap(transport, options.slowMo), options.logger);
const browser = new FFBrowser(connection, options);
await connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent });
const promises: Promise<any>[] = [
connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent }),
];
if (options.persistent) {
browser._defaultContext = new FFBrowserContext(browser, null, options.persistent);
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
}
await Promise.all(promises);
return browser;
}
@ -45,9 +52,6 @@ export class FFBrowser extends BrowserBase {
super(options);
this._connection = connection;
this._ffPages = new Map();
if (options.persistent)
this._defaultContext = new FFBrowserContext(this, null, validateBrowserContextOptions({}));
this._contexts = new Map();
this._connection.on(ConnectionEvents.Disconnected, () => {
for (const context of this._contexts.values())
@ -151,16 +155,18 @@ export class FFBrowserContext extends BrowserContextBase {
}
async _initialize() {
assert(!this._ffPages().length);
const browserContextId = this._browserContextId || undefined;
const promises: Promise<any>[] = [
this._browser._connection.send('Browser.setDownloadOptions', {
const promises: Promise<any>[] = [];
if (this._browser._options.downloadsPath) {
promises.push(this._browser._connection.send('Browser.setDownloadOptions', {
browserContextId,
downloadOptions: {
behavior: this._options.acceptDownloads ? 'saveToDisk' : 'cancel',
downloadsDir: this._browser._options.downloadsPath,
},
}),
];
}));
}
if (this._options.viewport) {
const viewport = {
viewportSize: { width: this._options.viewport.width, height: this._options.viewport.height },

View file

@ -56,11 +56,11 @@ export class BrowserServer extends EventEmitter {
private _webSocketWrapper: WebSocketWrapper | null = null;
readonly _launchOptions: LaunchOptions;
readonly _logger: RootLogger;
readonly _downloadsPath: string;
readonly _downloadsPath: string | undefined;
readonly _transport: ConnectionTransport;
readonly _headful: boolean;
constructor(options: LaunchOptions, process: ChildProcess, gracefullyClose: () => Promise<void>, transport: ConnectionTransport, downloadsPath: string, webSocketWrapper: WebSocketWrapper | null) {
constructor(options: LaunchOptions, process: ChildProcess, gracefullyClose: () => Promise<void>, transport: ConnectionTransport, downloadsPath: string | undefined, webSocketWrapper: WebSocketWrapper | null) {
super();
this._launchOptions = options;
this._headful = !processBrowserArgOptions(options).headless;

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { BrowserContext } from '../browserContext';
import { BrowserContext, PersistentContextOptions, validatePersistentContextOptions } from '../browserContext';
import { BrowserServer } from './browserServer';
import * as browserPaths from '../install/browserPaths';
import { Logger, RootLogger } from '../logger';
@ -60,7 +60,7 @@ export interface BrowserType {
name(): string;
launch(options?: LaunchOptions): Promise<Browser>;
launchServer(options?: LaunchServerOptions): Promise<BrowserServer>;
launchPersistentContext(userDataDir: string, options?: LaunchOptions): Promise<BrowserContext>;
launchPersistentContext(userDataDir: string, options?: LaunchOptions & PersistentContextOptions): Promise<BrowserContext>;
connect(options: ConnectOptions): Promise<Browser>;
}
@ -88,15 +88,16 @@ export abstract class BrowserTypeBase implements BrowserType {
async launch(options: LaunchOptions = {}): Promise<Browser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
return this._innerLaunch('local', options);
return this._innerLaunch('local', options, undefined);
}
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const browser = await this._innerLaunch('persistent', options, userDataDir);
async launchPersistentContext(userDataDir: string, options: LaunchOptions & PersistentContextOptions = {}): Promise<BrowserContext> {
const persistent = validatePersistentContextOptions(options);
const browser = await this._innerLaunch('persistent', options, persistent, userDataDir);
return browser._defaultContext!;
}
async _innerLaunch(launchType: LaunchType, options: LaunchOptions, userDataDir?: string): Promise<BrowserBase> {
async _innerLaunch(launchType: LaunchType, options: LaunchOptions, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
const deadline = TimeoutSettings.computeDeadline(options.timeout, 30000);
const logger = new RootLogger(options.logger);
logger.startLaunchRecording();
@ -104,7 +105,7 @@ export abstract class BrowserTypeBase implements BrowserType {
let browserServer: BrowserServer | undefined;
try {
browserServer = await this._launchServer(options, launchType, logger, deadline, userDataDir);
const promise = this._innerLaunchPromise(browserServer, launchType, options);
const promise = this._innerLaunchPromise(browserServer, options, persistent);
const browser = await helper.waitWithDeadline(promise, 'the browser to launch', deadline, 'pw:browser*');
return browser;
} catch (e) {
@ -119,12 +120,12 @@ export abstract class BrowserTypeBase implements BrowserType {
}
}
async _innerLaunchPromise(browserServer: BrowserServer, launchType: LaunchType, options: LaunchOptions): Promise<BrowserBase> {
async _innerLaunchPromise(browserServer: BrowserServer, options: LaunchOptions, persistent: PersistentContextOptions | undefined): Promise<BrowserBase> {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await this._connectToServer(browserServer, launchType === 'persistent');
if (launchType === 'persistent' && (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))) {
const browser = await this._connectToServer(browserServer, persistent);
if (persistent && (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))) {
const context = browser._defaultContext!;
await context._loadDefaultContext();
}
@ -166,10 +167,10 @@ export abstract class BrowserTypeBase implements BrowserType {
async _innerConnectPromise(transport: ConnectionTransport, options: ConnectOptions, logger: RootLogger): Promise<Browser> {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
return this._connectToTransport(transport, { slowMo: options.slowMo, logger, downloadsPath: '' });
return this._connectToTransport(transport, { slowMo: options.slowMo, logger });
}
abstract _launchServer(options: LaunchServerOptions, launchType: LaunchType, logger: RootLogger, deadline: number, userDataDir?: string): Promise<BrowserServer>;
abstract _connectToServer(browserServer: BrowserServer, persistent: boolean): Promise<BrowserBase>;
abstract _connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise<BrowserBase>;
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
}

View file

@ -33,6 +33,7 @@ import { InnerLogger, logError, RootLogger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths';
import { CRDevTools } from '../chromium/crDevTools';
import { BrowserBase, BrowserOptions } from '../browser';
import { PersistentContextOptions } from '../browserContext';
export class Chromium extends BrowserTypeBase {
private _devtools: CRDevTools | undefined;
@ -47,7 +48,7 @@ export class Chromium extends BrowserTypeBase {
return new CRDevTools(path.join(this._browserPath, 'devtools-preferences.json'));
}
async _connectToServer(browserServer: BrowserServer, persistent: boolean): Promise<BrowserBase> {
async _connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise<BrowserBase> {
const options = browserServer._launchOptions;
let devtools = this._devtools;
if ((options as any).__testHookForDevTools) {

View file

@ -198,8 +198,8 @@ export class Electron {
const chromeMatch = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, helper.timeUntilDeadline(deadline), timeoutError);
const chromeTransport = await WebSocketTransport.connect(chromeMatch[1], logger, deadline);
const browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, chromeTransport, '', null);
const browser = await CRBrowser.connect(chromeTransport, { headful: true, logger, persistent: true, viewport: null, ownedServer: browserServer, downloadsPath: '' });
const browserServer = new BrowserServer(options, launchedProcess, gracefullyClose, chromeTransport, undefined, null);
const browser = await CRBrowser.connect(chromeTransport, { headful: true, logger, persistent: { viewport: null }, ownedServer: browserServer });
app = new ElectronApplication(logger, browser, nodeConnection);
await app._init();
return app;

View file

@ -32,6 +32,7 @@ import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '..
import { InnerLogger, logError, RootLogger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths';
import { BrowserBase, BrowserOptions } from '../browser';
import { PersistentContextOptions } from '../browserContext';
const mkdtempAsync = util.promisify(fs.mkdtemp);
@ -40,7 +41,7 @@ export class Firefox extends BrowserTypeBase {
super(packagePath, browser);
}
_connectToServer(browserServer: BrowserServer, persistent: boolean): Promise<BrowserBase> {
_connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise<BrowserBase> {
return FFBrowser.connect(browserServer._transport, {
slowMo: browserServer._launchOptions.slowMo,
logger: browserServer._logger,

View file

@ -32,13 +32,14 @@ import { Events } from '../events';
import { InnerLogger, logError, RootLogger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths';
import { BrowserBase, BrowserOptions } from '../browser';
import { PersistentContextOptions } from '../browserContext';
export class WebKit extends BrowserTypeBase {
constructor(packagePath: string, browser: BrowserDescriptor) {
super(packagePath, browser);
}
_connectToServer(browserServer: BrowserServer, persistent: boolean): Promise<BrowserBase> {
_connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise<BrowserBase> {
return WKBrowser.connect(browserServer._transport, {
slowMo: browserServer._launchOptions.slowMo,
headful: browserServer._headful,

View file

@ -18,7 +18,7 @@
import { BrowserBase, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, BrowserContextBase, BrowserContextOptions, validateBrowserContextOptions, verifyGeolocation } from '../browserContext';
import { Events } from '../events';
import { helper, RegisteredListener } from '../helper';
import { helper, RegisteredListener, assert } from '../helper';
import * as network from '../network';
import { Page, PageBinding } from '../page';
import { ConnectionTransport, SlowMoTransport } from '../transport';
@ -38,7 +38,14 @@ export class WKBrowser extends BrowserBase {
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> {
const browser = new WKBrowser(SlowMoTransport.wrap(transport, options.slowMo), options);
await browser._browserSession.send('Playwright.enable');
const promises: Promise<any>[] = [
browser._browserSession.send('Playwright.enable'),
];
if (options.persistent) {
browser._defaultContext = new WKBrowserContext(browser, undefined, options.persistent);
promises.push((browser._defaultContext as WKBrowserContext)._initialize());
}
await Promise.all(promises);
return browser;
}
@ -46,10 +53,6 @@ export class WKBrowser extends BrowserBase {
super(options);
this._connection = new WKConnection(transport, options.logger, this._onDisconnect.bind(this));
this._browserSession = this._connection.browserSession;
if (options.persistent)
this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({}));
this._eventListeners = [
helper.addEventListener(this._browserSession, 'Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)),
helper.addEventListener(this._browserSession, 'Playwright.pageProxyDestroyed', this._onPageProxyDestroyed.bind(this)),
@ -200,14 +203,16 @@ export class WKBrowserContext extends BrowserContextBase {
}
async _initialize() {
assert(!this._wkPages().length);
const browserContextId = this._browserContextId;
const promises: Promise<any>[] = [
this._browser._browserSession.send('Playwright.setDownloadBehavior', {
const promises: Promise<any>[] = [];
if (this._browser._options.downloadsPath) {
promises.push(this._browser._browserSession.send('Playwright.setDownloadBehavior', {
behavior: this._options.acceptDownloads ? 'allow' : 'deny',
downloadPath: this._browser._options.downloadsPath,
browserContextId
})
];
}));
}
if (this._options.ignoreHTTPSErrors)
promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true }));
if (this._options.locale)

View file

@ -102,6 +102,12 @@ describe('BrowserContext', function() {
expect(await page.evaluate('window.innerHeight')).toBe(789);
await context.close();
});
it('should respect deviceScaleFactor', async({ browser }) => {
const context = await browser.newContext({ deviceScaleFactor: 3 });
const page = await context.newPage();
expect(await page.evaluate('window.devicePixelRatio')).toBe(3);
await context.close();
});
it('should not allow deviceScaleFactor with null viewport', async({ browser }) => {
const error = await browser.newContext({ viewport: null, deviceScaleFactor: 1 }).catch(e => e);
expect(error.message).toBe('"deviceScaleFactor" option is not supported with null "viewport"');

View file

@ -15,23 +15,28 @@
* limitations under the License.
*/
const fs = require('fs');
const utils = require('./utils');
const {makeUserDataDir, removeUserDataDir} = utils;
const {FFOX, MAC, CHROMIUM, WEBKIT} = utils.testOptions(browserType);
const {FFOX, MAC, CHROMIUM, WEBKIT, WIN} = utils.testOptions(browserType);
describe('launchPersistentContext()', function() {
beforeEach(async state => {
async function launch(state, options = {}) {
state.userDataDir = await makeUserDataDir();
state.browserContext = await state.browserType.launchPersistentContext(state.userDataDir, state.defaultBrowserOptions);
state.page = await state.browserContext.newPage();
});
afterEach(async state => {
await state.browserContext.close();
delete state.browserContext;
delete state.page;
state.context = await state.browserType.launchPersistentContext(state.userDataDir, {...state.defaultBrowserOptions, ...options});
state.page = state.context.pages()[0];
return state;
}
async function close(state) {
await state.context.close();
await removeUserDataDir(state.userDataDir);
});
it('context.cookies() should work', async({page, server}) => {
delete state.page;
delete state.context;
delete state.userDataDir;
}
it('context.cookies() should work', async state => {
const { page, server } = await launch(state);
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
document.cookie = 'username=John Doe';
@ -46,8 +51,10 @@ describe('launchPersistentContext()', function() {
secure: false,
sameSite: 'None',
}]);
await close(state);
});
it('context.addCookies() should work', async({page, server}) => {
it('context.addCookies() should work', async state => {
const { page, server } = await launch(state);
await page.goto(server.EMPTY_PAGE);
await page.context().addCookies([{
url: server.EMPTY_PAGE,
@ -65,8 +72,10 @@ describe('launchPersistentContext()', function() {
secure: false,
sameSite: 'None',
}]);
await close(state);
});
it('context.clearCookies() should work', async({page, server}) => {
it('context.clearCookies() should work', async state => {
const { page, server } = await launch(state);
await page.goto(server.EMPTY_PAGE);
await page.context().addCookies([{
url: server.EMPTY_PAGE,
@ -82,8 +91,10 @@ describe('launchPersistentContext()', function() {
await page.reload();
expect(await page.context().cookies([])).toEqual([]);
expect(await page.evaluate('document.cookie')).toBe('');
await close(state);
});
it('should(not) block third party cookies', async({browserContext, page, server}) => {
it('should(not) block third party cookies', async state => {
const { page, server, context } = await launch(state);
await page.goto(server.EMPTY_PAGE);
await page.evaluate(src => {
let fulfill;
@ -97,7 +108,7 @@ describe('launchPersistentContext()', function() {
await page.frames()[1].evaluate(`document.cookie = 'username=John Doe'`);
await page.waitForTimeout(2000);
const allowsThirdParty = CHROMIUM || FFOX;
const cookies = await browserContext.cookies(server.CROSS_PROCESS_PREFIX + '/grid.html');
const cookies = await context.cookies(server.CROSS_PROCESS_PREFIX + '/grid.html');
if (allowsThirdParty) {
expect(cookies).toEqual([
{
@ -114,5 +125,255 @@ describe('launchPersistentContext()', function() {
} else {
expect(cookies).toEqual([]);
}
await close(state);
});
it('should support viewport option', async state => {
let { page, context } = await launch(state, {viewport: { width: 456, height: 789 }});
expect(page.viewportSize().width).toBe(456);
expect(page.viewportSize().height).toBe(789);
expect(await page.evaluate('window.innerWidth')).toBe(456);
expect(await page.evaluate('window.innerHeight')).toBe(789);
page = await context.newPage();
expect(page.viewportSize().width).toBe(456);
expect(page.viewportSize().height).toBe(789);
expect(await page.evaluate('window.innerWidth')).toBe(456);
expect(await page.evaluate('window.innerHeight')).toBe(789);
await close(state);
});
it('should support deviceScaleFactor option', async state => {
const { page } = await launch(state, {deviceScaleFactor: 3});
expect(await page.evaluate('window.devicePixelRatio')).toBe(3);
await close(state);
});
it('should support userAgent option', async state => {
const { page, server } = await launch(state, {userAgent: 'foobar'});
expect(await page.evaluate(() => navigator.userAgent)).toBe('foobar');
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE),
]);
expect(request.headers['user-agent']).toBe('foobar');
await close(state);
});
it('should support bypassCSP option', async state => {
const { page, server } = await launch(state, {bypassCSP: true});
await page.goto(server.PREFIX + '/csp.html');
await page.addScriptTag({content: 'window.__injected = 42;'});
expect(await page.evaluate(() => window.__injected)).toBe(42);
await close(state);
});
it('should support javascriptEnabled option', async state => {
const { page } = await launch(state, {javaScriptEnabled: false});
await page.goto('data:text/html, <script>var something = "forbidden"</script>');
let error = null;
await page.evaluate('something').catch(e => error = e);
if (WEBKIT)
expect(error.message).toContain('Can\'t find variable: something');
else
expect(error.message).toContain('something is not defined');
await close(state);
});
it('should support httpCredentials option', async state => {
const { page, server } = await launch(state, {httpCredentials: { username: 'user', password: 'pass' }});
server.setAuth('/playground.html', 'user', 'pass');
const response = await page.goto(server.PREFIX + '/playground.html');
expect(response.status()).toBe(200);
await close(state);
});
it('should support offline option', async state => {
const { page, server } = await launch(state, {offline: true});
const error = await page.goto(server.EMPTY_PAGE).catch(e => e);
expect(error).toBeTruthy();
await close(state);
});
it.skip(true)('should support acceptDownloads option', async state => {
// TODO: unskip once we support downloads in persistent context.
const { page, server } = await launch(state, {acceptDownloads: true});
server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment');
res.end(`Hello world`);
});
await page.setContent(`<a href="${server.PREFIX}/download">download</a>`);
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.click('a')
]);
const path = await download.path();
expect(fs.existsSync(path)).toBeTruthy();
expect(fs.readFileSync(path).toString()).toBe('Hello world');
await close(state);
});
it('should support hasTouch option', async state => {
const { page, server } = await launch(state, {hasTouch: true});
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true);
await close(state);
});
it.skip(FFOX)('should work in persistent context', async state => {
// Firefox does not support mobile.
const { page, server } = await launch(state, {viewport: {width: 320, height: 480}, isMobile: true});
await page.goto(server.PREFIX + '/empty.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(980);
await close(state);
});
it('should support colorScheme option', async state => {
const { page } = await launch(state, {colorScheme: 'dark'});
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)).toBe(false);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)).toBe(true);
expect(await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)).toBe(false);
await close(state);
});
it('should support timezoneId option', async state => {
const { page } = await launch(state, {timezoneId: 'America/Jamaica'});
expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)');
await close(state);
});
it('should support locale option', async state => {
const { page } = await launch(state, {locale: 'fr-CH'});
expect(await page.evaluate(() => navigator.language)).toBe('fr-CH');
await close(state);
});
it('should support geolocation and permissions options', async state => {
const { page, server } = await launch(state, {geolocation: {longitude: 10, latitude: 10}, permissions: ['geolocation']});
await page.goto(server.EMPTY_PAGE);
const geolocation = await page.evaluate(() => new Promise(resolve => navigator.geolocation.getCurrentPosition(position => {
resolve({latitude: position.coords.latitude, longitude: position.coords.longitude});
})));
expect(geolocation).toEqual({latitude: 10, longitude: 10});
await close(state);
});
it('should support ignoreHTTPSErrors option', async state => {
const { page, httpsServer } = await launch(state, {ignoreHTTPSErrors: true});
let error = null;
const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e);
expect(error).toBe(null);
expect(response.ok()).toBe(true);
await close(state);
});
it('should support extraHTTPHeaders option', async state => {
const { page, server } = await launch(state, {extraHTTPHeaders: { foo: 'bar' }});
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE),
]);
expect(request.headers['foo']).toBe('bar');
await close(state);
});
it('should accept userDataDir', async state => {
const { userDataDir, context } = await launch(state);
// Note: we need an open page to make sure its functional.
expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
await context.close();
expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
// This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
await removeUserDataDir(userDataDir);
});
it.slow()('should restore state from userDataDir', async({browserType, defaultBrowserOptions, server}) => {
const userDataDir = await makeUserDataDir();
const browserContext = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const page = await browserContext.newPage();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => localStorage.hey = 'hello');
await browserContext.close();
const browserContext2 = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const page2 = await browserContext2.newPage();
await page2.goto(server.EMPTY_PAGE);
expect(await page2.evaluate(() => localStorage.hey)).toBe('hello');
await browserContext2.close();
const userDataDir2 = await makeUserDataDir();
const browserContext3 = await browserType.launchPersistentContext(userDataDir2, defaultBrowserOptions);
const page3 = await browserContext3.newPage();
await page3.goto(server.EMPTY_PAGE);
expect(await page3.evaluate(() => localStorage.hey)).not.toBe('hello');
await browserContext3.close();
// This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
await removeUserDataDir(userDataDir);
await removeUserDataDir(userDataDir2);
});
it.slow().fail(WIN && CHROMIUM)('should restore cookies from userDataDir', async({browserType, defaultBrowserOptions, server}) => {
// TODO: Flaky! See https://github.com/microsoft/playwright/pull/1795/checks?check_run_id=587685496
const userDataDir = await makeUserDataDir();
const browserContext = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const page = await browserContext.newPage();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
await browserContext.close();
const browserContext2 = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const page2 = await browserContext2.newPage();
await page2.goto(server.EMPTY_PAGE);
expect(await page2.evaluate(() => document.cookie)).toBe('doSomethingOnlyOnce=true');
await browserContext2.close();
const userDataDir2 = await makeUserDataDir();
const browserContext3 = await browserType.launchPersistentContext(userDataDir2, defaultBrowserOptions);
const page3 = await browserContext3.newPage();
await page3.goto(server.EMPTY_PAGE);
expect(await page3.evaluate(() => localStorage.hey)).not.toBe('doSomethingOnlyOnce=true');
await browserContext3.close();
// This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
await removeUserDataDir(userDataDir);
await removeUserDataDir(userDataDir2);
});
it('should have default URL when launching browser', async state => {
const { context } = await launch(state);
const urls = context.pages().map(page => page.url());
expect(urls).toEqual(['about:blank']);
await close(state);
});
it.skip(FFOX)('should throw if page argument is passed', async ({browserType, defaultBrowserOptions, server}) => {
const userDataDir = await makeUserDataDir();
const options = {...defaultBrowserOptions, args: [server.EMPTY_PAGE] };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error.message).toContain('can not specify page');
await removeUserDataDir(userDataDir);
});
it('should have passed URL when launching with ignoreDefaultArgs: true', async ({browserType, defaultBrowserOptions, server}) => {
const userDataDir = await makeUserDataDir();
const args = browserType._defaultArgs(defaultBrowserOptions, 'persistent', userDataDir, 0).filter(a => a !== 'about:blank');
const options = {
...defaultBrowserOptions,
args: [...args, server.EMPTY_PAGE],
ignoreDefaultArgs: true,
};
const browserContext = await browserType.launchPersistentContext(userDataDir, options);
if (!browserContext.pages().length)
await browserContext.waitForEvent('page');
await browserContext.pages()[0].waitForLoadState();
const gotUrls = browserContext.pages().map(page => page.url());
expect(gotUrls).toEqual([server.EMPTY_PAGE]);
await browserContext.close();
await removeUserDataDir(userDataDir);
});
it('should handle timeout', async({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error.message).toContain('Waiting for the browser to launch failed: timeout exceeded. Re-run with the DEBUG=pw:browser* env variable to see the debug log.');
await removeUserDataDir(userDataDir);
});
it('should handle exception', async({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const e = new Error('Dummy');
const options = { ...defaultBrowserOptions, __testHookBeforeCreateBrowser: () => { throw e; } };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error).toBe(e);
await removeUserDataDir(userDataDir);
});
it('should throw on unsupported options', async ({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const optionNames = [ 'acceptDownloads' ];
for (const option of optionNames) {
const options = { ...defaultBrowserOptions };
options[option] = 'hello';
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error.message).toBe(`Option "${option}" is not supported for persistent context`);
}
await removeUserDataDir(userDataDir);
});
});

View file

@ -18,8 +18,7 @@
const path = require('path');
const fs = require('fs');
const utils = require('./utils');
const { makeUserDataDir, removeUserDataDir } = utils;
const {FFOX, CHROMIUM, WEBKIT, WIN} = utils.testOptions(browserType);
const {FFOX, CHROMIUM, WEBKIT} = utils.testOptions(browserType);
describe('Playwright', function() {
describe('browserType.launch', function() {
@ -70,57 +69,6 @@ describe('Playwright', function() {
});
});
describe('browserType.launchPersistentContext', function() {
it('should have default URL when launching browser', async ({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const browserContext = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const urls = browserContext.pages().map(page => page.url());
expect(urls).toEqual(['about:blank']);
await browserContext.close();
await removeUserDataDir(userDataDir);
});
it.skip(FFOX)('should throw if page argument is passed', async ({browserType, defaultBrowserOptions, server}) => {
const userDataDir = await makeUserDataDir();
const options = Object.assign({}, defaultBrowserOptions);
options.args = [server.EMPTY_PAGE].concat(options.args || []);
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error.message).toContain('can not specify page');
await removeUserDataDir(userDataDir);
});
it('should have passed URL when launching with ignoreDefaultArgs: true', async ({browserType, defaultBrowserOptions, server}) => {
const userDataDir = await makeUserDataDir();
const args = browserType._defaultArgs(defaultBrowserOptions, 'persistent', userDataDir, 0).filter(a => a !== 'about:blank');
const options = {
...defaultBrowserOptions,
args: [...args, server.EMPTY_PAGE],
ignoreDefaultArgs: true,
};
const browserContext = await browserType.launchPersistentContext(userDataDir, options);
if (!browserContext.pages().length)
await browserContext.waitForEvent('page');
await browserContext.pages()[0].waitForLoadState();
const gotUrls = browserContext.pages().map(page => page.url());
expect(gotUrls).toEqual([server.EMPTY_PAGE]);
await browserContext.close();
await removeUserDataDir(userDataDir);
});
it('should handle timeout', async({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error.message).toContain('Waiting for the browser to launch failed: timeout exceeded. Re-run with the DEBUG=pw:browser* env variable to see the debug log.');
await removeUserDataDir(userDataDir);
});
it('should handle exception', async({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const e = new Error('Dummy');
const options = { ...defaultBrowserOptions, __testHookBeforeCreateBrowser: () => { throw e; } };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error).toBe(e);
await removeUserDataDir(userDataDir);
});
});
describe('browserType.launchServer', function() {
it('should return child_process instance', async ({browserType, defaultBrowserOptions}) => {
const browserServer = await browserType.launchServer(defaultBrowserOptions);
@ -332,68 +280,3 @@ describe('browserType.connect', function() {
expect(error).toBe(e);
});
});
describe('browserType.launchPersistentContext', function() {
it('userDataDir option', async({browserType, defaultBrowserOptions}) => {
const userDataDir = await makeUserDataDir();
const browserContext = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
// Open a page to make sure its functional.
await browserContext.newPage();
expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
await browserContext.close();
expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
// This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
await removeUserDataDir(userDataDir);
});
it.slow()('userDataDir option should restore state', async({browserType, defaultBrowserOptions, server}) => {
const userDataDir = await makeUserDataDir();
const browserContext = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const page = await browserContext.newPage();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => localStorage.hey = 'hello');
await browserContext.close();
const browserContext2 = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const page2 = await browserContext2.newPage();
await page2.goto(server.EMPTY_PAGE);
expect(await page2.evaluate(() => localStorage.hey)).toBe('hello');
await browserContext2.close();
const userDataDir2 = await makeUserDataDir();
const browserContext3 = await browserType.launchPersistentContext(userDataDir2, defaultBrowserOptions);
const page3 = await browserContext3.newPage();
await page3.goto(server.EMPTY_PAGE);
expect(await page3.evaluate(() => localStorage.hey)).not.toBe('hello');
await browserContext3.close();
// This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
await removeUserDataDir(userDataDir);
await removeUserDataDir(userDataDir2);
});
// TODO: Flaky! See https://github.com/microsoft/playwright/pull/1795/checks?check_run_id=587685496
it.slow().fail(WIN && CHROMIUM)('userDataDir option should restore cookies', async({browserType, defaultBrowserOptions, server}) => {
const userDataDir = await makeUserDataDir();
const browserContext = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const page = await browserContext.newPage();
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
await browserContext.close();
const browserContext2 = await browserType.launchPersistentContext(userDataDir, defaultBrowserOptions);
const page2 = await browserContext2.newPage();
await page2.goto(server.EMPTY_PAGE);
expect(await page2.evaluate(() => document.cookie)).toBe('doSomethingOnlyOnce=true');
await browserContext2.close();
const userDataDir2 = await makeUserDataDir();
const browserContext3 = await browserType.launchPersistentContext(userDataDir2, defaultBrowserOptions);
const page3 = await browserContext3.newPage();
await page3.goto(server.EMPTY_PAGE);
expect(await page3.evaluate(() => localStorage.hey)).not.toBe('doSomethingOnlyOnce=true');
await browserContext3.close();
// This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
await removeUserDataDir(userDataDir);
await removeUserDataDir(userDataDir2);
});
});

View file

@ -202,12 +202,12 @@ const utils = module.exports = {
const logger = {
isEnabled: (name, severity) => {
return name === 'browser' || (name === 'protocol' && dumpProtocolOnFailure);
return name.startsWith('browser') || (name === 'protocol' && dumpProtocolOnFailure);
},
log: (name, severity, message, args) => {
if (!testRun)
return;
if (name === 'browser') {
if (name.startsWith('browser')) {
if (severity === 'warning')
testRun.log(`${prefix}\x1b[31m[browser]\x1b[0m ${message}`)
else