diff --git a/docs/api.md b/docs/api.md index e76afbdeb0..7048bd901e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -32,15 +32,13 @@ * [browser.browserContexts()](#browserbrowsercontexts) * [browser.chromium](#browserchromium) * [browser.close()](#browserclose) - * [browser.defaultBrowserContext()](#browserdefaultbrowsercontext) + * [browser.defaultContext()](#browserdefaultContext()) * [browser.disconnect()](#browserdisconnect) * [browser.isConnected()](#browserisconnected) - * [browser.newContext()](#browsernewcontext) - * [browser.newPage()](#browsernewpage) + * [browser.newContext(options)](#browsernewcontextoptions) + * [browser.newPage(options)](#browsernewpageoptions) * [browser.pages()](#browserpages) * [browser.process()](#browserprocess) - * [browser.userAgent()](#browseruseragent) - * [browser.version()](#browserversion) - [class: BrowserContext](#class-browsercontext) * [browserContext.browser()](#browsercontextbrowser) * [browserContext.clearCookies()](#browsercontextclearcookies) @@ -48,12 +46,12 @@ * [browserContext.cookies([...urls])](#browsercontextcookiesurls) * [browserContext.isIncognito()](#browsercontextisincognito) * [browserContext.newPage()](#browsercontextnewpage) + * [browserContext.overrides](#browsercontextoverrides) * [browserContext.pages()](#browsercontextpages) * [browserContext.permissions](#browsercontextpermissions) * [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) - [class: Overrides](#class-overrides) * [overrides.setGeolocation(options)](#overridessetgeolocationoptions) - * [overrides.setTimezone(timezoneId)](#overridessettimezonetimezoneid) - [class: Permissions](#class-permissions) * [permissions.clearOverrides()](#permissionsclearoverrides) * [permissions.override(origin, permissions)](#permissionsoverrideorigin-permissions) @@ -88,8 +86,6 @@ * [page.content()](#pagecontent) * [page.coverage](#pagecoverage) * [page.dblclick(selector[, options])](#pagedblclickselector-options) - * [page.emulate(options)](#pageemulateoptions) - * [page.emulateMedia(options)](#pageemulatemediaoptions) * [page.evaluate(pageFunction[, ...args])](#pageevaluatepagefunction-args) * [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args) * [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args) @@ -106,25 +102,19 @@ * [page.keyboard](#pagekeyboard) * [page.mainFrame()](#pagemainframe) * [page.mouse](#pagemouse) - * [page.overrides](#pageoverrides) * [page.pdf](#pagepdf) * [page.reload([options])](#pagereloadoptions) * [page.screenshot([options])](#pagescreenshotoptions) * [page.select(selector, ...values)](#pageselectselector-values) - * [page.setBypassCSP(enabled)](#pagesetbypasscspenabled) * [page.setCacheEnabled([enabled])](#pagesetcacheenabledenabled) * [page.setContent(html[, options])](#pagesetcontenthtml-options) * [page.setDefaultNavigationTimeout(timeout)](#pagesetdefaultnavigationtimeouttimeout) * [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) * [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) - * [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) - * [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) - * [page.setViewport(viewport)](#pagesetviewportviewport) * [page.title()](#pagetitle) * [page.tripleclick(selector[, options])](#pagetripleclickselector-options) * [page.type(selector, text[, options])](#pagetypeselector-text-options) * [page.url()](#pageurl) - * [page.viewport()](#pageviewport) * [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) * [page.waitForFileChooser([options])](#pagewaitforfilechooseroptions) * [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) @@ -381,14 +371,6 @@ const playwright = require('playwright'); - `options` <[Object]> - `browserWSEndpoint` a [browser websocket endpoint](#browserwsendpoint) to connect to. - `browserURL` a browser url to connect to, in format `http://${host}:${port}`. Use interchangeably with `browserWSEndpoint` to let Playwright fetch it from [metadata endpoint](https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target). - - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. - - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. - - `width` <[number]> page width in pixels. - - `height` <[number]> page height in pixels. - - `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. Defaults to `false`. - - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` - - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. - `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. - `transport` <[ConnectionTransport]> **Experimental** Specify a custom transport object for Playwright to use. - returns: <[Promise]<[Browser]>> @@ -476,17 +458,9 @@ try { #### playwright.launch([options]) - `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: - - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `headless` <[boolean]> Whether to run browser in [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). Defaults to `true` unless the `devtools` option is `true`. - `executablePath` <[string]> Path to a Chromium or Chrome executable to run instead of the bundled Chromium. If `executablePath` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). **BEWARE**: Playwright is only [guaranteed to work](https://github.com/Microsoft/playwright/#q-why-doesnt-playwright-vxxx-work-with-chromium-vyyy) with the bundled Chromium, use at your own risk. - `slowMo` <[number]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. - - `defaultViewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. - - `width` <[number]> page width in pixels. - - `height` <[number]> page height in pixels. - - `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. Defaults to `false`. - - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` - - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. - `args` <[Array]<[string]>> Additional arguments to pass to the browser instance. The list of Chromium flags can be found [here](http://peter.sh/experiments/chromium-command-line-switches/). - `ignoreDefaultArgs` <[boolean]|[Array]<[string]>> If `true`, then do not use [`playwright.defaultArgs()`](#playwrightdefaultargsoptions). If an array is given, then filter out the given default arguments. Dangerous option; use with care. Defaults to `false`. - `handleSIGINT` <[boolean]> Close the browser process on Ctrl-C. Defaults to `true`. @@ -627,7 +601,7 @@ a single instance of [BrowserContext]. Closes Chromium and all of its pages (if any were opened). The [Browser] object itself is considered to be disposed and cannot be used anymore. -#### browser.defaultBrowserContext() +#### browser.defaultContext() - returns: <[BrowserContext]> Returns the default browser context. The default browser context can not be closed. @@ -642,7 +616,23 @@ Disconnects Playwright from the browser, but leaves the Chromium process running Indicates that the browser is connected. -#### browser.newContext() +#### browser.newContext(options) +- `options` <[Object]> + - `ignoreHTTPSErrors` Whether to ignore HTTPS errors during navigation. Defaults to `false`. + - `bypassCSP` Toggles bypassing page's Content-Security-Policy. + - `viewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `width` <[number]> page width in pixels. + - `height` <[number]> page height in pixels. + - `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. Defaults to `false`. + - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` + - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. + - `userAgent` Specific user agent to use in this page + - `mediaType` Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables CSS media emulation. + - `colorScheme` Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. + - `javaScriptEnabled` Whether or not to enable or disable JavaScript in the page. Defaults to true. + - `timezoneId` Changes the timezone of the page. 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. + - returns: <[Promise]<[BrowserContext]>> Creates a new browser context. It won't share cookies/cache with other browser contexts. @@ -659,10 +649,24 @@ Creates a new browser context. It won't share cookies/cache with other browser c })(); ``` -#### browser.newPage() -- returns: <[Promise]<[Page]>> +#### browser.newPage(options) +- `options` <[Object]> + - `ignoreHTTPSErrors` Whether to ignore HTTPS errors during navigation. Defaults to `false`. + - `bypassCSP` Toggles bypassing page's Content-Security-Policy. + - `viewport` Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. + - `width` <[number]> page width in pixels. + - `height` <[number]> page height in pixels. + - `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. Defaults to `false`. + - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` + - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. + - `userAgent` Specific user agent to use in this page + - `mediaType` Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables CSS media emulation. + - `colorScheme` Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. + - `javaScriptEnabled` Whether or not to enable or disable JavaScript in the page. Defaults to true. + - `timezoneId` Changes the timezone of the page. 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. -Promise which resolves to a new [Page] object. The [Page] is created in a default browser context. +Promise which resolves to a new [Page] object. The [Page] is created in a new browser context that it will own. Closing this page will close the context. #### browser.pages() - returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [target.page()](#targetpage). @@ -673,16 +677,6 @@ the method will return an array with all the pages in all browser contexts. #### browser.process() - returns: Spawned browser process. Returns `null` if the browser instance was created with [`playwright.connect`](#playwrightconnectoptions) method. -#### browser.userAgent() -- returns: <[Promise]<[string]>> Promise which resolves to the browser's original user agent. - -> **NOTE** Pages can override browser user agent with [page.setUserAgent](#pagesetuseragentuseragent) - -#### browser.version() -- returns: <[Promise]<[string]>> For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is similar to `Chrome/61.0.3153.0`. - -> **NOTE** the format of browser.version() might change with future releases of Chromium. - ### class: BrowserContext * extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) @@ -755,6 +749,9 @@ The default browser context is the only non-incognito browser context. Creates a new page in the browser context. +#### browserContext.overrides +- returns: <[Overrides]> + #### browserContext.pages() - returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [target.page()](#targetpage). @@ -792,15 +789,11 @@ await browserContext.setCookies([cookieObject1, cookieObject2]); Sets the page's geolocation. ```js -await page.overrides.setGeolocation({latitude: 59.95, longitude: 30.31667}); +await browserContext.overrides.setGeolocation({latitude: 59.95, longitude: 30.31667}); ``` > **NOTE** Consider using [browserContext.permissions.override](#permissionsoverrideorigin-permissions) to grant permissions for the page to read its geolocation. -#### overrides.setTimezone(timezoneId) -- `timezoneId` Changes the timezone of the page. 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. Passing `null` disables timezone emulation. -- returns: <[Promise]> - ### class: Permissions #### permissions.clearOverrides() @@ -809,7 +802,7 @@ await page.overrides.setGeolocation({latitude: 59.95, longitude: 30.31667}); Clears all permission overrides for the browser context. ```js -const context = browser.defaultBrowserContext(); +const context = browser.defaultContext(); context.permissions.override('https://example.com', ['clipboard-read']); // do stuff .. context.permissions.clearOverrides(); @@ -838,7 +831,7 @@ context.permissions.clearOverrides(); ```js -const context = browser.defaultBrowserContext(); +const context = browser.defaultContext(); await context.permissions.override('https://html5demos.com', ['geolocation']); ``` @@ -1150,77 +1143,6 @@ Bear in mind that if the first click of the `dblclick()` triggers a navigation e Shortcut for [page.mainFrame().dblclick(selector[, options])](#framedblclickselector-options). -#### page.emulate(options) -- `options` <[Object]> - - `viewport` <[Object]> - - `width` <[number]> page width in pixels. - - `height` <[number]> page height in pixels. - - `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. Defaults to `false`. - - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` - - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. - - `userAgent` <[string]> -- returns: <[Promise]> - -Emulates given device metrics and user agent. This method is a shortcut for calling two methods: -- [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) -- [page.setViewport(viewport)](#pagesetviewportviewport) - -To aid emulation, playwright provides a list of device descriptors which can be obtained via the [`playwright.devices`](#playwrightdevices). - -`page.emulate` will resize the page. A lot of websites don't expect phones to change size, so you should emulate before navigating to the page. - -```js -const playwright = require('playwright'); -const iPhone = playwright.devices['iPhone 6']; - -(async () => { - const browser = await playwright.launch(); - const page = await browser.newPage(); - await page.emulate(iPhone); - await page.goto('https://www.google.com'); - // other actions... - await browser.close(); -})(); -``` - -List of all available devices is available in the source code: [DeviceDescriptors.js](https://github.com/Microsoft/playwright/blob/master/lib/DeviceDescriptors.js). - -#### page.emulateMedia(options) -- `options` <[Object]> - - `type` Optional. Changes the CSS media type of the page. The only allowed values are `'screen'`, `'print'` and `null`. Passing `null` disables CSS media emulation. - - `colorScheme` <"dark"|"light"|"no-preference"> Optional. Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. -- returns: <[Promise]> - -```js -await page.evaluate(() => matchMedia('screen').matches)); -// → true -await page.evaluate(() => matchMedia('print').matches)); -// → true - -await page.emulateMedia({ type: 'print' }); -await page.evaluate(() => matchMedia('screen').matches)); -// → false -await page.evaluate(() => matchMedia('print').matches)); -// → true - -await page.emulateMedia({}); -await page.evaluate(() => matchMedia('screen').matches)); -// → true -await page.evaluate(() => matchMedia('print').matches)); -// → true -``` - -```js -await page.emulateMedia({ colorScheme: 'dark' }] }); -await page.evaluate(() => matchMedia('(prefers-color-scheme: dark)').matches)); -// → true -await page.evaluate(() => matchMedia('(prefers-color-scheme: light)').matches)); -// → false -await page.evaluate(() => matchMedia('(prefers-color-scheme: no-preference)').matches)); -// → false -``` - #### page.evaluate(pageFunction[, ...args]) - `pageFunction` <[function]|[string]> Function to be evaluated in the page context - `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction` @@ -1480,9 +1402,6 @@ Page is guaranteed to have a main frame which persists during navigations. - returns: <[Mouse]> -#### page.overrides -- returns: <[Overrides]> - #### page.pdf - returns: <[PDF]> @@ -1539,15 +1458,6 @@ page.select('select#colors', { value: 'blue' }, { index: 2 }, 'red'); Shortcut for [page.mainFrame().select()](#frameselectselector-values) -#### page.setBypassCSP(enabled) -- `enabled` <[boolean]> sets bypassing of page's Content-Security-Policy. -- returns: <[Promise]> - -Toggles bypassing page's Content-Security-Policy. - -> **NOTE** CSP bypassing happens at the moment of CSP initialization rather then evaluation. Usually this means -that `page.setBypassCSP` should be called before navigating to the domain. - #### page.setCacheEnabled([enabled]) - `enabled` <[boolean]> sets the `enabled` state of the cache. - returns: <[Promise]> @@ -1607,42 +1517,6 @@ The extra HTTP headers will be sent with every request the page initiates. > **NOTE** page.setExtraHTTPHeaders does not guarantee the order of headers in the outgoing requests. -#### page.setJavaScriptEnabled(enabled) -- `enabled` <[boolean]> Whether or not to enable JavaScript on the page. -- returns: <[Promise]> - -> **NOTE** changing this value won't affect scripts that have already been run. It will take full effect on the next [navigation](#pagegotourl-options). - -#### page.setUserAgent(userAgent) -- `userAgent` <[string]> Specific user agent to use in this page -- returns: <[Promise]> Promise which resolves when the user agent is set. - -#### page.setViewport(viewport) -- `viewport` <[Object]> - - `width` <[number]> page width in pixels. **required** - - `height` <[number]> page height in pixels. **required** - - `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. Defaults to `false`. - - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` - - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. -- returns: <[Promise]> - -> **NOTE** in certain cases, setting viewport will reload the page in order to set the `isMobile` or `hasTouch` properties. - -In the case of multiple pages in a single browser, each page can have its own viewport size. - -`page.setViewport` will resize the page. A lot of websites don't expect phones to change size, so you should set the viewport before navigating to the page. - -```js -const page = await browser.newPage(); -await page.setViewport({ - width: 640, - height: 480, - deviceScaleFactor: 1, -}); -await page.goto('https://example.com'); -``` - #### page.title() - returns: <[Promise]<[string]>> The page's title. @@ -1691,15 +1565,6 @@ Shortcut for [page.mainFrame().type(selector, text[, options])](#frametypeselect This is a shortcut for [page.mainFrame().url()](#frameurl) -#### page.viewport() -- returns: - - `width` <[number]> page width in pixels. - - `height` <[number]> page height in pixels. - - `deviceScaleFactor` <[number]> Specify device scale factor (can be though of as dpr). Defaults to `1`. - - `isMobile` <[boolean]> Whether the `meta viewport` tag is taken into account. Defaults to `false`. - - `hasTouch`<[boolean]> Specifies if viewport supports touch events. Defaults to `false` - - `isLandscape` <[boolean]> Specifies if viewport is in landscape mode. Defaults to `false`. - #### page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]]) - `selectorOrFunctionOrTimeout` <[string]|[number]|[function]> A [selector], predicate or timeout to wait for - `options` <[Object]> Optional waiting parameters diff --git a/src/browserContext.ts b/src/browserContext.ts index 07b8432b2d..36cede7db2 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -17,14 +17,16 @@ import { assert } from './helper'; import { Page } from './page'; +import * as input from './input'; import * as network from './network'; +import * as types from './types'; import * as childProcess from 'child_process'; export interface BrowserInterface { browserContexts(): BrowserContext[]; close(): Promise; newContext(): Promise; - defaultBrowserContext(): BrowserContext; + defaultContext(): BrowserContext; newPage(): Promise; pages(): Promise; process(): childProcess.ChildProcess | null; @@ -41,15 +43,30 @@ export interface BrowserDelegate { setContextCookies(cookies: network.SetNetworkCookieParam[]): Promise; } +export type BrowserContextOptions = { + viewport?: types.Viewport | null, + ignoreHTTPSErrors?: boolean, + javaScriptEnabled?: boolean, + bypassCSP?: boolean, + mediaType?: input.MediaType, + colorScheme?: input.ColorScheme, + userAgent?: string, + timezoneId?: string +}; + export class BrowserContext { private readonly _delegate: BrowserDelegate; private readonly _browser: BrowserInterface; private readonly _isIncognito: boolean; + readonly _options: BrowserContextOptions; - constructor(delegate: BrowserDelegate, browser: BrowserInterface, isIncognito: boolean) { + constructor(delegate: BrowserDelegate, browser: BrowserInterface, isIncognito: boolean, options: BrowserContextOptions) { this._delegate = delegate; this._browser = browser; this._isIncognito = isIncognito; + this._options = options; + if (!options.viewport && options.viewport !== null) + options.viewport = { width: 800, height: 600 }; } async pages(): Promise { @@ -64,6 +81,12 @@ export class BrowserContext { return this._delegate.createPageInContext(); } + async _createOwnerPage(): Promise { + const page = await this._delegate.createPageInContext(); + page._isContextOwner = true; + return page; + } + browser(): BrowserInterface { return this._browser; } diff --git a/src/chromium/Browser.ts b/src/chromium/Browser.ts index ef2143366b..9a3a612ef6 100644 --- a/src/chromium/Browser.ts +++ b/src/chromium/Browser.ts @@ -19,20 +19,19 @@ import * as childProcess from 'child_process'; import { EventEmitter } from 'events'; import { Events } from './events'; import { assert, helper } from '../helper'; -import { BrowserContext, BrowserInterface } from '../browserContext'; +import { BrowserContext, BrowserInterface, BrowserContextOptions } from '../browserContext'; import { Connection, ConnectionEvents, CDPSession } from './Connection'; import { Page } from '../page'; import { Target } from './Target'; import { Protocol } from './protocol'; import { Chromium } from './features/chromium'; -import * as types from '../types'; import { FrameManager } from './FrameManager'; +import * as events from '../events'; import * as network from '../network'; import { Permissions } from './features/permissions'; +import { Overrides } from './features/overrides'; export class Browser extends EventEmitter implements BrowserInterface { - private _ignoreHTTPSErrors: boolean; - private _defaultViewport: types.Viewport; private _process: childProcess.ChildProcess; _connection: Connection; _client: CDPSession; @@ -45,11 +44,9 @@ export class Browser extends EventEmitter implements BrowserInterface { static async create( connection: Connection, contextIds: string[], - ignoreHTTPSErrors: boolean, - defaultViewport: types.Viewport | null, process: childProcess.ChildProcess | null, closeCallback?: (() => Promise)) { - const browser = new Browser(connection, contextIds, ignoreHTTPSErrors, defaultViewport, process, closeCallback); + const browser = new Browser(connection, contextIds, process, closeCallback); await connection.rootSession.send('Target.setDiscoverTargets', { discover: true }); return browser; } @@ -57,22 +54,18 @@ export class Browser extends EventEmitter implements BrowserInterface { constructor( connection: Connection, contextIds: string[], - ignoreHTTPSErrors: boolean, - defaultViewport: types.Viewport | null, process: childProcess.ChildProcess | null, closeCallback?: (() => Promise)) { super(); this._connection = connection; this._client = connection.rootSession; - this._ignoreHTTPSErrors = ignoreHTTPSErrors; - this._defaultViewport = defaultViewport; this._process = process; this._closeCallback = closeCallback || (() => Promise.resolve()); this.chromium = new Chromium(this); - this._defaultContext = this._createBrowserContext(null); + this._defaultContext = this._createBrowserContext(null, {}); for (const contextId of contextIds) - this._contexts.set(contextId, this._createBrowserContext(contextId)); + this._contexts.set(contextId, this._createBrowserContext(contextId, {})); this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); this._client.on('Target.targetCreated', this._targetCreated.bind(this)); @@ -80,8 +73,9 @@ export class Browser extends EventEmitter implements BrowserInterface { this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); } - _createBrowserContext(contextId: string | null): BrowserContext { + _createBrowserContext(contextId: string | null, options: BrowserContextOptions): BrowserContext { const isIncognito = !!contextId; + let overrides: Overrides | null = null; const context = new BrowserContext({ contextPages: async (): Promise => { const targets = this._allTargets().filter(target => target.browserContext() === context && target.type() === 'page'); @@ -94,6 +88,25 @@ export class Browser extends EventEmitter implements BrowserInterface { const target = this._targets.get(targetId); assert(await target._initializedPromise, 'Failed to create target for page'); const page = await target.page(); + const session = (page._delegate as FrameManager)._client; + const promises: Promise[] = [ overrides._applyOverrides(page) ]; + if (options.bypassCSP) + promises.push(session.send('Page.setBypassCSP', { enabled: true })); + if (options.ignoreHTTPSErrors) + promises.push(session.send('Security.setIgnoreCertificateErrors', { ignore: true })); + if (options.viewport) + promises.push(page._delegate.setViewport(options.viewport)); + if (options.javaScriptEnabled === false) + promises.push(session.send('Emulation.setScriptExecutionDisabled', { value: true })); + if (options.userAgent) + (page._delegate as FrameManager)._networkManager.setUserAgent(options.userAgent); + if (options.mediaType || options.colorScheme) { + const features = options.colorScheme ? [{ name: 'prefers-color-scheme', value: options.colorScheme }] : []; + promises.push(session.send('Emulation.setEmulatedMedia', { media: options.mediaType || '', features })); + } + if (options.timezoneId) + promises.push(emulateTimezone(session, options.timezoneId)); + await Promise.all(promises); return page; }, @@ -119,8 +132,10 @@ export class Browser extends EventEmitter implements BrowserInterface { setContextCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { await this._client.send('Storage.setCookies', { cookies, browserContextId: contextId || undefined }); }, - }, this, isIncognito); + }, this, isIncognito, options); + overrides = new Overrides(context); (context as any).permissions = new Permissions(this._client, contextId); + (context as any).overrides = overrides; return context; } @@ -128,9 +143,9 @@ export class Browser extends EventEmitter implements BrowserInterface { return this._process; } - async newContext(): Promise { - const {browserContextId} = await this._client.send('Target.createBrowserContext'); - const context = this._createBrowserContext(browserContextId); + async newContext(options: BrowserContextOptions = {}): Promise { + const { browserContextId } = await this._client.send('Target.createBrowserContext'); + const context = this._createBrowserContext(browserContextId, options); this._contexts.set(browserContextId, context); return context; } @@ -139,7 +154,7 @@ export class Browser extends EventEmitter implements BrowserInterface { return [this._defaultContext, ...Array.from(this._contexts.values())]; } - defaultBrowserContext(): BrowserContext { + defaultContext(): BrowserContext { return this._defaultContext; } @@ -148,7 +163,7 @@ export class Browser extends EventEmitter implements BrowserInterface { const {browserContextId} = targetInfo; const context = (browserContextId && this._contexts.has(browserContextId)) ? this._contexts.get(browserContextId) : this._defaultContext; - const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo), this._ignoreHTTPSErrors, this._defaultViewport); + const target = new Target(targetInfo, context, () => this._connection.createSession(targetInfo)); assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated'); this._targets.set(event.targetInfo.targetId, target); @@ -175,8 +190,9 @@ export class Browser extends EventEmitter implements BrowserInterface { this.chromium.emit(Events.Chromium.TargetChanged, target); } - async newPage(): Promise { - return this._defaultContext.newPage(); + async newPage(options?: BrowserContextOptions): Promise { + const context = await this.newContext(options); + return context._createOwnerPage(); } async _closePage(page: Page) { @@ -250,3 +266,13 @@ export class Browser extends EventEmitter implements BrowserInterface { return this._client.send('Browser.getVersion'); } } + +async function emulateTimezone(session: CDPSession, timezoneId: string) { + try { + await session.send('Emulation.setTimezoneOverride', { timezoneId: timezoneId }); + } catch (exception) { + if (exception.message.includes('Invalid timezone')) + throw new Error(`Invalid timezone ID: ${timezoneId}`); + throw exception; + } +} diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 2c8f1ae775..ac9bbab30f 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -33,7 +33,6 @@ import { Accessibility } from './features/accessibility'; import { Coverage } from './features/coverage'; import { PDF } from './features/pdf'; import { Workers } from './features/workers'; -import { Overrides } from './features/overrides'; import { Interception } from './features/interception'; import { Browser } from './Browser'; import { BrowserContext } from '../browserContext'; @@ -46,24 +45,23 @@ const UTILITY_WORLD_NAME = '__playwright_utility_world__'; export class FrameManager implements PageDelegate { _client: CDPSession; private _page: Page; - private _networkManager: NetworkManager; + readonly _networkManager: NetworkManager; private _contextIdToContext = new Map(); private _isolatedWorlds = new Set(); private _eventListeners: RegisteredListener[]; rawMouse: RawMouseImpl; rawKeyboard: RawKeyboardImpl; - constructor(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean) { + constructor(client: CDPSession, browserContext: BrowserContext) { this._client = client; this.rawKeyboard = new RawKeyboardImpl(client); this.rawMouse = new RawMouseImpl(client); this._page = new Page(this, browserContext); - this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this._page); + this._networkManager = new NetworkManager(client, this._page); (this._page as any).accessibility = new Accessibility(client); (this._page as any).coverage = new Coverage(client); (this._page as any).pdf = new PDF(client); (this._page as any).workers = new Workers(client, this._page._addConsoleMessage.bind(this._page), error => this._page.emit(Events.Page.PageError, error)); - (this._page as any).overrides = new Overrides(client); (this._page as any).interception = new Interception(this._networkManager); this._eventListeners = [ @@ -276,18 +274,6 @@ export class FrameManager implements PageDelegate { await this._client.send('Network.setExtraHTTPHeaders', { headers }); } - setUserAgent(userAgent: string): Promise { - return this._networkManager.setUserAgent(userAgent); - } - - async setJavaScriptEnabled(enabled: boolean): Promise { - await this._client.send('Emulation.setScriptExecutionDisabled', { value: !enabled }); - } - - async setBypassCSP(enabled: boolean): Promise { - await this._client.send('Page.setBypassCSP', { enabled }); - } - async setViewport(viewport: types.Viewport): Promise { const { width, @@ -306,7 +292,7 @@ export class FrameManager implements PageDelegate { ]); } - async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise { + async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise { const features = mediaColorScheme ? [{ name: 'prefers-color-scheme', value: mediaColorScheme }] : []; await this._client.send('Emulation.setEmulatedMedia', { media: mediaType || '', features }); } diff --git a/src/chromium/Launcher.ts b/src/chromium/Launcher.ts index 84418af2eb..9e6bcd8cc2 100644 --- a/src/chromium/Launcher.ts +++ b/src/chromium/Launcher.ts @@ -26,7 +26,6 @@ import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher'; import { Connection } from './Connection'; import { TimeoutError } from '../errors'; import { assert, debugError, helper } from '../helper'; -import * as types from '../types'; import { ConnectionTransport, WebSocketTransport, PipeTransport } from '../transport'; import * as util from 'util'; import { launchProcess, waitForLine } from '../processLauncher'; @@ -71,7 +70,7 @@ export class Launcher { this._preferredRevision = preferredRevision; } - async launch(options: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) = {}): Promise { + async launch(options: (LauncherLaunchOptions & LauncherChromeArgOptions & ConnectionOptions) = {}): Promise { const { ignoreDefaultArgs = false, args = [], @@ -82,8 +81,6 @@ export class Launcher { handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, - ignoreHTTPSErrors = false, - defaultViewport = {width: 800, height: 600}, slowMo = 0, timeout = 30000 } = options; @@ -146,7 +143,7 @@ export class Launcher { const transport = new PipeTransport(launched.process.stdio[3] as NodeJS.WritableStream, launched.process.stdio[4] as NodeJS.ReadableStream); connection = new Connection('', transport, slowMo); } - const browser = await Browser.create(connection, [], ignoreHTTPSErrors, defaultViewport, launched.process, launched.gracefullyClose); + const browser = await Browser.create(connection, [], launched.process, launched.gracefullyClose); await browser._waitForTarget(t => t.type() === 'page'); return browser; } catch (e) { @@ -184,15 +181,13 @@ export class Launcher { return this._resolveExecutablePath().executablePath; } - async connect(options: (LauncherBrowserOptions & { + async connect(options: (ConnectionOptions & { browserWSEndpoint?: string; browserURL?: string; transport?: ConnectionTransport; })): Promise { const { browserWSEndpoint, browserURL, - ignoreHTTPSErrors = false, - defaultViewport = {width: 800, height: 600}, transport, slowMo = 0, } = options; @@ -212,7 +207,7 @@ export class Launcher { } const { browserContextIds } = await connection.rootSession.send('Target.getBrowserContexts'); - return Browser.create(connection, browserContextIds, ignoreHTTPSErrors, defaultViewport, null, async () => { + return Browser.create(connection, browserContextIds, null, async () => { connection.rootSession.send('Browser.close').catch(debugError); }); } @@ -275,9 +270,7 @@ export type LauncherLaunchOptions = { pipe?: boolean, }; -export type LauncherBrowserOptions = { - ignoreHTTPSErrors?: boolean, - defaultViewport?: types.Viewport | null, +export type ConnectionOptions = { slowMo?: number, }; diff --git a/src/chromium/NetworkManager.ts b/src/chromium/NetworkManager.ts index df929bd15e..58fe1f2eec 100644 --- a/src/chromium/NetworkManager.ts +++ b/src/chromium/NetworkManager.ts @@ -24,7 +24,6 @@ import * as frames from '../frames'; export class NetworkManager { private _client: CDPSession; - private _ignoreHTTPSErrors: boolean; private _page: Page; private _requestIdToRequest = new Map(); private _requestIdToRequestWillBeSentEvent = new Map(); @@ -37,9 +36,8 @@ export class NetworkManager { private _requestIdToInterceptionId = new Map(); private _eventListeners: RegisteredListener[]; - constructor(client: CDPSession, ignoreHTTPSErrors: boolean, page: Page) { + constructor(client: CDPSession, page: Page) { this._client = client; - this._ignoreHTTPSErrors = ignoreHTTPSErrors; this._page = page; this._eventListeners = [ @@ -54,8 +52,6 @@ export class NetworkManager { async initialize() { await this._client.send('Network.enable'); - if (this._ignoreHTTPSErrors) - await this._client.send('Security.setIgnoreCertificateErrors', {ignore: true}); } dispose() { diff --git a/src/chromium/Playwright.ts b/src/chromium/Playwright.ts index 86e856734c..dc3cceb30a 100644 --- a/src/chromium/Playwright.ts +++ b/src/chromium/Playwright.ts @@ -20,7 +20,7 @@ import { BrowserFetcher, BrowserFetcherOptions, BrowserFetcherRevisionInfo, OnPr import { ConnectionTransport } from '../transport'; import { DeviceDescriptors, DeviceDescriptor } from '../deviceDescriptors'; import * as Errors from '../errors'; -import { Launcher, LauncherBrowserOptions, LauncherChromeArgOptions, LauncherLaunchOptions, createBrowserFetcher } from './Launcher'; +import { Launcher, ConnectionOptions, LauncherChromeArgOptions, LauncherLaunchOptions, createBrowserFetcher } from './Launcher'; type Devices = { [name: string]: DeviceDescriptor } & DeviceDescriptor[]; @@ -42,11 +42,11 @@ export class Playwright { return revisionInfo; } - launch(options?: (LauncherLaunchOptions & LauncherChromeArgOptions & LauncherBrowserOptions) | undefined): Promise { + launch(options?: (LauncherLaunchOptions & LauncherChromeArgOptions & ConnectionOptions) | undefined): Promise { return this._launcher.launch(options); } - connect(options: (LauncherBrowserOptions & { + connect(options: (ConnectionOptions & { browserWSEndpoint?: string; browserURL?: string; transport?: ConnectionTransport; })): Promise { diff --git a/src/chromium/Target.ts b/src/chromium/Target.ts index 98f46b069e..4004260845 100644 --- a/src/chromium/Target.ts +++ b/src/chromium/Target.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import * as types from '../types'; import { Browser } from './Browser'; import { BrowserContext } from '../browserContext'; import { CDPSession, CDPSessionEvents } from './Connection'; @@ -33,8 +32,6 @@ export class Target { private _browserContext: BrowserContext; _targetId: string; private _sessionFactory: () => Promise; - private _ignoreHTTPSErrors: boolean; - private _defaultViewport: types.Viewport; private _pagePromise: Promise | null = null; private _frameManager: FrameManager | null = null; private _workerPromise: Promise | null = null; @@ -49,15 +46,11 @@ export class Target { constructor( targetInfo: Protocol.Target.TargetInfo, browserContext: BrowserContext, - sessionFactory: () => Promise, - ignoreHTTPSErrors: boolean, - defaultViewport: types.Viewport | null) { + sessionFactory: () => Promise) { this._targetInfo = targetInfo; this._browserContext = browserContext; this._targetId = targetInfo.targetId; this._sessionFactory = sessionFactory; - this._ignoreHTTPSErrors = ignoreHTTPSErrors; - this._defaultViewport = defaultViewport; this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill).then(async success => { if (!success) return false; @@ -84,7 +77,7 @@ export class Target { async page(): Promise { if ((this._targetInfo.type === 'page' || this._targetInfo.type === 'background_page') && !this._pagePromise) { this._pagePromise = this._sessionFactory().then(async client => { - this._frameManager = new FrameManager(client, this._browserContext, this._ignoreHTTPSErrors); + this._frameManager = new FrameManager(client, this._browserContext); const page = this._frameManager.page(); (page as any)[targetSymbol] = this; client.once(CDPSessionEvents.Disconnected, () => page._didDisconnect()); @@ -96,8 +89,6 @@ export class Target { }); await this._frameManager.initialize(); await client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}); - if (this._defaultViewport) - await page.setViewport(this._defaultViewport); return page; }); } diff --git a/src/chromium/features/overrides.ts b/src/chromium/features/overrides.ts index fec511f526..8086ea781d 100644 --- a/src/chromium/features/overrides.ts +++ b/src/chromium/features/overrides.ts @@ -15,16 +15,26 @@ * limitations under the License. */ -import { CDPSession } from '../Connection'; +import { BrowserContext } from '../../browserContext'; +import { FrameManager } from '../FrameManager'; +import { Page } from '../api'; export class Overrides { - private _client: CDPSession; + private _context: BrowserContext; + private _geolocation: { longitude?: number; latitude?: number; accuracy?: number; } | null = null; - constructor(client: CDPSession) { - this._client = client; + constructor(context: BrowserContext) { + this._context = context; } - async setGeolocation(options: { longitude: number; latitude: number; accuracy: (number | undefined); }) { + async setGeolocation(options: { longitude?: number; latitude?: number; accuracy?: (number | undefined); } | null) { + if (!options) { + for (const page of await this._context.pages()) + await (page._delegate as FrameManager)._client.send('Emulation.clearGeolocationOverride', {}); + this._geolocation = null; + return; + } + const { longitude, latitude, accuracy = 0} = options; if (longitude < -180 || longitude > 180) throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`); @@ -32,16 +42,13 @@ export class Overrides { throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`); if (accuracy < 0) throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`); - await this._client.send('Emulation.setGeolocationOverride', {longitude, latitude, accuracy}); + this._geolocation = { longitude, latitude, accuracy }; + for (const page of await this._context.pages()) + await (page._delegate as FrameManager)._client.send('Emulation.setGeolocationOverride', this._geolocation); } - async setTimezone(timezoneId: string | null) { - try { - await this._client.send('Emulation.setTimezoneOverride', {timezoneId: timezoneId || ''}); - } catch (exception) { - if (exception.message.includes('Invalid timezone')) - throw new Error(`Invalid timezone ID: ${timezoneId}`); - throw exception; - } + async _applyOverrides(page: Page): Promise { + if (this._geolocation) + await (page._delegate as FrameManager)._client.send('Emulation.setGeolocationOverride', this._geolocation); } } diff --git a/src/dom.ts b/src/dom.ts index 74428cf3ba..96bfb935ca 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -123,7 +123,7 @@ export class ElementHandle extends js.JSHandle { element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'}); } return false; - }, !!this._page._state.javascriptEnabled); + }, !!this._page.browserContext()._options.javaScriptEnabled); if (error) throw new Error(error); } diff --git a/src/firefox/Browser.ts b/src/firefox/Browser.ts index 18ddc44be0..0652eb797f 100644 --- a/src/firefox/Browser.ts +++ b/src/firefox/Browser.ts @@ -26,11 +26,10 @@ import * as types from '../types'; import { FrameManager } from './FrameManager'; import { Firefox } from './features/firefox'; import * as network from '../network'; -import { BrowserContext, BrowserInterface } from '../browserContext'; +import { BrowserContext, BrowserInterface, BrowserContextOptions } from '../browserContext'; export class Browser extends EventEmitter implements BrowserInterface { _connection: Connection; - _defaultViewport: types.Viewport; private _process: import('child_process').ChildProcess; private _closeCallback: () => Promise; _targets: Map; @@ -39,27 +38,26 @@ export class Browser extends EventEmitter implements BrowserInterface { private _eventListeners: RegisteredListener[]; readonly firefox: Firefox; - static async create(connection: Connection, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => Promise) { + static async create(connection: Connection, process: import('child_process').ChildProcess | null, closeCallback: () => Promise) { const {browserContextIds} = await connection.send('Target.getBrowserContexts'); - const browser = new Browser(connection, browserContextIds, defaultViewport, process, closeCallback); + const browser = new Browser(connection, browserContextIds, process, closeCallback); await connection.send('Target.enable'); return browser; } - constructor(connection: Connection, browserContextIds: Array, defaultViewport: types.Viewport | null, process: import('child_process').ChildProcess | null, closeCallback: () => Promise) { + constructor(connection: Connection, browserContextIds: Array, process: import('child_process').ChildProcess | null, closeCallback: () => Promise) { super(); this._connection = connection; - this._defaultViewport = defaultViewport; this._process = process; this._closeCallback = closeCallback; this.firefox = new Firefox(this); this._targets = new Map(); - this._defaultContext = this._createBrowserContext(null); + this._defaultContext = this._createBrowserContext(null, {}); this._contexts = new Map(); for (const browserContextId of browserContextIds) - this._contexts.set(browserContextId, this._createBrowserContext(browserContextId)); + this._contexts.set(browserContextId, this._createBrowserContext(browserContextId, {})); this._connection.on(ConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected)); @@ -78,9 +76,12 @@ export class Browser extends EventEmitter implements BrowserInterface { return !this._connection._closed; } - async newContext(): Promise { + async newContext(options: BrowserContextOptions = {}): Promise { const {browserContextId} = await this._connection.send('Target.createBrowserContext'); - const context = this._createBrowserContext(browserContextId); + // TODO: move ignoreHTTPSErrors to browser context level. + if (options.ignoreHTTPSErrors) + await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true }); + const context = this._createBrowserContext(browserContextId, options); this._contexts.set(browserContextId, context); return context; } @@ -89,7 +90,7 @@ export class Browser extends EventEmitter implements BrowserInterface { return [this._defaultContext, ...Array.from(this._contexts.values())]; } - defaultBrowserContext() { + defaultContext() { return this._defaultContext; } @@ -114,7 +115,7 @@ export class Browser extends EventEmitter implements BrowserInterface { const existingTarget = this._allTargets().find(predicate); if (existingTarget) return existingTarget; - let resolve; + let resolve: (t: Target) => void; const targetPromise = new Promise(x => resolve = x); this.on('targetchanged', check); try { @@ -131,8 +132,9 @@ export class Browser extends EventEmitter implements BrowserInterface { } } - newPage(): Promise { - return this._defaultContext.newPage(); + async newPage(options?: BrowserContextOptions): Promise { + const context = await this.newContext(options); + return context._createOwnerPage(); } async pages() { @@ -173,7 +175,7 @@ export class Browser extends EventEmitter implements BrowserInterface { await this._closeCallback(); } - _createBrowserContext(browserContextId: string | null): BrowserContext { + _createBrowserContext(browserContextId: string | null, options: BrowserContextOptions): BrowserContext { const isIncognito = !!browserContextId; const context = new BrowserContext({ contextPages: async (): Promise => { @@ -187,7 +189,21 @@ export class Browser extends EventEmitter implements BrowserInterface { browserContextId: browserContextId || undefined }); const target = this._targets.get(targetId); - return await target.page(); + const page = await target.page(); + const session = (page._delegate as FrameManager)._session; + const promises: Promise[] = []; + if (options.viewport) + promises.push(page._delegate.setViewport(options.viewport)); + if (options.bypassCSP) + promises.push(session.send('Page.setBypassCSP', { enabled: true })); + if (options.javaScriptEnabled === false) + promises.push(session.send('Page.setJavascriptEnabled', { enabled: false })); + if (options.userAgent) + promises.push(session.send('Page.setUserAgent', { userAgent: options.userAgent })); + if (options.mediaType || options.colorScheme) + promises.push(session.send('Page.setEmulatedMedia', { type: options.mediaType, colorScheme: options.colorScheme })); + Promise.all(promises); + return page; }, closeContext: async (): Promise => { @@ -211,7 +227,7 @@ export class Browser extends EventEmitter implements BrowserInterface { setContextCookies: async (cookies: network.SetNetworkCookieParam[]): Promise => { await this._connection.send('Browser.setCookies', { browserContextId: browserContextId || undefined, cookies }); }, - }, this, isIncognito); + }, this, isIncognito, options); (context as any).permissions = new Permissions(this._connection, browserContextId); return context; } @@ -267,8 +283,6 @@ export class Target { const page = this._frameManager._page; session.once(JugglerSessionEvents.Disconnected, () => page._didDisconnect()); await this._frameManager._initialize(); - if (this._browser._defaultViewport) - await page.setViewport(this._browser._defaultViewport); f(page); }); } diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 4e1e804458..4a8ee48723 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -189,18 +189,6 @@ export class FrameManager implements PageDelegate { await this._session.send('Network.setExtraHTTPHeaders', { headers: array }); } - async setUserAgent(userAgent: string): Promise { - await this._session.send('Page.setUserAgent', { userAgent }); - } - - async setJavaScriptEnabled(enabled: boolean): Promise { - await this._session.send('Page.setJavascriptEnabled', { enabled }); - } - - async setBypassCSP(enabled: boolean): Promise { - await this._session.send('Page.setBypassCSP', { enabled }); - } - async setViewport(viewport: types.Viewport): Promise { const { width, @@ -215,7 +203,7 @@ export class FrameManager implements PageDelegate { }); } - async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise { + async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise { await this._session.send('Page.setEmulatedMedia', { type: mediaType === null ? undefined : mediaType, colorScheme: mediaColorScheme === null ? undefined : mediaColorScheme diff --git a/src/firefox/Launcher.ts b/src/firefox/Launcher.ts index 82bf78da2a..b33ec4a997 100644 --- a/src/firefox/Launcher.ts +++ b/src/firefox/Launcher.ts @@ -69,8 +69,6 @@ export class Launcher { handleSIGHUP = true, handleSIGINT = true, handleSIGTERM = true, - ignoreHTTPSErrors = false, - defaultViewport = {width: 800, height: 600}, slowMo = 0, timeout = 30000, } = options; @@ -129,9 +127,7 @@ export class Launcher { const url = match[1]; const transport = await WebSocketTransport.create(url); connection = new Connection(url, transport, slowMo); - const browser = await Browser.create(connection, defaultViewport, launched.process, launched.gracefullyClose); - if (ignoreHTTPSErrors) - await connection.send('Browser.setIgnoreHTTPSErrors', {enabled: true}); + const browser = await Browser.create(connection, launched.process, launched.gracefullyClose); await browser._waitForTarget(t => t.type() === 'page'); return browser; } catch (e) { @@ -144,16 +140,11 @@ export class Launcher { const { browserWSEndpoint, slowMo = 0, - defaultViewport = {width: 800, height: 600}, - ignoreHTTPSErrors = false, } = options; let connection = null; const transport = await WebSocketTransport.create(browserWSEndpoint); connection = new Connection(browserWSEndpoint, transport, slowMo); - const browser = await Browser.create(connection, defaultViewport, null, () => connection.send('Browser.close').catch(debugError)); - if (ignoreHTTPSErrors) - await connection.send('Browser.setIgnoreHTTPSErrors', {enabled: true}); - return browser; + return await Browser.create(connection, null, () => connection.send('Browser.close').catch(debugError)); } executablePath(): string { diff --git a/src/input.ts b/src/input.ts index c9c71682e8..a3e23d2a4f 100644 --- a/src/input.ts +++ b/src/input.ts @@ -406,5 +406,5 @@ export type FilePayload = { export type MediaType = 'screen' | 'print'; export const mediaTypes: Set = new Set(['screen', 'print']); -export type MediaColorScheme = 'dark' | 'light' | 'no-preference'; -export const mediaColorSchemes: Set = new Set(['dark', 'light', 'no-preference']); +export type ColorScheme = 'dark' | 'light' | 'no-preference'; +export const mediaColorSchemes: Set = new Set(['dark', 'light', 'no-preference']); diff --git a/src/page.ts b/src/page.ts index 06083c98d7..21a35e7540 100644 --- a/src/page.ts +++ b/src/page.ts @@ -45,11 +45,8 @@ export interface PageDelegate { needsLifecycleResetOnSetContent(): boolean; setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise; - setUserAgent(userAgent: string): Promise; - setJavaScriptEnabled(enabled: boolean): Promise; - setBypassCSP(enabled: boolean): Promise; setViewport(viewport: types.Viewport): Promise; - setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise; + setEmulateMedia(mediaType: input.MediaType | null, colorScheme: input.ColorScheme | null): Promise; setCacheEnabled(enabled: boolean): Promise; getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise; @@ -69,12 +66,9 @@ export interface PageDelegate { type PageState = { viewport: types.Viewport | null; - userAgent: string | null; mediaType: input.MediaType | null; - mediaColorScheme: input.MediaColorScheme | null; - javascriptEnabled: boolean | null; + colorScheme: input.ColorScheme | null; extraHTTPHeaders: network.Headers | null; - bypassCSP: boolean | null; cacheEnabled: boolean | null; }; @@ -99,6 +93,7 @@ export class Page extends EventEmitter { private _pageBindings = new Map(); readonly _screenshotter: Screenshotter; readonly _frameManager: frames.FrameManager; + _isContextOwner = false; constructor(delegate: PageDelegate, browserContext: BrowserContext) { super(); @@ -107,13 +102,10 @@ export class Page extends EventEmitter { this._disconnectedPromise = new Promise(f => this._disconnectedCallback = f); this._browserContext = browserContext; this._state = { - viewport: null, - userAgent: null, - mediaType: null, - mediaColorScheme: null, - javascriptEnabled: null, + viewport: browserContext._options.viewport || null, + mediaType: browserContext._options.mediaType || null, + colorScheme: browserContext._options.colorScheme || null, extraHTTPHeaders: null, - bypassCSP: null, cacheEnabled: null, }; this.keyboard = new input.Keyboard(delegate.rawKeyboard); @@ -244,11 +236,6 @@ export class Page extends EventEmitter { return this._delegate.setExtraHTTPHeaders(headers); } - setUserAgent(userAgent: string) { - this._state.userAgent = userAgent; - return this._delegate.setUserAgent(userAgent); - } - async _onBindingCalled(payload: string, context: js.ExecutionContext) { const {name, seq, args} = JSON.parse(payload); let expression = null; @@ -360,35 +347,15 @@ export class Page extends EventEmitter { return waitPromise; } - async emulate(options: { viewport: types.Viewport; userAgent: string; }) { - await Promise.all([ - this.setViewport(options.viewport), - this.setUserAgent(options.userAgent) - ]); - } - async setJavaScriptEnabled(enabled: boolean) { - if (this._state.javascriptEnabled === enabled) - return; - this._state.javascriptEnabled = enabled; - await this._delegate.setJavaScriptEnabled(enabled); - } - - async setBypassCSP(enabled: boolean) { - if (this._state.bypassCSP === enabled) - return; - this._state.bypassCSP = enabled; - await this._delegate.setBypassCSP(enabled); - } - - async emulateMedia(options: { type?: input.MediaType, colorScheme?: input.MediaColorScheme }) { + async emulateMedia(options: { type?: input.MediaType, colorScheme?: input.ColorScheme }) { assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type); assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme); if (options.type !== undefined) this._state.mediaType = options.type; if (options.colorScheme !== undefined) - this._state.mediaColorScheme = options.colorScheme; - await this._delegate.setEmulateMedia(this._state.mediaType, this._state.mediaColorScheme); + this._state.colorScheme = options.colorScheme; + await this._delegate.setEmulateMedia(this._state.mediaType, this._state.colorScheme); } async setViewport(viewport: types.Viewport) { @@ -436,6 +403,8 @@ export class Page extends EventEmitter { await this._delegate.closePage(runBeforeUnload); if (!runBeforeUnload) await this._closedPromise; + if (this._isContextOwner) + await this._browserContext.close(); } isClosed(): boolean { diff --git a/src/webkit/Browser.ts b/src/webkit/Browser.ts index 42f6a38321..e5faef7f06 100644 --- a/src/webkit/Browser.ts +++ b/src/webkit/Browser.ts @@ -19,43 +19,37 @@ import * as childProcess from 'child_process'; import { EventEmitter } from 'events'; import { helper, RegisteredListener, debugError } from '../helper'; import * as network from '../network'; +import * as types from '../types'; import { Connection, ConnectionEvents, TargetSession } from './Connection'; import { Page } from '../page'; import { Target } from './Target'; import { Protocol } from './protocol'; -import * as types from '../types'; import { Events } from '../events'; -import { BrowserContext, BrowserInterface } from '../browserContext'; +import { BrowserContext, BrowserInterface, BrowserContextOptions } from '../browserContext'; export class Browser extends EventEmitter implements BrowserInterface { - readonly _defaultViewport: types.Viewport; private readonly _process: childProcess.ChildProcess; readonly _connection: Connection; private _closeCallback: () => Promise; - private readonly _defaultContext: BrowserContext; + private _defaultContext: BrowserContext; private _contexts = new Map(); _targets = new Map(); private _eventListeners: RegisteredListener[]; private _privateEvents = new EventEmitter(); - private readonly _ignoreHTTPSErrors: boolean; constructor( connection: Connection, - ignoreHTTPSErrors: boolean, - defaultViewport: types.Viewport | null, process: childProcess.ChildProcess | null, closeCallback?: (() => Promise)) { super(); this._connection = connection; - this._ignoreHTTPSErrors = ignoreHTTPSErrors; - this._defaultViewport = defaultViewport; this._process = process; this._closeCallback = closeCallback || (() => Promise.resolve()); /** @type {!Map} */ this._targets = new Map(); - this._defaultContext = this._createBrowserContext(undefined); + this._defaultContext = this._createBrowserContext(undefined, {}); /** @type {!Map} */ this._contexts = new Map(); @@ -70,9 +64,6 @@ export class Browser extends EventEmitter implements BrowserInterface { debugError(e); throw e; }); - - if (this._ignoreHTTPSErrors) - this._setIgnoreTLSFailures(undefined); } async userAgent(): Promise { @@ -92,11 +83,11 @@ export class Browser extends EventEmitter implements BrowserInterface { return this._process; } - async newContext(): Promise { - const {browserContextId} = await this._connection.send('Browser.createContext'); - if (this._ignoreHTTPSErrors) - await this._setIgnoreTLSFailures(browserContextId); - const context = this._createBrowserContext(browserContextId); + async newContext(options: BrowserContextOptions = {}): Promise { + const { browserContextId } = await this._connection.send('Browser.createContext'); + const context = this._createBrowserContext(browserContextId, options); + if (options.ignoreHTTPSErrors) + await this._connection.send('Browser.setIgnoreCertificateErrors', { browserContextId, ignore: true }); this._contexts.set(browserContextId, context); return context; } @@ -105,15 +96,13 @@ export class Browser extends EventEmitter implements BrowserInterface { return [this._defaultContext, ...Array.from(this._contexts.values())]; } - defaultBrowserContext(): BrowserContext { + defaultContext(): BrowserContext { return this._defaultContext; } - async _disposeContext(browserContextId: string | null) { - } - - async newPage(): Promise { - return this._defaultContext.newPage(); + async newPage(options?: BrowserContextOptions): Promise { + const context = await this.newContext(options); + return context._createOwnerPage(); } targets(): Target[] { @@ -220,7 +209,7 @@ export class Browser extends EventEmitter implements BrowserInterface { await this._closeCallback.call(null); } - _createBrowserContext(browserContextId: string | undefined): BrowserContext { + _createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext { const isIncognito = !!browserContextId; const context = new BrowserContext({ contextPages: async (): Promise => { @@ -256,13 +245,9 @@ export class Browser extends EventEmitter implements BrowserInterface { const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[]; await this._connection.send('Browser.setCookies', { cookies: cc, browserContextId }); }, - }, this, isIncognito); + }, this, isIncognito, options); return context; } - - async _setIgnoreTLSFailures(browserContextId: string | undefined) { - await this._connection.send('Browser.setIgnoreCertificateErrors', { browserContextId, ignore: true }); - } } const BrowserEvents = { diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index dc07807fe4..bf9208440b 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -86,16 +86,19 @@ export class FrameManager implements PageDelegate { // Dialog agent resides in the UI process and should not be re-enabled on navigation. promises.push(session.send('Dialog.enable')); } - if (this._page._state.userAgent !== null) - promises.push(this._setUserAgent(session, this._page._state.userAgent)); - if (this._page._state.mediaType !== null || this._page._state.mediaColorScheme !== null) - promises.push(this._setEmulateMedia(session, this._page._state.mediaType, this._page._state.mediaColorScheme)); - if (this._page._state.javascriptEnabled !== null) - promises.push(this._setJavaScriptEnabled(session, this._page._state.javascriptEnabled)); - if (this._page._state.bypassCSP !== null) - promises.push(this._setBypassCSP(session, this._page._state.bypassCSP)); + const contextOptions = this._page.browserContext()._options; + if (contextOptions.userAgent) + promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent })); + if (this._page._state.mediaType || this._page._state.colorScheme) + promises.push(this._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme)); + if (contextOptions.javaScriptEnabled === false) + promises.push(session.send('Emulation.setJavaScriptEnabled', { enabled: false })); + if (contextOptions.bypassCSP) + promises.push(session.send('Page.setBypassCSP', { enabled: true })); if (this._page._state.extraHTTPHeaders !== null) promises.push(this._setExtraHTTPHeaders(session, this._page._state.extraHTTPHeaders)); + if (this._page._state.viewport) + promises.push(this.setViewport(this._page._state.viewport)); await Promise.all(promises); } @@ -256,19 +259,7 @@ export class FrameManager implements PageDelegate { await session.send('Network.setExtraHTTPHeaders', { headers }); } - private async _setUserAgent(session: TargetSession, userAgent: string): Promise { - await session.send('Page.overrideUserAgent', { value: userAgent }); - } - - private async _setJavaScriptEnabled(session: TargetSession, enabled: boolean): Promise { - await session.send('Emulation.setJavaScriptEnabled', { enabled }); - } - - private async _setBypassCSP(session: TargetSession, enabled: boolean): Promise { - await session.send('Page.setBypassCSP', { enabled }); - } - - private async _setEmulateMedia(session: TargetSession, mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise { + private async _setEmulateMedia(session: TargetSession, mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise { const promises = []; promises.push(session.send('Page.setEmulatedMedia', { media: mediaType || '' })); if (mediaColorScheme !== null) { @@ -286,23 +277,12 @@ export class FrameManager implements PageDelegate { await this._setExtraHTTPHeaders(this._session, headers); } - async setUserAgent(userAgent: string): Promise { - await this._setUserAgent(this._session, userAgent); - await this._page.reload(); - } - - async setJavaScriptEnabled(enabled: boolean): Promise { - await this._setJavaScriptEnabled(this._session, enabled); - } - - async setBypassCSP(enabled: boolean): Promise { - await this._setBypassCSP(this._session, enabled); - } - - async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.MediaColorScheme | null): Promise { + async setEmulateMedia(mediaType: input.MediaType | null, mediaColorScheme: input.ColorScheme | null): Promise { await this._setEmulateMedia(this._session, mediaType, mediaColorScheme); } + + async setViewport(viewport: types.Viewport): Promise { if (viewport.isMobile || viewport.isLandscape || viewport.hasTouch) throw new Error('Not implemented'); diff --git a/src/webkit/Launcher.ts b/src/webkit/Launcher.ts index 7f520bb6f8..6bb23bd861 100644 --- a/src/webkit/Launcher.ts +++ b/src/webkit/Launcher.ts @@ -19,7 +19,6 @@ import { debugError, assert } from '../helper'; import { Browser } from './Browser'; import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher'; import { Connection } from './Connection'; -import * as types from '../types'; import { PipeTransport } from '../transport'; import { execSync } from 'child_process'; import * as path from 'path'; @@ -58,9 +57,7 @@ export class Launcher { handleSIGINT = true, handleSIGTERM = true, handleSIGHUP = true, - defaultViewport = {width: 800, height: 600}, slowMo = 0, - ignoreHTTPSErrors = false } = options; const webkitArguments = []; @@ -104,7 +101,7 @@ export class Launcher { try { const transport = new PipeTransport(launched.process.stdio[3] as NodeJS.WritableStream, launched.process.stdio[4] as NodeJS.ReadableStream); connection = new Connection(transport, slowMo); - const browser = new Browser(connection, ignoreHTTPSErrors, defaultViewport, launched.process, launched.gracefullyClose); + const browser = new Browser(connection, launched.process, launched.gracefullyClose); await browser._waitForTarget(t => t._type === 'page'); return browser; } catch (e) { @@ -136,9 +133,7 @@ export type LauncherLaunchOptions = { headless?: boolean, dumpio?: boolean, env?: {[key: string]: string} | undefined, - defaultViewport?: types.Viewport | null, slowMo?: number, - ignoreHTTPSErrors?: boolean, }; let cachedMacVersion = undefined; diff --git a/src/webkit/Target.ts b/src/webkit/Target.ts index b174d70f89..2d89cf4a3e 100644 --- a/src/webkit/Target.ts +++ b/src/webkit/Target.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { Browser } from './Browser'; import { BrowserContext } from '../browserContext'; import { Page } from '../page'; import { Protocol } from './protocol'; @@ -86,7 +85,6 @@ export class Target { async page(): Promise { if (this._type === 'page' && !this._pagePromise) { - const browser = this._browserContext.browser() as Browser; this._frameManager = new FrameManager(this._browserContext); // Reference local page variable as |this._frameManager| may be // cleared on swap. @@ -94,8 +92,6 @@ export class Target { this._pagePromise = new Promise(async f => { this._adoptPage(); await this._initializeSession(this._session); - if (browser._defaultViewport) - await page.setViewport(browser._defaultViewport); f(page); }); } diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index e1252202e2..4a37dff644 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -17,7 +17,7 @@ const utils = require('./utils'); -module.exports.addTests = function({testRunner, expect}) { +module.exports.addTests = function({testRunner, expect, playwright, WEBKIT}) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; @@ -29,7 +29,7 @@ module.exports.addTests = function({testRunner, expect}) { expect(defaultContext.isIncognito()).toBe(false); let error = null; await defaultContext.close().catch(e => error = e); - expect(browser.defaultBrowserContext()).toBe(defaultContext); + expect(browser.defaultContext()).toBe(defaultContext); expect(error.message).toContain('cannot be closed'); }); it('should create new incognito context', async function({browser, server}) { @@ -107,5 +107,161 @@ module.exports.addTests = function({testRunner, expect}) { ]); expect(browser.browserContexts().length).toBe(1); }); + it('should set the default viewport', async({ newPage }) => { + const page = await newPage({ viewport: { width: 456, height: 789 } }); + expect(await page.evaluate('window.innerWidth')).toBe(456); + expect(await page.evaluate('window.innerHeight')).toBe(789); + }); + it('should take fullPage screenshots when default viewport is null', async({server, newPage}) => { + const page = await newPage({ viewport: null }); + await page.goto(server.PREFIX + '/grid.html'); + const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); + const screenshot = await page.screenshot({ + fullPage: true + }); + expect(screenshot).toBeInstanceOf(Buffer); + + const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); + expect(sizeBefore.width).toBe(sizeAfter.width); + expect(sizeBefore.height).toBe(sizeAfter.height); + }); + }); + + describe('BrowserContext({setUserAgent})', function() { + it('should work', async({newPage, server}) => { + { + const page = await newPage(); + expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); + } + { + const page = await newPage({ userAgent: 'foobar' }); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + } + }); + it('should work for subframes', async({newPage, server}) => { + { + const page = await newPage(); + expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); + } + { + const page = await newPage({ userAgent: 'foobar' }); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + } + }); + it('should emulate device user-agent', async({newPage, server}) => { + { + const page = await newPage(); + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => navigator.userAgent)).not.toContain('iPhone'); + } + { + const page = await newPage({ userAgent: playwright.devices['iPhone 6'].userAgent }); + await page.goto(server.PREFIX + '/mobile.html'); + expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); + } + }); + }); + + describe('BrowserContext({bypassCSP})', function() { + it('should bypass CSP meta tag', async({newPage, server}) => { + // Make sure CSP prohibits addScriptTag. + { + const page = await newPage(); + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await page.evaluate(() => window.__injected)).toBe(undefined); + } + + // By-pass CSP and try one more time. + { + const page = await newPage({ bypassCSP: true }); + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + } + }); + + it('should bypass CSP header', async({newPage, server}) => { + // Make sure CSP prohibits addScriptTag. + server.setCSP('/empty.html', 'default-src "self"'); + + { + const page = await newPage(); + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await page.evaluate(() => window.__injected)).toBe(undefined); + } + + // By-pass CSP and try one more time. + { + const page = await newPage({ bypassCSP: true }); + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + } + }); + + it('should bypass after cross-process navigation', async({newPage, server}) => { + const page = await newPage({ 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 page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect(await page.evaluate(() => window.__injected)).toBe(42); + }); + it('should bypass CSP in iframes as well', async({newPage, server}) => { + // Make sure CSP prohibits addScriptTag in an iframe. + { + const page = await newPage(); + await page.goto(server.EMPTY_PAGE); + const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); + await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await frame.evaluate(() => window.__injected)).toBe(undefined); + } + + // By-pass CSP and try one more time. + { + const page = await newPage({ bypassCSP: true }); + await page.goto(server.EMPTY_PAGE); + const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); + await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); + expect(await frame.evaluate(() => window.__injected)).toBe(42); + } + }); + }); + + describe('BrowserContext({javaScriptEnabled})', function() { + it('should work', async({newPage}) => { + { + const page = await newPage({ 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'); + } + + { + const page = await newPage(); + await page.goto('data:text/html, '); + expect(await page.evaluate('something')).toBe('forbidden'); + } + }); + it('should be able to navigate after disabling javascript', async({newPage, server}) => { + const page = await newPage({ javaScriptEnabled: false }); + await page.goto(server.EMPTY_PAGE); + }); }); }; diff --git a/test/chromium/connect.spec.js b/test/chromium/connect.spec.js index 97f831e159..f904c82909 100644 --- a/test/chromium/connect.spec.js +++ b/test/chromium/connect.spec.js @@ -55,16 +55,17 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p const originalBrowser = await playwright.launch(defaultBrowserOptions); const browserWSEndpoint = originalBrowser.chromium.wsEndpoint(); - const browser = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint, ignoreHTTPSErrors: true}); - const page = await browser.newPage(); + const browser = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint}); + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + const page = await context.newPage(); let error = null; - const [serverRequest, response] = await Promise.all([ + const [, response] = await Promise.all([ httpsServer.waitForRequest('/empty.html'), page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e) ]); expect(error).toBe(null); expect(response.ok()).toBe(true); - await page.close(); + await context.close(); await browser.close(); }); it('should be able to reconnect to a disconnected browser', async({server}) => { diff --git a/test/chromium/geolocation.spec.js b/test/chromium/geolocation.spec.js index 242e691640..21ceb65421 100644 --- a/test/chromium/geolocation.spec.js +++ b/test/chromium/geolocation.spec.js @@ -26,7 +26,7 @@ module.exports.addTests = function ({ testRunner, expect }) { it('should work', async({page, server, context}) => { await context.permissions.override(server.PREFIX, ['geolocation']); await page.goto(server.EMPTY_PAGE); - await page.overrides.setGeolocation({longitude: 10, latitude: 10}); + await context.overrides.setGeolocation({longitude: 10, latitude: 10}); const geolocation = await page.evaluate(() => new Promise(resolve => navigator.geolocation.getCurrentPosition(position => { resolve({latitude: position.coords.latitude, longitude: position.coords.longitude}); }))); @@ -35,10 +35,10 @@ module.exports.addTests = function ({ testRunner, expect }) { longitude: 10 }); }); - it('should throw when invalid longitude', async({page, server, context}) => { + it('should throw when invalid longitude', async({context}) => { let error = null; try { - await page.overrides.setGeolocation({longitude: 200, latitude: 10}); + await context.overrides.setGeolocation({longitude: 200, latitude: 10}); } catch (e) { error = e; } diff --git a/test/chromium/headful.spec.js b/test/chromium/headful.spec.js index 6027adc2fb..5da9628887 100644 --- a/test/chromium/headful.spec.js +++ b/test/chromium/headful.spec.js @@ -71,13 +71,13 @@ module.exports.addTests = function({testRunner, expect, playwright, defaultBrows const userDataDir = await mkdtempAsync(TMP_FOLDER); // Write a cookie in headful chrome const headfulBrowser = await playwright.launch(Object.assign({userDataDir}, headfulOptions)); - const headfulPage = await headfulBrowser.newPage(); + const headfulPage = await headfulBrowser.defaultContext().newPage(); await headfulPage.goto(server.EMPTY_PAGE); await headfulPage.evaluate(() => document.cookie = 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'); await headfulBrowser.close(); // Read the cookie from headless chrome const headlessBrowser = await playwright.launch(Object.assign({userDataDir}, headlessOptions)); - const headlessPage = await headlessBrowser.newPage(); + const headlessPage = await headlessBrowser.defaultContext().newPage(); await headlessPage.goto(server.EMPTY_PAGE); const cookie = await headlessPage.evaluate(() => document.cookie); await headlessBrowser.close(); diff --git a/test/chromium/launcher.spec.js b/test/chromium/launcher.spec.js index 8decce045c..bc4d70ce66 100644 --- a/test/chromium/launcher.spec.js +++ b/test/chromium/launcher.spec.js @@ -124,13 +124,13 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p const userDataDir = await mkdtempAsync(TMP_FOLDER); const options = Object.assign({userDataDir}, defaultBrowserOptions); const browser = await playwright.launch(options); - const page = await browser.newPage(); + const page = await browser.defaultContext().newPage(); await page.goto(server.EMPTY_PAGE); await page.evaluate(() => localStorage.hey = 'hello'); await browser.close(); const browser2 = await playwright.launch(options); - const page2 = await browser2.newPage(); + const page2 = await browser2.defaultContext().newPage(); await page2.goto(server.EMPTY_PAGE); expect(await page2.evaluate(() => localStorage.hey)).toBe('hello'); await browser2.close(); @@ -142,13 +142,13 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p const userDataDir = await mkdtempAsync(TMP_FOLDER); const options = Object.assign({userDataDir}, defaultBrowserOptions); const browser = await playwright.launch(options); - const page = await browser.newPage(); + const page = await browser.defaultContext().newPage(); await page.goto(server.EMPTY_PAGE); await page.evaluate(() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'); await browser.close(); const browser2 = await playwright.launch(options); - const page2 = await browser2.newPage(); + const page2 = await browser2.defaultContext().newPage(); await page2.goto(server.EMPTY_PAGE); expect(await page2.evaluate(() => document.cookie)).toBe('doSomethingOnlyOnce=true'); await browser2.close(); @@ -161,7 +161,7 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p it('should support the pipe option', async() => { const options = Object.assign({pipe: true}, defaultBrowserOptions); const browser = await playwright.launch(options); - expect((await browser.pages()).length).toBe(1); + expect((await browser.defaultContext().pages()).length).toBe(1); expect(browser.chromium.wsEndpoint()).toBe(''); const page = await browser.newPage(); expect(await page.evaluate('11 * 11')).toBe(121); diff --git a/test/click.spec.js b/test/click.spec.js index ec9803ac90..add72cea4f 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -70,8 +70,8 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME await page.click('button'); expect(await page.evaluate(() => result)).toBe('Clicked'); }); - it.skip(FFOX)('should click with disabled javascript', async({page, server}) => { - await page.setJavaScriptEnabled(false); + it.skip(FFOX)('should click with disabled javascript', async({newPage, server}) => { + const page = await newPage({ javaScriptEnabled: false }); await page.goto(server.PREFIX + '/wrappedlink.html'); await Promise.all([ page.click('a'), diff --git a/test/defaultbrowsercontext.spec.js b/test/defaultbrowsercontext.spec.js index fd3e178c70..615632200b 100644 --- a/test/defaultbrowsercontext.spec.js +++ b/test/defaultbrowsercontext.spec.js @@ -20,7 +20,7 @@ module.exports.addTests = function ({ testRunner, expect, defaultBrowserOptions, const {it, fit, xit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; - describe('DefaultBrowserContext', function() { + describe('defaultContext()', function() { beforeEach(async state => { state.browser = await playwright.launch(defaultBrowserOptions); state.page = await state.browser.newPage(); diff --git a/test/emulation.spec.js b/test/emulation.spec.js index 8164f94374..02c192cd46 100644 --- a/test/emulation.spec.js +++ b/test/emulation.spec.js @@ -81,14 +81,14 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME }); describe('Page.emulate', function() { - it.skip(WEBKIT)('should work', async({page, server}) => { + it.skip(WEBKIT)('should work', async({newPage, server}) => { + const page = await newPage({ viewport: iPhone.viewport, userAgent: iPhone.userAgent }); await page.goto(server.PREFIX + '/mobile.html'); - await page.emulate(iPhone); expect(await page.evaluate(() => window.innerWidth)).toBe(375); expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); }); - it.skip(WEBKIT)('should support clicking', async({page, server}) => { - await page.emulate(iPhone); + it.skip(WEBKIT)('should support clicking', async({newPage, server}) => { + const page = await newPage({ viewport: iPhone.viewport, userAgent: iPhone.userAgent }); await page.goto(server.PREFIX + '/input/button.html'); const button = await page.$('button'); await page.evaluate(button => button.style.marginTop = '200px', button); @@ -143,29 +143,32 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME }); }); - describe.skip(FFOX || WEBKIT)('Overrides.setTimezone', function() { - it('should work', async({page, server}) => { - page.evaluate(() => { - globalThis.date = new Date(1479579154987); - }); - await page.overrides.setTimezone('America/Jamaica'); - expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); - - await page.overrides.setTimezone('Pacific/Honolulu'); - expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'); - - await page.overrides.setTimezone('America/Buenos_Aires'); - expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); - - await page.overrides.setTimezone('Europe/Berlin'); - expect(await page.evaluate(() => date.toString())).toBe('Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'); + describe.skip(FFOX || WEBKIT)('BrowserContext({timezoneId})', function() { + it('should work', async ({ newPage }) => { + const func = () => new Date(1479579154987).toString(); + { + const page = await newPage({ timezoneId: 'America/Jamaica' }); + expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); + } + { + const page = await newPage({ timezoneId: 'Pacific/Honolulu' }); + expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'); + } + { + const page = await newPage({ timezoneId: 'America/Buenos_Aires' }); + expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); + } + { + const page = await newPage({ timezoneId: 'Europe/Berlin' }); + expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'); + } }); - it('should throw for invalid timezone IDs', async({page, server}) => { + it('should throw for invalid timezone IDs', async({newPage}) => { let error = null; - await page.overrides.setTimezone('Foo/Bar').catch(e => error = e); + await newPage({ timezoneId: 'Foo/Bar' }).catch(e => error = e); expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); - await page.overrides.setTimezone('Baz/Qux').catch(e => error = e); + await newPage({ timezoneId: 'Baz/Qux' }).catch(e => error = e); expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); }); }); diff --git a/test/features/interception.spec.js b/test/features/interception.spec.js index d0facb2f00..e4306f49a9 100644 --- a/test/features/interception.spec.js +++ b/test/features/interception.spec.js @@ -663,17 +663,13 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p }); describe('ignoreHTTPSErrors', function() { - it('should work with request interception', async({httpsServer}) => { - const browser = await playwright.launch({...defaultBrowserOptions, ignoreHTTPSErrors: true}); - const context = await browser.newContext(); - const page = await context.newPage(); + it('should work with request interception', async({newPage, httpsServer}) => { + const page = await newPage({ ignoreHTTPSErrors: true }); await page.interception.enable(); page.on('request', request => page.interception.continue(request)); const response = await page.goto(httpsServer.EMPTY_PAGE); expect(response.status()).toBe(200); - - await browser.close(); }); }); }; diff --git a/test/fixtures/dumpio.js b/test/fixtures/dumpio.js index b20fd6b8ff..47b70097de 100644 --- a/test/fixtures/dumpio.js +++ b/test/fixtures/dumpio.js @@ -1,7 +1,7 @@ (async() => { const [, , playwrightRoot, options] = process.argv; const browser = await require(playwrightRoot).launch(JSON.parse(options)); - const page = await browser.newPage(); + const page = await browser.defaultContext().newPage(); await page.evaluate(() => console.error('message from dumpio')); await page.close(); await browser.close(); diff --git a/test/ignorehttpserrors.spec.js b/test/ignorehttpserrors.spec.js index 6cfbdda16f..0cb554e5b3 100644 --- a/test/ignorehttpserrors.spec.js +++ b/test/ignorehttpserrors.spec.js @@ -20,33 +20,18 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p const {it, fit, xit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; describe('ignoreHTTPSErrors', function() { - beforeAll(async state => { - state.browser = await playwright.launch({...defaultBrowserOptions, ignoreHTTPSErrors: true}); - }); - afterAll(async state => { - await state.browser.close(); - delete state.browser; - }); - beforeEach(async state => { - state.context = await state.browser.newContext(); - state.page = await state.context.newPage(); - }); - afterEach(async state => { - await state.context.close(); - delete state.context; - delete state.page; - }); - - it('should work', async({page, httpsServer}) => { + it('should work', async({newPage, httpsServer}) => { let error = null; + const page = await newPage({ ignoreHTTPSErrors: true }); const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); expect(error).toBe(null); expect(response.ok()).toBe(true); }); - it('should work with mixed content', async({page, server, httpsServer}) => { + it('should work with mixed content', async({newPage, server, httpsServer}) => { httpsServer.setRoute('/mixedcontent.html', (req, res) => { res.end(``); }); + const page = await newPage({ ignoreHTTPSErrors: true }); await page.goto(httpsServer.PREFIX + '/mixedcontent.html', {waitUntil: 'load'}); expect(page.frames().length).toBe(2); // Make sure blocked iframe has functional execution context diff --git a/test/launcher.spec.js b/test/launcher.spec.js index f74cd35467..bc8f54b6f2 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -45,46 +45,6 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p await playwright.launch(options).catch(e => waitError = e); expect(waitError.message).toContain('Failed to launch'); }); - it('should set the default viewport', async() => { - const options = Object.assign({}, defaultBrowserOptions, { - defaultViewport: { - width: 456, - height: 789 - } - }); - const browser = await playwright.launch(options); - const page = await browser.newPage(); - expect(await page.evaluate('window.innerWidth')).toBe(456); - expect(await page.evaluate('window.innerHeight')).toBe(789); - await browser.close(); - }); - it('should disable the default viewport', async() => { - const options = Object.assign({}, defaultBrowserOptions, { - defaultViewport: null - }); - const browser = await playwright.launch(options); - const page = await browser.newPage(); - expect(page.viewport()).toBe(null); - await browser.close(); - }); - it('should take fullPage screenshots when defaultViewport is null', async({server}) => { - const options = Object.assign({}, defaultBrowserOptions, { - defaultViewport: null - }); - const browser = await playwright.launch(options); - const page = await browser.newPage(); - await page.goto(server.PREFIX + '/grid.html'); - const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); - const screenshot = await page.screenshot({ - fullPage: true - }); - expect(screenshot).toBeInstanceOf(Buffer); - - const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); - expect(sizeBefore.width).toBe(sizeAfter.width); - expect(sizeBefore.height).toBe(sizeAfter.height); - await browser.close(); - }); it('should have default URL when launching browser', async function() { const browser = await playwright.launch(defaultBrowserOptions); const pages = (await browser.pages()).map(page => page.url()); diff --git a/test/navigation.spec.js b/test/navigation.spec.js index fe7e685be4..1e5d480ce0 100644 --- a/test/navigation.spec.js +++ b/test/navigation.spec.js @@ -125,7 +125,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'}); expect(response.status()).toBe(200); }); - it('should work when page calls history API in beforeunload', async({page, server}) => { + it.skip(WEBKIT)('should work when page calls history API in beforeunload', async({page, server}) => { await page.goto(server.EMPTY_PAGE); await page.evaluate(() => { window.addEventListener('beforeunload', () => history.replaceState(null, 'initial', window.location.href), false); diff --git a/test/page.spec.js b/test/page.spec.js index 4b48e1cc23..7ea7155a70 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -166,7 +166,7 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF expect(await page.evaluate(() => !!window.opener)).toBe(false); expect(await popup.evaluate(() => !!window.opener)).toBe(false); }); - it('should not treat navigations as new popups', async({page, server}) => { + it.skip(WEBKIT)('should not treat navigations as new popups', async({page, server}) => { await page.goto(server.EMPTY_PAGE); await page.setContent('yo'); const [popup] = await Promise.all([ @@ -585,33 +585,6 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF }); }); - describe('Page.setUserAgent', function() { - it('should work', async({page, server}) => { - expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); - await page.setUserAgent('foobar'); - const [request] = await Promise.all([ - server.waitForRequest('/empty.html'), - page.goto(server.EMPTY_PAGE), - ]); - expect(request.headers['user-agent']).toBe('foobar'); - }); - it('should work for subframes', async({page, server}) => { - expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); - await page.setUserAgent('foobar'); - const [request] = await Promise.all([ - server.waitForRequest('/empty.html'), - utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), - ]); - expect(request.headers['user-agent']).toBe('foobar'); - }); - it('should emulate device user-agent', async({page, server}) => { - await page.goto(server.PREFIX + '/mobile.html'); - expect(await page.evaluate(() => navigator.userAgent)).not.toContain('iPhone'); - await page.setUserAgent(playwright.devices['iPhone 6'].userAgent); - expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); - }); - }); - describe('Page.setContent', function() { const expectedOutput = '
hello
'; it('should work', async({page, server}) => { @@ -705,64 +678,6 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF }); }); - describe('Page.setBypassCSP', function() { - it('should bypass CSP meta tag', async({page, server}) => { - // Make sure CSP prohibits addScriptTag. - await page.goto(server.PREFIX + '/csp.html'); - await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); - expect(await page.evaluate(() => window.__injected)).toBe(undefined); - - // By-pass CSP and try one more time. - await page.setBypassCSP(true); - await page.reload(); - await page.addScriptTag({content: 'window.__injected = 42;'}); - expect(await page.evaluate(() => window.__injected)).toBe(42); - }); - - it('should bypass CSP header', async({page, server}) => { - // Make sure CSP prohibits addScriptTag. - server.setCSP('/empty.html', 'default-src "self"'); - await page.goto(server.EMPTY_PAGE); - await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); - expect(await page.evaluate(() => window.__injected)).toBe(undefined); - - // By-pass CSP and try one more time. - await page.setBypassCSP(true); - await page.reload(); - await page.addScriptTag({content: 'window.__injected = 42;'}); - expect(await page.evaluate(() => window.__injected)).toBe(42); - }); - - it('should bypass after cross-process navigation', async({page, server}) => { - await page.setBypassCSP(true); - await page.goto(server.PREFIX + '/csp.html'); - await page.addScriptTag({content: 'window.__injected = 42;'}); - expect(await page.evaluate(() => window.__injected)).toBe(42); - - await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); - await page.addScriptTag({content: 'window.__injected = 42;'}); - expect(await page.evaluate(() => window.__injected)).toBe(42); - }); - it('should bypass CSP in iframes as well', async({page, server}) => { - await page.goto(server.EMPTY_PAGE); - { - // Make sure CSP prohibits addScriptTag in an iframe. - const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); - await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); - expect(await frame.evaluate(() => window.__injected)).toBe(undefined); - } - - // By-pass CSP and try one more time. - await page.setBypassCSP(true); - await page.reload(); - - { - const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); - await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); - expect(await frame.evaluate(() => window.__injected)).toBe(42); - } - }); - }); describe('Page.addScriptTag', function() { it('should throw an error if no options are provided', async({page, server}) => { @@ -924,27 +839,6 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF }); }); - describe('Page.setJavaScriptEnabled', function() { - it('should work', async({page, server}) => { - await page.setJavaScriptEnabled(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 page.setJavaScriptEnabled(true); - await page.goto('data:text/html, '); - expect(await page.evaluate('something')).toBe('forbidden'); - }); - it('should be able to navigate after disabling javascript', async({page, server}) => { - await page.setJavaScriptEnabled(false); - await page.goto(server.EMPTY_PAGE); - }); - }); - describe('Page.setCacheEnabled', function() { // FIXME: 'if-modified-since' is not set for some reason even if cache is on. it.skip(WEBKIT)('should enable or disable the cache based on the state passed', async({page, server}) => { diff --git a/test/playwright.spec.js b/test/playwright.spec.js index e375f2a742..22ee27383d 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -98,36 +98,51 @@ module.exports.addTests = ({testRunner, product, playwrightPath}) => { state.browser = null; }); - if (!WEBKIT) { - beforeEach(async(state, test) => { - const rl = require('readline').createInterface({input: state.browser.process().stderr}); + beforeEach(async(state, test) => { + const pages = []; + const contexts = []; + const onLine = (line) => test.output += line + '\n'; + + let rl; + if (!WEBKIT) { + rl = require('readline').createInterface({ input: state.browser.process().stderr }); test.output = ''; rl.on('line', onLine); - state.tearDown = () => { + } + + state.tearDown = async () => { + await Promise.all(pages.map(p => p.close())); + await Promise.all(contexts.map(c => c.close())); + if (!WEBKIT) { rl.removeListener('line', onLine); rl.close(); - }; - function onLine(line) { - test.output += line + '\n'; } - }); - } + }; - if (!WEBKIT) { - afterEach(async state => { - state.tearDown(); - }); - } + state.newContext = async (options) => { + const context = await state.browser.newContext(options); + contexts.push(context); + return context; + }; + + state.newPage = async (options) => { + const page = await state.browser.newPage(options); + pages.push(page); + return page; + }; + }); + + afterEach(async state => { + await state.tearDown(); + }); describe('Page', function() { beforeEach(async state => { - state.context = await state.browser.newContext(); + state.context = await state.newContext(); state.page = await state.context.newPage(); }); afterEach(async state => { - // This closes all pages. - await state.context.close(); state.context = null; state.page = null; }); @@ -172,12 +187,12 @@ module.exports.addTests = ({testRunner, product, playwrightPath}) => { // Browser-level tests that are given a browser. require('./browsercontext.spec.js').addTests(testOptions); + require('./ignorehttpserrors.spec.js').addTests(testOptions); }); // Top-level tests that launch Browser themselves. require('./defaultbrowsercontext.spec.js').addTests(testOptions); require('./fixtures.spec.js').addTests(testOptions); - require('./ignorehttpserrors.spec.js').addTests(testOptions); require('./launcher.spec.js').addTests(testOptions); if (CHROME) {