feat(popups): introduce BrowserContext.setDefaultHTTPHeaders (#1116)

This commit is contained in:
Dmitry Gozman 2020-02-26 12:42:20 -08:00 committed by GitHub
parent 4f69930fbe
commit 672f3f9960
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 134 additions and 30 deletions

View File

@ -199,6 +199,7 @@ Indicates that the browser is connected.
- `accuracy` <[number]> Optional non-negative accuracy value. - `accuracy` <[number]> Optional non-negative accuracy value.
- `locale` <?[string]> Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules. - `locale` <?[string]> Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules.
- `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) for more details. - `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.
- returns: <[Promise]<[BrowserContext]>> - returns: <[Promise]<[BrowserContext]>>
Creates a new browser context. It won't share cookies/cache with other browser contexts. Creates a new browser context. It won't share cookies/cache with other browser contexts.
@ -232,6 +233,7 @@ Creates a new browser context. It won't share cookies/cache with other browser c
- `accuracy` <[number]> Optional non-negative accuracy value. - `accuracy` <[number]> Optional non-negative accuracy value.
- `locale` <?[string]> Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules. - `locale` <?[string]> Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules.
- `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) for more details. - `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.
- returns: <[Promise]<[Page]>> - returns: <[Promise]<[Page]>>
Creates a new page in a new browser context. Closing this page will close the context as well. Creates a new page in a new browser context. Closing this page will close the context as well.
@ -271,6 +273,7 @@ await context.close();
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) - [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
- [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout) - [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout)
- [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) - [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout)
- [browserContext.setExtraHTTPHeaders(headers)](#browsercontextsetextrahttpheadersheaders)
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation) - [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) - [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions)
<!-- GEN:stop --> <!-- GEN:stop -->
@ -374,6 +377,14 @@ This setting will change the default maximum time for all the methods accepting
> **NOTE** [`page.setDefaultNavigationTimeout`](#pagesetdefaultnavigationtimeouttimeout), [`page.setDefaultTimeout`](#pagesetdefaulttimeouttimeout) and [`browserContext.setDefaultNavigationTimeout`](#browsercontextsetdefaultnavigationtimeouttimeout) take priority over [`browserContext.setDefaultTimeout`](#browserContextsetdefaulttimeouttimeout). > **NOTE** [`page.setDefaultNavigationTimeout`](#pagesetdefaultnavigationtimeouttimeout), [`page.setDefaultTimeout`](#pagesetdefaulttimeouttimeout) and [`browserContext.setDefaultNavigationTimeout`](#browsercontextsetdefaultnavigationtimeouttimeout) take priority over [`browserContext.setDefaultTimeout`](#browserContextsetdefaulttimeouttimeout).
#### browserContext.setExtraHTTPHeaders(headers)
- `headers` <[Object]> An object containing additional HTTP headers to be sent with every request. All header values must be strings.
- returns: <[Promise]>
The extra HTTP headers will be sent with every request initiated by any page in the context. These headers are merged with page-specific extra HTTP headers set with [page.setExtraHTTPHeaders()](#pagesetextrahttpheadersheaders). If page overrides a particular header, page-specific header value will be used instead of the browser context header value.
> **NOTE** `browserContext.setExtraHTTPHeaders` does not guarantee the order of headers in the outgoing requests.
#### browserContext.setGeolocation(geolocation) #### browserContext.setGeolocation(geolocation)
- `geolocation` <[Object]> - `geolocation` <[Object]>
- `latitude` <[number]> Latitude between -90 and 90. - `latitude` <[number]> Latitude between -90 and 90.
@ -3582,6 +3593,7 @@ const backgroundPage = await backroundPageTarget.page();
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) - [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
- [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout) - [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout)
- [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) - [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout)
- [browserContext.setExtraHTTPHeaders(headers)](#browsercontextsetextrahttpheadersheaders)
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation) - [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) - [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions)
<!-- GEN:stop --> <!-- GEN:stop -->

View File

@ -9,7 +9,7 @@
"main": "index.js", "main": "index.js",
"playwright": { "playwright": {
"chromium_revision": "744254", "chromium_revision": "744254",
"firefox_revision": "1029", "firefox_revision": "1031",
"webkit_revision": "1155" "webkit_revision": "1155"
}, },
"scripts": { "scripts": {

View File

@ -30,7 +30,8 @@ export type BrowserContextOptions = {
locale?: string, locale?: string,
timezoneId?: string, timezoneId?: string,
geolocation?: types.Geolocation, geolocation?: types.Geolocation,
permissions?: { [key: string]: string[] }; permissions?: { [key: string]: string[] },
extraHTTPHeaders?: network.Headers,
}; };
export interface BrowserContext { export interface BrowserContext {
@ -44,6 +45,7 @@ export interface BrowserContext {
setPermissions(origin: string, permissions: string[]): Promise<void>; setPermissions(origin: string, permissions: string[]): Promise<void>;
clearPermissions(): Promise<void>; clearPermissions(): Promise<void>;
setGeolocation(geolocation: types.Geolocation | null): Promise<void>; setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
setExtraHTTPHeaders(headers: network.Headers): Promise<void>;
close(): Promise<void>; close(): Promise<void>;
_existingPages(): Page[]; _existingPages(): Page[];
@ -67,6 +69,8 @@ export function validateBrowserContextOptions(options: BrowserContextOptions): B
result.viewport = { ...result.viewport }; result.viewport = { ...result.viewport };
if (result.geolocation) if (result.geolocation)
result.geolocation = verifyGeolocation(result.geolocation); result.geolocation = verifyGeolocation(result.geolocation);
if (result.extraHTTPHeaders)
result.extraHTTPHeaders = network.verifyHeaders(result.extraHTTPHeaders);
return result; return result;
} }

View File

@ -294,6 +294,12 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {}); await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {});
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
for (const page of this._existingPages())
await (page._delegate as CRPage).updateExtraHTTPHeaders();
}
async close() { async close() {
if (this._closed) if (this._closed)
return; return;

View File

@ -118,6 +118,7 @@ export class CRPage implements PageDelegate {
promises.push(emulateTimezone(this._client, options.timezoneId)); promises.push(emulateTimezone(this._client, options.timezoneId));
if (options.geolocation) if (options.geolocation)
promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation)); promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation));
promises.push(this.updateExtraHTTPHeaders());
await Promise.all(promises); await Promise.all(promises);
} }
@ -316,7 +317,11 @@ export class CRPage implements PageDelegate {
this._page._onFileChooserOpened(handle); this._page._onFileChooserOpened(handle);
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
const headers = network.mergeHeaders([
this._page.context()._options.extraHTTPHeaders,
this._page._state.extraHTTPHeaders
]);
await this._client.send('Network.setExtraHTTPHeaders', { headers }); await this._client.send('Network.setExtraHTTPHeaders', { headers });
} }

View File

@ -28,6 +28,7 @@ import * as platform from '../platform';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings'; import { TimeoutSettings } from '../timeoutSettings';
import { headersArray } from './ffNetworkManager';
export class FFBrowser extends platform.EventEmitter implements Browser { export class FFBrowser extends platform.EventEmitter implements Browser {
_connection: FFConnection; _connection: FFConnection;
@ -274,6 +275,8 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation) if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation); await this.setGeolocation(this._options.geolocation);
if (this._options.extraHTTPHeaders)
await this.setExtraHTTPHeaders(this._options.extraHTTPHeaders);
} }
_existingPages(): Page[] { _existingPages(): Page[] {
@ -349,6 +352,11 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
throw new Error('Geolocation emulation is not supported in Firefox'); throw new Error('Geolocation emulation is not supported in Firefox');
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId || undefined, headers: headersArray(this._options.extraHTTPHeaders) });
}
async close() { async close() {
if (this._closed) if (this._closed)
return; return;

View File

@ -208,7 +208,7 @@ class InterceptableRequest implements network.RequestDelegate {
} }
} }
function headersArray(headers: network.Headers): Protocol.Network.HTTPHeader[] { export function headersArray(headers: network.Headers): Protocol.Network.HTTPHeader[] {
const result: Protocol.Network.HTTPHeader[] = []; const result: Protocol.Network.HTTPHeader[] = [];
for (const name in headers) { for (const name in headers) {
if (!Object.is(headers[name], undefined)) if (!Object.is(headers[name], undefined))

View File

@ -21,14 +21,13 @@ import * as dom from '../dom';
import { FFSession } from './ffConnection'; import { FFSession } from './ffConnection';
import { FFExecutionContext } from './ffExecutionContext'; import { FFExecutionContext } from './ffExecutionContext';
import { Page, PageDelegate, Worker } from '../page'; import { Page, PageDelegate, Worker } from '../page';
import { FFNetworkManager } from './ffNetworkManager'; import { FFNetworkManager, headersArray } from './ffNetworkManager';
import { Events } from '../events'; import { Events } from '../events';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { RawMouseImpl, RawKeyboardImpl } from './ffInput'; import { RawMouseImpl, RawKeyboardImpl } from './ffInput';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { getAccessibilityTree } from './ffAccessibility'; import { getAccessibilityTree } from './ffAccessibility';
import * as network from '../network';
import * as types from '../types'; import * as types from '../types';
import * as platform from '../platform'; import * as platform from '../platform';
import { kScreenshotDuringNavigationError } from '../screenshotter'; import { kScreenshotDuringNavigationError } from '../screenshotter';
@ -251,11 +250,8 @@ export class FFPage implements PageDelegate {
return { newDocumentId: response.navigationId || undefined }; return { newDocumentId: response.navigationId || undefined };
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
const array = []; await this._session.send('Network.setExtraHTTPHeaders', { headers: headersArray(this._page._state.extraHTTPHeaders || {}) });
for (const [name, value] of Object.entries(headers))
array.push({ name, value });
await this._session.send('Network.setExtraHTTPHeaders', { headers: array });
} }
async setViewportSize(viewportSize: types.Size): Promise<void> { async setViewportSize(viewportSize: types.Size): Promise<void> {

View File

@ -15,7 +15,7 @@
*/ */
import * as frames from './frames'; import * as frames from './frames';
import { assert } from './helper'; import { assert, helper } from './helper';
import * as platform from './platform'; import * as platform from './platform';
export type NetworkCookie = { export type NetworkCookie = {
@ -373,3 +373,31 @@ export const STATUS_TEXTS: { [status: string]: string } = {
'510': 'Not Extended', '510': 'Not Extended',
'511': 'Network Authentication Required', '511': 'Network Authentication Required',
}; };
export function verifyHeaders(headers: Headers): Headers {
const result: Headers = {};
for (const key of Object.keys(headers)) {
const value = headers[key];
assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
result[key] = value;
}
return result;
}
export function mergeHeaders(headers: (Headers | undefined | null)[]): Headers {
const lowerCaseToValue = new Map<string, string>();
const lowerCaseToOriginalCase = new Map<string, string>();
for (const h of headers) {
if (!h)
continue;
for (const key of Object.keys(h)) {
const lower = key.toLowerCase();
lowerCaseToOriginalCase.set(lower, key);
lowerCaseToValue.set(lower, h[key]);
}
}
const result: Headers = {};
for (const [lower, value] of lowerCaseToValue)
result[lowerCaseToOriginalCase.get(lower)!] = value;
return result;
}

View File

@ -45,7 +45,7 @@ export interface PageDelegate {
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>; navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>; updateExtraHTTPHeaders(): Promise<void>;
setViewportSize(viewportSize: types.Size): Promise<void>; setViewportSize(viewportSize: types.Size): Promise<void>;
setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void>; setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void>;
setCacheEnabled(enabled: boolean): Promise<void>; setCacheEnabled(enabled: boolean): Promise<void>;
@ -268,13 +268,8 @@ export class Page extends platform.EventEmitter {
} }
setExtraHTTPHeaders(headers: network.Headers) { setExtraHTTPHeaders(headers: network.Headers) {
this._state.extraHTTPHeaders = {}; this._state.extraHTTPHeaders = network.verifyHeaders(headers);
for (const key of Object.keys(headers)) { return this._delegate.updateExtraHTTPHeaders();
const value = headers[key];
assert(helper.isString(value), `Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
this._state.extraHTTPHeaders[key] = value;
}
return this._delegate.setExtraHTTPHeaders(headers);
} }
async _onBindingCalled(payload: string, context: js.ExecutionContext) { async _onBindingCalled(payload: string, context: js.ExecutionContext) {

View File

@ -28,6 +28,7 @@ import { WKConnection, WKSession, kPageProxyMessageReceived, PageProxyMessageRec
import { WKPageProxy } from './wkPageProxy'; import { WKPageProxy } from './wkPageProxy';
import * as platform from '../platform'; import * as platform from '../platform';
import { TimeoutSettings } from '../timeoutSettings'; import { TimeoutSettings } from '../timeoutSettings';
import { WKPage } from './wkPage';
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15'; const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
@ -269,6 +270,12 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
await this._browser._browserSession.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload }); await this._browser._browserSession.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload });
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> {
this._options.extraHTTPHeaders = network.verifyHeaders(headers);
for (const page of this._existingPages())
await (page._delegate as WKPage).updateExtraHTTPHeaders();
}
async close() { async close() {
if (this._closed) if (this._closed)
return; return;

View File

@ -142,12 +142,7 @@ export class WKPage implements PageDelegate {
} }
if (contextOptions.bypassCSP) if (contextOptions.bypassCSP)
promises.push(session.send('Page.setBypassCSP', { enabled: true })); promises.push(session.send('Page.setBypassCSP', { enabled: true }));
if (this._page._state.extraHTTPHeaders || contextOptions.locale) { promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() }));
const headers = this._page._state.extraHTTPHeaders || {};
if (contextOptions.locale)
headers['Accept-Language'] = contextOptions.locale;
promises.push(session.send('Network.setExtraHTTPHeaders', { headers }));
}
if (this._page._state.hasTouch) if (this._page._state.hasTouch)
promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: true })); promises.push(session.send('Page.setTouchEmulationEnabled', { enabled: true }));
if (contextOptions.timezoneId) { if (contextOptions.timezoneId) {
@ -378,12 +373,19 @@ export class WKPage implements PageDelegate {
await Promise.all(promises); await Promise.all(promises);
} }
async setExtraHTTPHeaders(headers: network.Headers): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
const copy = { ...headers }; await this._updateState('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() });
}
_calculateExtraHTTPHeaders(): network.Headers {
const headers = network.mergeHeaders([
this._page.context()._options.extraHTTPHeaders,
this._page._state.extraHTTPHeaders
]);
const locale = this._page.context()._options.locale; const locale = this._page.context()._options.locale;
if (locale) if (locale)
copy['Accept-Language'] = locale; headers['Accept-Language'] = locale;
await this._updateState('Network.setExtraHTTPHeaders', { headers: copy }); return headers;
} }
async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> { async setEmulateMedia(mediaType: types.MediaType | null, colorScheme: types.ColorScheme | null): Promise<void> {

View File

@ -331,6 +331,35 @@ module.exports.describe = function({testRunner, expect, MAC, WIN, FFOX, CHROMIUM
]); ]);
expect(request.headers['foo']).toBe('bar'); expect(request.headers['foo']).toBe('bar');
}); });
it('should work with extra headers from browser context', async({browser, server}) => {
const context = await browser.newContext();
await context.setExtraHTTPHeaders({
'foo': 'bar',
});
const page = await context.newPage();
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE),
]);
await context.close();
expect(request.headers['foo']).toBe('bar');
});
it('should override extra headers from browser context', async({browser, server}) => {
const context = await browser.newContext({
extraHTTPHeaders: { 'fOo': 'bAr', 'baR': 'foO' },
});
const page = await context.newPage();
await page.setExtraHTTPHeaders({
'Foo': 'Bar'
});
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE),
]);
await context.close();
expect(request.headers['foo']).toBe('Bar');
expect(request.headers['bar']).toBe('foO');
});
it('should throw for non-string header values', async({page, server}) => { it('should throw for non-string header values', async({page, server}) => {
let error = null; let error = null;
try { try {

View File

@ -36,6 +36,18 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(userAgent).toBe('hey'); expect(userAgent).toBe('hey');
expect(request.headers['user-agent']).toBe('hey'); expect(request.headers['user-agent']).toBe('hey');
}); });
it.skip(CHROMIUM)('should inherit extra headers from browser context', async function({browser, server}) {
const context = await browser.newContext({
extraHTTPHeaders: { 'foo': 'bar' },
});
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const requestPromise = server.waitForRequest('/dummy.html');
await page.evaluate(url => window._popup = window.open(url), server.PREFIX + '/dummy.html');
const request = await requestPromise;
await context.close();
expect(request.headers['foo']).toBe('bar');
});
it.skip(CHROMIUM)('should inherit touch support from browser context', async function({browser, server}) { it.skip(CHROMIUM)('should inherit touch support from browser context', async function({browser, server}) {
const context = await browser.newContext({ const context = await browser.newContext({
viewport: { width: 400, height: 500, isMobile: true } viewport: { width: 400, height: 500, isMobile: true }