From aae3f1e75d349a7aaea16804f01a4c6cf729e13b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 21 May 2020 15:13:16 -0700 Subject: [PATCH] feat(default context): support selected options for default context (#2177) --- browsers.json | 2 +- docs/api.md | 27 ++- src/browser.ts | 10 +- src/browserContext.ts | 44 ++++- src/chromium/crBrowser.ts | 20 +- src/firefox/ffBrowser.ts | 22 ++- src/server/browserServer.ts | 4 +- src/server/browserType.ts | 25 +-- src/server/chromium.ts | 3 +- src/server/electron.ts | 4 +- src/server/firefox.ts | 3 +- src/server/webkit.ts | 3 +- src/webkit/wkBrowser.ts | 25 ++- test/browsercontext.spec.js | 6 + test/defaultbrowsercontext.spec.js | 291 +++++++++++++++++++++++++++-- test/launcher.spec.js | 119 +----------- test/utils.js | 4 +- 17 files changed, 416 insertions(+), 196 deletions(-) diff --git a/browsers.json b/browsers.json index d27f2d49f9..28f52ca612 100644 --- a/browsers.json +++ b/browsers.json @@ -6,7 +6,7 @@ }, { "name": "firefox", - "revision": "1094" + "revision": "1097" }, { "name": "webkit", diff --git a/docs/api.md b/docs/api.md index 44b1353ed3..ae457a358a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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` 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 [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. + - `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: diff --git a/src/browser.ts b/src/browser.ts index d361ff867f..36e3e06936 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -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); } diff --git a/src/browserContext.ts b/src/browserContext.ts index 234cf0d96c..ea02a152ad 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -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; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 6161f7471a..3604aec222 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -56,6 +56,8 @@ export class CRBrowser extends BrowserBase { return browser; } + browser._defaultContext = new CRBrowserContext(browser, null, options.persistent); + const existingTargetAttachPromises: Promise[] = []; // 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[] = [ - this._browser._session.send('Browser.setDownloadBehavior', { + assert(!Array.from(this._browser._crPages.values()).some(page => page._browserContext === this)); + const promises: Promise[] = []; + 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); } diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 3ea517fdf1..f3aeef1a8c 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -37,7 +37,14 @@ export class FFBrowser extends BrowserBase { static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise { 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[] = [ + 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[] = [ - this._browser._connection.send('Browser.setDownloadOptions', { + const promises: Promise[] = []; + 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 }, diff --git a/src/server/browserServer.ts b/src/server/browserServer.ts index fcba491aec..6624d9ee16 100644 --- a/src/server/browserServer.ts +++ b/src/server/browserServer.ts @@ -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, transport: ConnectionTransport, downloadsPath: string, webSocketWrapper: WebSocketWrapper | null) { + constructor(options: LaunchOptions, process: ChildProcess, gracefullyClose: () => Promise, transport: ConnectionTransport, downloadsPath: string | undefined, webSocketWrapper: WebSocketWrapper | null) { super(); this._launchOptions = options; this._headful = !processBrowserArgOptions(options).headless; diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 31c80c8b33..41460452d4 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -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; launchServer(options?: LaunchServerOptions): Promise; - launchPersistentContext(userDataDir: string, options?: LaunchOptions): Promise; + launchPersistentContext(userDataDir: string, options?: LaunchOptions & PersistentContextOptions): Promise; connect(options: ConnectOptions): Promise; } @@ -88,15 +88,16 @@ export abstract class BrowserTypeBase implements BrowserType { async launch(options: LaunchOptions = {}): Promise { 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 { - const browser = await this._innerLaunch('persistent', options, userDataDir); + async launchPersistentContext(userDataDir: string, options: LaunchOptions & PersistentContextOptions = {}): Promise { + 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 { + async _innerLaunch(launchType: LaunchType, options: LaunchOptions, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise { 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 { + async _innerLaunchPromise(browserServer: BrowserServer, options: LaunchOptions, persistent: PersistentContextOptions | undefined): Promise { 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 { 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; - abstract _connectToServer(browserServer: BrowserServer, persistent: boolean): Promise; + abstract _connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise; abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise; } diff --git a/src/server/chromium.ts b/src/server/chromium.ts index 863bfd06a3..73c6d4bdcb 100644 --- a/src/server/chromium.ts +++ b/src/server/chromium.ts @@ -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 { + async _connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise { const options = browserServer._launchOptions; let devtools = this._devtools; if ((options as any).__testHookForDevTools) { diff --git a/src/server/electron.ts b/src/server/electron.ts index 173fe08457..e3a3c9973d 100644 --- a/src/server/electron.ts +++ b/src/server/electron.ts @@ -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; diff --git a/src/server/firefox.ts b/src/server/firefox.ts index 92edd2845a..55bd54d61f 100644 --- a/src/server/firefox.ts +++ b/src/server/firefox.ts @@ -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 { + _connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise { return FFBrowser.connect(browserServer._transport, { slowMo: browserServer._launchOptions.slowMo, logger: browserServer._logger, diff --git a/src/server/webkit.ts b/src/server/webkit.ts index fcfc469f08..bd3a4ef425 100644 --- a/src/server/webkit.ts +++ b/src/server/webkit.ts @@ -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 { + _connectToServer(browserServer: BrowserServer, persistent: PersistentContextOptions | undefined): Promise { return WKBrowser.connect(browserServer._transport, { slowMo: browserServer._launchOptions.slowMo, headful: browserServer._headful, diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 9ed53359b1..8904d48044 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -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 { const browser = new WKBrowser(SlowMoTransport.wrap(transport, options.slowMo), options); - await browser._browserSession.send('Playwright.enable'); + const promises: Promise[] = [ + 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[] = [ - this._browser._browserSession.send('Playwright.setDownloadBehavior', { + const promises: Promise[] = []; + 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) diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index e124cb28a7..19b3b2fbef 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -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"'); diff --git a/test/defaultbrowsercontext.spec.js b/test/defaultbrowsercontext.spec.js index a5cd4f4349..206cb51a77 100644 --- a/test/defaultbrowsercontext.spec.js +++ b/test/defaultbrowsercontext.spec.js @@ -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, '); + 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(`download`); + 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); }); }); diff --git a/test/launcher.spec.js b/test/launcher.spec.js index 90c021cf9d..4746cd043c 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -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); - }); -}); diff --git a/test/utils.js b/test/utils.js index 5d206d1df4..d79408e6b4 100644 --- a/test/utils.js +++ b/test/utils.js @@ -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