diff --git a/docs/api.md b/docs/api.md index b950218f5f..d04d5c5e88 100644 --- a/docs/api.md +++ b/docs/api.md @@ -210,6 +210,9 @@ Indicates that the browser is connected. - `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) 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). + - `username` <[string]> + - `password` <[string]> - returns: <[Promise]<[BrowserContext]>> Creates a new browser context. It won't share cookies/cache with other browser contexts. @@ -245,6 +248,9 @@ Creates a new browser context. It won't share cookies/cache with other browser c - `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) 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). + - `username` <[string]> + - `password` <[string]> - returns: <[Promise]<[Page]>> Creates a new page in a new browser context. Closing this page will close the context as well. @@ -289,6 +295,7 @@ await context.close(); - [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) - [browserContext.setExtraHTTPHeaders(headers)](#browsercontextsetextrahttpheadersheaders) - [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation) +- [browserContext.setHTTPCredentials(httpCredentials)](#browsercontextsethttpcredentialshttpcredentials) - [browserContext.setOffline(offline)](#browsercontextsetofflineoffline) - [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) - [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate) @@ -521,6 +528,16 @@ await browserContext.setGeolocation({latitude: 59.95, longitude: 30.31667}); > **NOTE** Consider using [browserContext.setPermissions](#browsercontextsetpermissions-permissions) to grant permissions for the page to read its geolocation. +#### browserContext.setHTTPCredentials(httpCredentials) +- `httpCredentials` + - `username` <[string]> + - `password` <[string]> +- returns: <[Promise]> + +Provide credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). + +To disable authentication, pass `null`. + #### browserContext.setOffline(offline) - `offline` <[boolean]> Whether to emulate network being offline for the browser context. - returns: <[Promise]> @@ -624,7 +641,6 @@ page.removeListener('request', logRequest); - [page.addInitScript(script[, ...args])](#pageaddinitscriptscript-args) - [page.addScriptTag(options)](#pageaddscripttagoptions) - [page.addStyleTag(options)](#pageaddstyletagoptions) -- [page.authenticate(credentials)](#pageauthenticatecredentials) - [page.check(selector, [options])](#pagecheckselector-options) - [page.click(selector[, options])](#pageclickselector-options) - [page.close([options])](#pagecloseoptions) @@ -894,16 +910,6 @@ Adds a `` tag into the page with the desired url or a ` - - `username` <[string]> - - `password` <[string]> -- returns: <[Promise]> - -Provide credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). - -To disable authentication, pass `null`. - #### page.check(selector, [options]) - `selector` <[string]> A selector to search for checkbox or radio button to check. If there are multiple elements satisfying the selector, the first will be checked. - `options` <[Object]> @@ -3895,6 +3901,7 @@ const backgroundPage = await backroundPageTarget.page(); - [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) - [browserContext.setExtraHTTPHeaders(headers)](#browsercontextsetextrahttpheadersheaders) - [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 3c140737d9..59a06503e5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "main": "index.js", "playwright": { "chromium_revision": "747023", - "firefox_revision": "1035", + "firefox_revision": "1039", "webkit_revision": "1168" }, "scripts": { diff --git a/src/browserContext.ts b/src/browserContext.ts index 7bca57a2eb..8a30fe3e3d 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -35,6 +35,7 @@ export type BrowserContextOptions = { permissions?: { [key: string]: string[] }, extraHTTPHeaders?: network.Headers, offline?: boolean, + httpCredentials?: types.Credentials, }; export interface BrowserContext { @@ -50,6 +51,7 @@ export interface BrowserContext { setGeolocation(geolocation: types.Geolocation | null): Promise; setExtraHTTPHeaders(headers: network.Headers): Promise; setOffline(offline: boolean): Promise; + setHTTPCredentials(httpCredentials: types.Credentials | null): Promise; addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]): Promise; exposeFunction(name: string, playwrightFunction: Function): Promise; waitForEvent(event: string, optionsOrPredicate?: Function | (types.TimeoutOptions & { predicate?: Function })): Promise; @@ -93,6 +95,7 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement abstract setPermissions(origin: string, permissions: string[]): Promise; abstract clearPermissions(): Promise; abstract setGeolocation(geolocation: types.Geolocation | null): Promise; + abstract setHTTPCredentials(httpCredentials: types.Credentials | null): Promise; abstract setExtraHTTPHeaders(headers: network.Headers): Promise; abstract setOffline(offline: boolean): Promise; abstract addInitScript(script: string | Function | { path?: string | undefined; content?: string | undefined; }, ...args: any[]): Promise; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 3b2102298a..2e55d21b0f 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -244,6 +244,8 @@ export class CRBrowserContext extends BrowserContextBase { await this.setGeolocation(this._options.geolocation); if (this._options.offline) await this.setOffline(this._options.offline); + if (this._options.httpCredentials) + await this.setHTTPCredentials(this._options.httpCredentials); } _existingPages(): Page[] { @@ -344,6 +346,12 @@ export class CRBrowserContext extends BrowserContextBase { await (page._delegate as CRPage)._networkManager.setOffline(offline); } + async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise { + this._options.httpCredentials = httpCredentials || undefined; + for (const page of this._existingPages()) + await (page._delegate as CRPage)._networkManager.authenticate(httpCredentials); + } + async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { const source = await helper.evaluationScript(script, args); this._evaluateOnNewDocumentSources.push(source); diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index f7e7b5d70c..a6c03aa996 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -121,6 +121,8 @@ export class CRPage implements PageDelegate { promises.push(this.updateExtraHTTPHeaders()); if (options.offline) promises.push(this._networkManager.setOffline(options.offline)); + if (options.httpCredentials) + promises.push(this._networkManager.authenticate(options.httpCredentials)); for (const binding of this._browserContext._pageBindings.values()) promises.push(this._initBinding(binding)); for (const source of this._browserContext._evaluateOnNewDocumentSources) @@ -376,10 +378,6 @@ export class CRPage implements PageDelegate { await this._networkManager.setRequestInterception(enabled); } - async authenticate(credentials: types.Credentials | null) { - await this._networkManager.authenticate(credentials); - } - async setFileChooserIntercepted(enabled: boolean) { await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed. } diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 0bca65ffe7..6d34d5a7f7 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -289,6 +289,8 @@ export class FFBrowserContext extends BrowserContextBase { await this.setExtraHTTPHeaders(this._options.extraHTTPHeaders); if (this._options.offline) await this.setOffline(this._options.offline); + if (this._options.httpCredentials) + await this.setHTTPCredentials(this._options.httpCredentials); } _existingPages(): Page[] { @@ -381,6 +383,11 @@ export class FFBrowserContext extends BrowserContextBase { this._options.offline = offline; } + async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise { + this._options.httpCredentials = httpCredentials || undefined; + await this._browser._connection.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId || undefined, credentials: httpCredentials }); + } + async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { const source = await helper.evaluationScript(script, args); this._evaluateOnNewDocumentSources.push(source); diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 9da098d886..f00616b6d2 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -284,10 +284,6 @@ export class FFPage implements PageDelegate { await this._networkManager.setRequestInterception(enabled); } - async authenticate(credentials: types.Credentials | null): Promise { - await this._session.send('Network.setAuthCredentials', credentials || { username: null, password: null }); - } - async setFileChooserIntercepted(enabled: boolean) { await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed. } diff --git a/src/page.ts b/src/page.ts index f2e12274ab..24b4677c20 100644 --- a/src/page.ts +++ b/src/page.ts @@ -49,7 +49,6 @@ export interface PageDelegate { setViewportSize(viewportSize: types.Size): Promise; setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise; setRequestInterception(enabled: boolean): Promise; - authenticate(credentials: types.Credentials | null): Promise; setFileChooserIntercepted(enabled: boolean): Promise; canScreenshotOutsideViewport(): boolean; @@ -79,7 +78,6 @@ type PageState = { colorScheme: types.ColorScheme | null; extraHTTPHeaders: network.Headers | null; interceptNetwork: boolean | null; - credentials: types.Credentials | null; hasTouch: boolean | null; }; @@ -150,7 +148,6 @@ export class Page extends platform.EventEmitter { colorScheme: null, extraHTTPHeaders: null, interceptNetwork: null, - credentials: null, hasTouch: null, }; this.accessibility = new accessibility.Accessibility(delegate.getAccessibilityTree.bind(delegate)); @@ -412,11 +409,6 @@ export class Page extends platform.EventEmitter { request.continue(); } - async authenticate(credentials: types.Credentials | null) { - this._state.credentials = credentials; - await this._delegate.authenticate(credentials); - } - async screenshot(options?: types.ScreenshotOptions): Promise { return this._screenshotter.screenshotPage(options); } diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 517836fe54..4dc83599b6 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -198,6 +198,8 @@ export class WKBrowserContext extends BrowserContextBase { await this.setGeolocation(this._options.geolocation); if (this._options.offline) await this.setOffline(this._options.offline); + if (this._options.httpCredentials) + await this.setHTTPCredentials(this._options.httpCredentials); } _existingPages(): Page[] { @@ -285,6 +287,12 @@ export class WKBrowserContext extends BrowserContextBase { await (page._delegate as WKPage).updateOffline(); } + async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise { + this._options.httpCredentials = httpCredentials || undefined; + for (const page of this._existingPages()) + await (page._delegate as WKPage).updateHttpCredentials(); + } + async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { const source = await helper.evaluationScript(script, args); this._evaluateOnNewDocumentSources.push(source); diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index dabde85b05..5b0fa33e4d 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -90,13 +90,13 @@ export class WKPage implements PageDelegate { const promises: Promise[] = [ this._pageProxySession.send('Dialog.enable'), this._pageProxySession.send('Emulation.setActiveAndFocused', { active: true }), - this.authenticate(this._page._state.credentials) ]; const contextOptions = this._browserContext._options; if (contextOptions.javaScriptEnabled === false) promises.push(this._pageProxySession.send('Emulation.setJavaScriptEnabled', { enabled: false })); if (this._page._state.viewportSize || contextOptions.viewport) promises.push(this._updateViewport(true /* updateTouch */)); + promises.push(this.updateHttpCredentials()); await Promise.all(promises); } @@ -515,8 +515,9 @@ export class WKPage implements PageDelegate { await this._updateState('Network.setEmulateOfflineState', { offline: !!this._browserContext._options.offline }); } - async authenticate(credentials: types.Credentials | null) { - await this._pageProxySession.send('Emulation.setAuthCredentials', { ...(credentials || { username: '', password: '' }) }); + async updateHttpCredentials() { + const credentials = this._browserContext._options.httpCredentials || { username: '', password: '' }; + await this._pageProxySession.send('Emulation.setAuthCredentials', { username: credentials.username, password: credentials.password }); } async setFileChooserIntercepted(enabled: boolean) { diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 8e5f47bb8a..92487d5bf7 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -367,6 +367,53 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, FF }); }); + describe('BrowserContext.setHTTPCredentials', function() { + it('should work', async({browser, server}) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext(); + const page = await context.newPage(); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + await context.setHTTPCredentials({ + username: 'user', + password: 'pass' + }); + response = await page.reload(); + expect(response.status()).toBe(200); + await context.close(); + }); + it('should fail if wrong credentials', async({browser, server}) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: { username: 'foo', password: 'bar' } + }); + const page = await context.newPage(); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(401); + await context.setHTTPCredentials({ + username: 'user', + password: 'pass' + }); + response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + await context.close(); + }); + it('should allow disable authentication', async({browser, server}) => { + server.setAuth('/empty.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: { username: 'user', password: 'pass' } + }); + const page = await context.newPage(); + let response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + await context.setHTTPCredentials(null); + // Navigate to a different origin to bust Chromium's credential caching. + response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(response.status()).toBe(401); + await context.close(); + }); + }); + describe.fail(FFOX)('BrowserContext.setOffline', function() { it('should work with initial option', async({browser, server}) => { const context = await browser.newContext({offline: true}); diff --git a/test/interception.spec.js b/test/interception.spec.js index 6f5f349b54..4f434540b7 100644 --- a/test/interception.spec.js +++ b/test/interception.spec.js @@ -486,44 +486,6 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p }); }); - describe('Page.authenticate', function() { - it('should work', async({page, server}) => { - server.setAuth('/empty.html', 'user', 'pass'); - let response = await page.goto(server.EMPTY_PAGE); - expect(response.status()).toBe(401); - await page.authenticate({ - username: 'user', - password: 'pass' - }); - response = await page.reload(); - expect(response.status()).toBe(200); - }); - it('should fail if wrong credentials', async({page, server}) => { - // Use unique user/password since Chromium caches credentials per origin. - server.setAuth('/empty.html', 'user2', 'pass2'); - await page.authenticate({ - username: 'foo', - password: 'bar' - }); - const response = await page.goto(server.EMPTY_PAGE); - expect(response.status()).toBe(401); - }); - it('should allow disable authentication', async({page, server}) => { - // Use unique user/password since Chromium caches credentials per origin. - server.setAuth('/empty.html', 'user3', 'pass3'); - await page.authenticate({ - username: 'user3', - password: 'pass3' - }); - let response = await page.goto(server.EMPTY_PAGE); - expect(response.status()).toBe(200); - await page.authenticate(null); - // Navigate to a different origin to bust Chromium's credential caching. - response = await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); - expect(response.status()).toBe(401); - }); - }); - describe('Interception vs isNavigationRequest', () => { it('should work with request interception', async({page, server}) => { const requests = new Map(); diff --git a/test/popup.spec.js b/test/popup.spec.js index bf049abb9a..bdb51e97ad 100644 --- a/test/popup.spec.js +++ b/test/popup.spec.js @@ -82,6 +82,21 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE await context.close(); expect(online).toBe(false); }); + it('should inherit http credentials from browser context', async function({browser, server}) { + // Use unique user/password since Chromium caches credentials per origin. + server.setAuth('/title.html', 'user', 'pass'); + const context = await browser.newContext({ + httpCredentials: { username: 'user', password: 'pass' } + }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const [popup] = await Promise.all([ + page.waitForEvent('popup').then(async e => { const popup = await e.page(); await popup.waitForLoadState(); return popup; }), + page.evaluate(url => window._popup = window.open(url), server.PREFIX + '/title.html'), + ]); + expect(await popup.title()).toBe('Woof-Woof'); + await context.close(); + }); it.skip(FFOX)('should inherit touch support from browser context', async function({browser, server}) { const context = await browser.newContext({ viewport: { width: 400, height: 500, isMobile: true }