diff --git a/docs/api.md b/docs/api.md index 2e1255a95ac6c5..c1098d5fd874a1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -208,7 +208,7 @@ Indicates that the browser is connected. - `longitude` <[number]> Longitude between -180 and 180. - `accuracy` <[number]> Optional non-negative accuracy value. - `locale` Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules. - - `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) for more details. + - `permissions` <[Array]<[string]>> A list of permissions to grant to all pages in this context. See [browserContext.grantPermissions](#browsercontextgrantpermissionspermissions-options) for more details. - `extraHTTPHeaders` <[Object]> An object containing additional HTTP headers to be sent with every request. All header values must be strings. - `offline` <[boolean]> Whether to emulate network being offline. Defaults to `false`. - `httpCredentials` <[Object]> Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). @@ -246,7 +246,7 @@ Creates a new browser context. It won't share cookies/cache with other browser c - `longitude` <[number]> Longitude between -180 and 180. - `accuracy` <[number]> Optional non-negative accuracy value. - `locale` Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules. - - `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) for more details. + - `permissions` <[Array]<[string]>> A list of permissions to grant to all pages in this context. See [browserContext.grantPermissions](#browsercontextgrantpermissionspermissions-options) for more details. - `extraHTTPHeaders` <[Object]> An object containing additional HTTP headers to be sent with every request. All header values must be strings. - `offline` <[boolean]> Whether to emulate network being offline. Defaults to `false`. - `httpCredentials` <[Object]> Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). @@ -290,6 +290,7 @@ await context.close(); - [browserContext.close()](#browsercontextclose) - [browserContext.cookies([urls])](#browsercontextcookiesurls) - [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction) +- [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options) - [browserContext.newPage()](#browsercontextnewpage) - [browserContext.pages()](#browsercontextpages) - [browserContext.route(url, handler)](#browsercontextrouteurl-handler) @@ -299,7 +300,6 @@ await context.close(); - [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation) - [browserContext.setHTTPCredentials(httpCredentials)](#browsercontextsethttpcredentialshttpcredentials) - [browserContext.setOffline(offline)](#browsercontextsetofflineoffline) -- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) - [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate) @@ -381,7 +381,7 @@ Clears all permission overrides for the browser context. ```js const context = await browser.newContext(); -context.setPermissions('https://example.com', ['clipboard-read']); +await context.grantPermissions(['clipboard-read']); // do stuff .. context.clearPermissions(); ``` @@ -447,6 +447,31 @@ const crypto = require('crypto'); })(); ``` +#### browserContext.grantPermissions(permissions[][, options]) +- `permissions` <[Array]<[string]>> A permission or an array of permissions to grant. Permissions can be one of the following values: + - `'*'` + - `'geolocation'` + - `'midi'` + - `'midi-sysex'` (system-exclusive midi) + - `'notifications'` + - `'push'` + - `'camera'` + - `'microphone'` + - `'background-sync'` + - `'ambient-light-sensor'` + - `'accelerometer'` + - `'gyroscope'` + - `'magnetometer'` + - `'accessibility-events'` + - `'clipboard-read'` + - `'clipboard-write'` + - `'payment-handler'` +- `options` <[Object]> + - `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". +- returns: <[Promise]> + +Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if specified. + #### browserContext.newPage() - returns: <[Promise]<[Page]>> @@ -544,27 +569,6 @@ To disable authentication, pass `null`. - `offline` <[boolean]> Whether to emulate network being offline for the browser context. - returns: <[Promise]> -#### browserContext.setPermissions(origin, permissions[]) -- `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". -- `permissions` <[Array]<[string]>> An array of permissions to grant. All permissions that are not listed here will be automatically denied. Permissions can be one of the following values: - - `'geolocation'` - - `'midi'` - - `'midi-sysex'` (system-exclusive midi) - - `'notifications'` - - `'push'` - - `'camera'` - - `'microphone'` - - `'background-sync'` - - `'ambient-light-sensor'` - - `'accelerometer'` - - `'gyroscope'` - - `'magnetometer'` - - `'accessibility-events'` - - `'clipboard-read'` - - `'clipboard-write'` - - `'payment-handler'` -- returns: <[Promise]> - #### browserContext.waitForEvent(event[, optionsOrPredicate]) - `event` <[string]> Event name, same one would pass into `browserContext.on(event)`. - `optionsOrPredicate` <[Function]|[Object]> Either a predicate that receives an event or an options object. @@ -577,7 +581,7 @@ is fired. ```js const context = await browser.newContext(); -await context.setPermissions('https://html5demos.com', ['geolocation']); +await context.grantPermissions(['geolocation']); ``` ### class: Page @@ -3784,6 +3788,7 @@ const backgroundPage = await backroundPageTarget.page(); - [browserContext.close()](#browsercontextclose) - [browserContext.cookies([urls])](#browsercontextcookiesurls) - [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction) +- [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options) - [browserContext.newPage()](#browsercontextnewpage) - [browserContext.pages()](#browsercontextpages) - [browserContext.route(url, handler)](#browsercontextrouteurl-handler) @@ -3793,7 +3798,6 @@ const backgroundPage = await backroundPageTarget.page(); - [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation) - [browserContext.setHTTPCredentials(httpCredentials)](#browsercontextsethttpcredentialshttpcredentials) - [browserContext.setOffline(offline)](#browsercontextsetofflineoffline) -- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) - [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate) diff --git a/package.json b/package.json index 8291d918e22e46..07139f9fcec9c8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "main": "index.js", "playwright": { "chromium_revision": "750417", - "firefox_revision": "1042", + "firefox_revision": "1043", "webkit_revision": "1179" }, "scripts": { diff --git a/src/browserContext.ts b/src/browserContext.ts index 5fd6db1367bf19..9b8653a70bd641 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -32,7 +32,7 @@ export type BrowserContextOptions = { locale?: string, timezoneId?: string, geolocation?: types.Geolocation, - permissions?: { [key: string]: string[] }, + permissions?: string[], extraHTTPHeaders?: network.Headers, offline?: boolean, httpCredentials?: types.Credentials, @@ -46,7 +46,7 @@ export interface BrowserContext { cookies(urls?: string | string[]): Promise; addCookies(cookies: network.SetNetworkCookieParam[]): Promise; clearCookies(): Promise; - setPermissions(origin: string, permissions: string[]): Promise; + grantPermissions(permissions: string[], options?: { origin?: string }): Promise; clearPermissions(): Promise; setGeolocation(geolocation: types.Geolocation | null): Promise; setExtraHTTPHeaders(headers: network.Headers): Promise; @@ -67,6 +67,7 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement _closed = false; private readonly _closePromise: Promise; private _closePromiseFulfill: ((error: Error) => void) | undefined; + private _permissions = new Map(); constructor(options: BrowserContextOptions) { super(); @@ -92,8 +93,8 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement abstract cookies(...urls: string[]): Promise; abstract addCookies(cookies: network.SetNetworkCookieParam[]): Promise; abstract clearCookies(): Promise; - abstract setPermissions(origin: string, permissions: string[]): Promise; - abstract clearPermissions(): Promise; + abstract _doGrantPermissions(origin: string, permissions: string[]): Promise; + abstract _doClearPermissions(): Promise; abstract setGeolocation(geolocation: types.Geolocation | null): Promise; abstract setHTTPCredentials(httpCredentials: types.Credentials | null): Promise; abstract setExtraHTTPHeaders(headers: network.Headers): Promise; @@ -103,6 +104,24 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement abstract route(url: types.URLMatch, handler: network.RouteHandler): Promise; abstract close(): Promise; + async grantPermissions(permissions: string[], options?: { origin?: string }) { + let origin = '*'; + if (options && options.origin) { + const url = new URL(options.origin); + origin = url.origin; + } + const existing = new Set(this._permissions.get(origin) || []); + permissions.forEach(p => existing.add(p)); + const list = [...existing.values()]; + this._permissions.set(origin, list); + await this._doGrantPermissions(origin, list); + } + + async clearPermissions() { + this._permissions.clear(); + await this._doClearPermissions(); + } + setDefaultNavigationTimeout(timeout: number) { this._timeoutSettings.setDefaultNavigationTimeout(timeout); } diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index d764d705083f0c..cb0759abe6448f 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -252,8 +252,8 @@ export class CRBrowserContext extends BrowserContextBase { } async _initialize() { - const entries = Object.entries(this._options.permissions || {}); - await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.permissions) + await this.grantPermissions(this._options.permissions); if (this._options.geolocation) await this.setGeolocation(this._options.geolocation); if (this._options.offline) @@ -302,7 +302,7 @@ export class CRBrowserContext extends BrowserContextBase { await this._browser._session.send('Storage.clearCookies', { browserContextId: this._browserContextId || undefined }); } - async setPermissions(origin: string, permissions: string[]): Promise { + async _doGrantPermissions(origin: string, permissions: string[]) { const webPermissionToProtocol = new Map([ ['geolocation', 'geolocation'], ['midi', 'midi'], @@ -327,10 +327,10 @@ export class CRBrowserContext extends BrowserContextBase { throw new Error('Unknown permission: ' + permission); return protocolPermission; }); - await this._browser._session.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId || undefined, permissions: filtered }); + await this._browser._session.send('Browser.grantPermissions', { origin: origin === '*' ? undefined : origin, browserContextId: this._browserContextId || undefined, permissions: filtered }); } - async clearPermissions() { + async _doClearPermissions() { await this._browser._session.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined }); } diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index f51563cc0b16a5..56ec5a0e1dbd8f 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -166,8 +166,8 @@ export class FFBrowserContext extends BrowserContextBase { } async _initialize() { - const entries = Object.entries(this._options.permissions || {}); - await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.permissions) + await this.grantPermissions(this._options.permissions); if (this._options.geolocation) await this.setGeolocation(this._options.geolocation); if (this._options.extraHTTPHeaders) @@ -227,12 +227,12 @@ export class FFBrowserContext extends BrowserContextBase { await this._browser._connection.send('Browser.clearCookies', { browserContextId: this._browserContextId || undefined }); } - async setPermissions(origin: string, permissions: string[]): Promise { - const webPermissionToProtocol = new Map([ + async _doGrantPermissions(origin: string, permissions: string[]) { + const webPermissionToProtocol = new Map([ ['geolocation', 'geo'], - ['microphone', 'microphone'], - ['camera', 'camera'], - ['notifications', 'desktop-notifications'], + ['persistent-storage', 'persistent-storage'], + ['push', 'push'], + ['notifications', 'desktop-notification'], ]); const filtered = permissions.map(permission => { const protocolPermission = webPermissionToProtocol.get(permission); @@ -240,10 +240,10 @@ export class FFBrowserContext extends BrowserContextBase { throw new Error('Unknown permission: ' + permission); return protocolPermission; }); - await this._browser._connection.send('Browser.grantPermissions', {origin, browserContextId: this._browserContextId || undefined, permissions: filtered}); + await this._browser._connection.send('Browser.grantPermissions', { origin: origin, browserContextId: this._browserContextId || undefined, permissions: filtered}); } - async clearPermissions() { + async _doClearPermissions() { await this._browser._connection.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined }); } diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 59a700a4cafc87..2f9f05750015d0 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -190,8 +190,8 @@ export class WKBrowserContext extends BrowserContextBase { await this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId: this._browserContextId, ignore: true }); if (this._options.locale) await this._browser._browserSession.send('Playwright.setLanguages', { browserContextId: this._browserContextId, languages: [this._options.locale] }); - const entries = Object.entries(this._options.permissions || {}); - await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.permissions) + await this.grantPermissions(this._options.permissions); if (this._options.geolocation) await this.setGeolocation(this._options.geolocation); if (this._options.offline) @@ -240,11 +240,11 @@ export class WKBrowserContext extends BrowserContextBase { await this._browser._browserSession.send('Playwright.setCookies', { cookies: cc, browserContextId: this._browserContextId }); } - async clearCookies() { + async _doClearPermissions() { await this._browser._browserSession.send('Playwright.deleteAllCookies', { browserContextId: this._browserContextId }); } - async setPermissions(origin: string, permissions: string[]): Promise { + async _doGrantPermissions(origin: string, permissions: string[]) { const webPermissionToProtocol = new Map([ ['geolocation', 'geolocation'], ]); @@ -258,6 +258,7 @@ export class WKBrowserContext extends BrowserContextBase { } async clearPermissions() { + super.clearPermissions(); await this._browser._browserSession.send('Playwright.resetPermissions', { browserContextId: this._browserContextId }); } diff --git a/test/geolocation.spec.js b/test/geolocation.spec.js index 337cc51b9419e7..216231fc434d37 100644 --- a/test/geolocation.spec.js +++ b/test/geolocation.spec.js @@ -25,7 +25,7 @@ module.exports.describe = function ({ testRunner, expect, FFOX, WEBKIT }) { describe.fail(FFOX)('Overrides.setGeolocation', function() { it('should work', async({page, server, context}) => { - await context.setPermissions(server.PREFIX, ['geolocation']); + await context.grantPermissions(['geolocation']); await page.goto(server.EMPTY_PAGE); await context.setGeolocation({longitude: 10, latitude: 10}); const geolocation = await page.evaluate(() => new Promise(resolve => navigator.geolocation.getCurrentPosition(position => { diff --git a/test/permissions.spec.js b/test/permissions.spec.js index af38b1c09f96cb..e1576c789d5041 100644 --- a/test/permissions.spec.js +++ b/test/permissions.spec.js @@ -21,7 +21,7 @@ const path = require('path'); /** * @type {PageTestSuite} */ -module.exports.describe = function({testRunner, expect, WEBKIT}) { +module.exports.describe = function({testRunner, expect, WEBKIT, FFOX}) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit, dit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; @@ -38,23 +38,55 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) { }); it('should deny permission when not listed', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); - await context.setPermissions(server.EMPTY_PAGE, []); + await context.grantPermissions([], { origin: server.EMPTY_PAGE }); expect(await getPermission(page, 'geolocation')).toBe('denied'); }); it('should fail when bad permission is given', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); let error = {}; - await context.setPermissions(server.EMPTY_PAGE, ['foo']).catch(e => error = e); + await context.grantPermissions(['foo'], { origin: server.EMPTY_PAGE }).catch(e => error = e); expect(error.message).toBe('Unknown permission: foo'); }); - it('should grant permission when listed', async({page, server, context}) => { + it('should grant geolocation permission when listed', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); - await context.setPermissions(server.EMPTY_PAGE, ['geolocation']); + await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await getPermission(page, 'geolocation')).toBe('granted'); }); + it.fail(FFOX)('should grant notifications permission when listed', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['notifications'], { origin: server.EMPTY_PAGE }); + expect(await getPermission(page, 'notifications')).toBe('granted'); + }); + it.fail(FFOX)('should accumulate when adding', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['geolocation']); + await context.grantPermissions(['notifications']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + expect(await getPermission(page, 'notifications')).toBe('granted'); + }); + it.fail(FFOX)('should clear permissions', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['geolocation']); + await context.clearPermissions(); + await context.grantPermissions(['notifications']); + expect(await getPermission(page, 'geolocation')).not.toBe('granted'); + expect(await getPermission(page, 'notifications')).toBe('granted'); + }); + it('should grant permission when listed for all domains', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + }); + it('should grant permission when creating context', async({server, browser}) => { + const context = await browser.newContext({ permissions: ['geolocation'] }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + await context.close(); + }); it('should reset permissions', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); - await context.setPermissions(server.EMPTY_PAGE, ['geolocation']); + await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await getPermission(page, 'geolocation')).toBe('granted'); await context.clearPermissions(); expect(await getPermission(page, 'geolocation')).toBe('prompt'); @@ -71,9 +103,9 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) { }); }); expect(await page.evaluate(() => window['events'])).toEqual(['prompt']); - await context.setPermissions(server.EMPTY_PAGE, []); + await context.grantPermissions([], { origin: server.EMPTY_PAGE }); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied']); - await context.setPermissions(server.EMPTY_PAGE, ['geolocation']); + await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted']); await context.clearPermissions(); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted', 'prompt']); @@ -86,8 +118,8 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) { expect(await getPermission(page, 'geolocation')).toBe('prompt'); expect(await getPermission(otherPage, 'geolocation')).toBe('prompt'); - await context.setPermissions(server.EMPTY_PAGE, []); - await otherContext.setPermissions(server.EMPTY_PAGE, ['geolocation']); + await context.grantPermissions([], { origin: server.EMPTY_PAGE }); + await otherContext.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await getPermission(page, 'geolocation')).toBe('denied'); expect(await getPermission(otherPage, 'geolocation')).toBe('granted');