Skip to content

Commit

Permalink
feat(browsercontext): implement BrowserContext.overridePermissions (#…
Browse files Browse the repository at this point in the history
…3159)

Introduce an API to manage permissions per browser context:
- BrowserContext.overridePermissions(origin, permissions)
- BrowserContext.clearPermissionOverrides()

Fixes #846.
  • Loading branch information
aslushnikov committed Aug 30, 2018
1 parent df459ba commit 50d6c2d
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 4 deletions.
50 changes: 50 additions & 0 deletions docs/api.md
Expand Up @@ -35,6 +35,7 @@ Next Release: **Sep 6, 2018**
* [browser.browserContexts()](#browserbrowsercontexts)
* [browser.close()](#browserclose)
* [browser.createIncognitoBrowserContext()](#browsercreateincognitobrowsercontext)
* [browser.defaultBrowserContext()](#browserdefaultbrowsercontext)
* [browser.disconnect()](#browserdisconnect)
* [browser.newPage()](#browsernewpage)
* [browser.pages()](#browserpages)
Expand All @@ -48,9 +49,11 @@ Next Release: **Sep 6, 2018**
* [event: 'targetcreated'](#event-targetcreated-1)
* [event: 'targetdestroyed'](#event-targetdestroyed-1)
* [browserContext.browser()](#browsercontextbrowser)
* [browserContext.clearPermissionOverrides()](#browsercontextclearpermissionoverrides)
* [browserContext.close()](#browsercontextclose)
* [browserContext.isIncognito()](#browsercontextisincognito)
* [browserContext.newPage()](#browsercontextnewpage)
* [browserContext.overridePermissions(origin, permissions)](#browsercontextoverridepermissionsorigin-permissions)
* [browserContext.pages()](#browsercontextpages)
* [browserContext.targets()](#browsercontexttargets)
- [class: Page](#class-page)
Expand Down Expand Up @@ -612,6 +615,11 @@ const page = await context.newPage();
await page.goto('https://example.com');
```

#### browser.defaultBrowserContext()
- returns: <[BrowserContext]>

Returns the default browser context. The default browser context can not be closed.

#### browser.disconnect()

Disconnects Puppeteer from the browser, but leaves the Chromium process running. After calling `disconnect`, the [Browser] object is considered disposed and cannot be used anymore.
Expand Down Expand Up @@ -698,6 +706,18 @@ Emitted when a target inside the browser context is destroyed, for example when

The browser this browser context belongs to.

#### browserContext.clearPermissionOverrides()
- returns: <[Promise]>

Clears all permission overrides for the browser context.

```js
const context = browser.defaultBrowserContext();
context.overridePermissions('https://example.com', ['clipboard-read']);
// do stuff ..
context.clearPermissionOverrides();
```

#### browserContext.close()
- returns: <[Promise]>

Expand All @@ -719,6 +739,35 @@ The default browser context is the only non-incognito browser context.

Creates a new page in the browser context.


#### browserContext.overridePermissions(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]>


```js
const context = browser.defaultBrowserContext();
await context.overridePermissions('https://html5demos.com', ['geolocation']);
```


#### 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).

Expand Down Expand Up @@ -3165,6 +3214,7 @@ TimeoutError is emitted whenever certain operations are terminated due to timeou
[function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function "Function"
[number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type "Number"
[Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object"
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin "Origin"
[Page]: #class-page "Page"
[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise"
[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String"
Expand Down
54 changes: 50 additions & 4 deletions lib/Browser.js
Expand Up @@ -37,11 +37,11 @@ class Browser extends EventEmitter {
this._connection = connection;
this._closeCallback = closeCallback || new Function();

this._defaultContext = new BrowserContext(this, null);
this._defaultContext = new BrowserContext(this._connection, this, null);
/** @type {Map<string, BrowserContext>} */
this._contexts = new Map();
for (const contextId of contextIds)
this._contexts.set(contextId, new BrowserContext(this, contextId));
this._contexts.set(contextId, new BrowserContext(this._connection, this, contextId));

/** @type {Map<string, Target>} */
this._targets = new Map();
Expand All @@ -65,7 +65,7 @@ class Browser extends EventEmitter {
*/
async createIncognitoBrowserContext() {
const {browserContextId} = await this._connection.send('Target.createBrowserContext');
const context = new BrowserContext(this, browserContextId);
const context = new BrowserContext(this._connection, this, browserContextId);
this._contexts.set(browserContextId, context);
return context;
}
Expand All @@ -77,6 +77,13 @@ class Browser extends EventEmitter {
return [this._defaultContext, ...Array.from(this._contexts.values())];
}

/**
* @return {!BrowserContext}
*/
defaultBrowserContext() {
return this._defaultContext;
}

/**
* @param {?string} contextId
*/
Expand Down Expand Up @@ -231,11 +238,13 @@ Browser.Events = {

class BrowserContext extends EventEmitter {
/**
* @param {!Puppeteer.Connection} connection
* @param {!Browser} browser
* @param {?string} contextId
*/
constructor(browser, contextId) {
constructor(connection, browser, contextId) {
super();
this._connection = connection;
this._browser = browser;
this._id = contextId;
}
Expand Down Expand Up @@ -266,6 +275,43 @@ class BrowserContext extends EventEmitter {
return !!this._id;
}

/**
* @param {string} origin
* @param {!Array<string>} permissions
*/
async overridePermissions(origin, permissions) {
const webPermissionToProtocol = new Map([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
['push', 'push'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardRead'],
['clipboard-write', 'clipboardWrite'],
['payment-handler', 'paymentHandler'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
permissions = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._connection.send('Browser.grantPermissions', {origin, browserContextId: this._id || undefined, permissions});
}

async clearPermissionOverrides() {
await this._connection.send('Browser.resetPermissions', {browserContextId: this._id || undefined});
}

/**
* @return {!Promise<!Puppeteer.Page>}
*/
Expand Down
1 change: 1 addition & 0 deletions test/browsercontext.spec.js
Expand Up @@ -29,6 +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(error.message).toContain('cannot be closed');
});
it('should create new incognito context', async function({browser, server}) {
Expand Down
53 changes: 53 additions & 0 deletions test/page.spec.js
Expand Up @@ -75,6 +75,59 @@ module.exports.addTests = function({testRunner, expect, headless}) {
});
});

describe('BrowserContext.overridePermissions', function() {
function getPermission(page, name) {
return page.evaluate(name => navigator.permissions.query({name}).then(result => result.state), name);
}

it('should be prompt by default', async({page, server, context}) => {
await page.goto(server.EMPTY_PAGE);
expect(await getPermission(page, 'geolocation')).toBe('prompt');
});
it('should deny permission when not listed', async({page, server, context}) => {
await page.goto(server.EMPTY_PAGE);
await context.overridePermissions(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 = null;
await context.overridePermissions(server.EMPTY_PAGE, ['foo']).catch(e => error = e);
expect(error.message).toBe('Unknown permission: foo');
});
it('should grant permission when listed', async({page, server, context}) => {
await page.goto(server.EMPTY_PAGE);
await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
expect(await getPermission(page, 'geolocation')).toBe('granted');
});
it('should reset permissions', async({page, server, context}) => {
await page.goto(server.EMPTY_PAGE);
await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']);
expect(await getPermission(page, 'geolocation')).toBe('granted');
await context.clearPermissionOverrides();
expect(await getPermission(page, 'geolocation')).toBe('prompt');
});
it('should trigger permission onchange', async({page, server, context}) => {
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
window.events = [];
return navigator.permissions.query({name: 'clipboard-read'}).then(function(result) {
window.events.push(result.state);
result.onchange = function() {
window.events.push(result.state);
};
});
});
expect(await page.evaluate(() => window.events)).toEqual(['prompt']);
await context.overridePermissions(server.EMPTY_PAGE, []);
expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied']);
await context.overridePermissions(server.EMPTY_PAGE, ['clipboard-read']);
expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied', 'granted']);
await context.clearPermissionOverrides();
expect(await page.evaluate(() => window.events)).toEqual(['prompt', 'denied', 'granted', 'prompt']);
});
});

describe('Page.evaluate', function() {
it('should work', async({page, server}) => {
const result = await page.evaluate(() => 7 * 3);
Expand Down

0 comments on commit 50d6c2d

Please sign in to comment.