feat: support extra http headers in browserType.connect() (#6301)

This commit is contained in:
Yury Semikhatsky 2021-04-23 14:52:27 -07:00 committed by GitHub
parent 83758fa48c
commit fd31ea8b0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 110 additions and 11 deletions

View File

@ -75,6 +75,7 @@ This methods attaches Playwright to an existing browser instance.
* langs: js * langs: js
- `params` <[Object]> - `params` <[Object]>
- `wsEndpoint` <[string]> A browser websocket endpoint to connect to. - `wsEndpoint` <[string]> A browser websocket endpoint to connect to.
- `headers` <[Object]<[string], [string]>> Additional HTTP headers to be sent with web socket connect request. Optional.
- `slowMo` <[float]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you - `slowMo` <[float]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you
can see what is going on. Defaults to 0. can see what is going on. Defaults to 0.
- `logger` <[Logger]> Logger sink for Playwright logging. Optional. - `logger` <[Logger]> Logger sink for Playwright logging. Optional.
@ -87,6 +88,12 @@ This methods attaches Playwright to an existing browser instance.
A browser websocket endpoint to connect to. A browser websocket endpoint to connect to.
### param: BrowserType.connect.headers
* langs: java, python
- `headers` <[string]>
Additional HTTP headers to be sent with web socket connect request. Optional.
### option: BrowserType.connect.slowMo ### option: BrowserType.connect.slowMo
* langs: java, python * langs: java, python
- `slowMo` <[float]> - `slowMo` <[float]>
@ -117,6 +124,7 @@ Connecting over the Chrome DevTools Protocol is only supported for Chromium-base
* langs: js * langs: js
- `params` <[Object]> - `params` <[Object]>
- `endpointURL` <[string]> A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`. - `endpointURL` <[string]> A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`.
- `headers` <[Object]<[string], [string]>> Additional HTTP headers to be sent with connect request. Optional.
- `slowMo` <[float]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you - `slowMo` <[float]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you
can see what is going on. Defaults to 0. can see what is going on. Defaults to 0.
- `logger` <[Logger]> Logger sink for Playwright logging. Optional. - `logger` <[Logger]> Logger sink for Playwright logging. Optional.
@ -129,6 +137,12 @@ Connecting over the Chrome DevTools Protocol is only supported for Chromium-base
A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`. A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`.
### option: BrowserType.connectOverCDP.headers
* langs: java, python
- `headers` <[Object]<[string], [string]>>
Additional HTTP headers to be sent with connect request. Optional.
### option: BrowserType.connectOverCDP.slowMo ### option: BrowserType.connectOverCDP.slowMo
* langs: java, python * langs: java, python
- `slowMo` <[float]> - `slowMo` <[float]>

View File

@ -26,7 +26,7 @@ import { Events } from './events';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { envObjectToArray } from './clientHelper'; import { envObjectToArray } from './clientHelper';
import { assert, makeWaitForNextTask } from '../utils/utils'; import { assert, headersObjectToArray, makeWaitForNextTask } from '../utils/utils';
import { kBrowserClosedError } from '../utils/errors'; import { kBrowserClosedError } from '../utils/errors';
import * as api from '../../types/types'; import * as api from '../../types/types';
import type { Playwright } from './playwright'; import type { Playwright } from './playwright';
@ -117,6 +117,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
perMessageDeflate: false, perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb, maxPayload: 256 * 1024 * 1024, // 256Mb,
handshakeTimeout: this._timeoutSettings.timeout(params), handshakeTimeout: this._timeoutSettings.timeout(params),
headers: params.headers,
}); });
// The 'ws' module in node sometimes sends us multiple messages in a single task. // The 'ws' module in node sometimes sends us multiple messages in a single task.
@ -133,9 +134,14 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
ws.send(JSON.stringify(message)); ws.send(JSON.stringify(message));
}; };
ws.addEventListener('message', event => { ws.addEventListener('message', event => {
waitForNextTask(() => connection.dispatch(JSON.parse(event.data))); waitForNextTask(() => {
try {
connection.dispatch(JSON.parse(event.data));
} catch (e) {
ws.close();
}
});
}); });
return await new Promise<Browser>(async (fulfill, reject) => { return await new Promise<Browser>(async (fulfill, reject) => {
if ((params as any).__testHookBeforeCreateBrowser) { if ((params as any).__testHookBeforeCreateBrowser) {
try { try {
@ -193,9 +199,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
throw new Error('Connecting over CDP is only supported in Chromium.'); throw new Error('Connecting over CDP is only supported in Chromium.');
const logger = params.logger; const logger = params.logger;
return this._wrapApiCall('browserType.connectOverCDP', async (channel: channels.BrowserTypeChannel) => { return this._wrapApiCall('browserType.connectOverCDP', async (channel: channels.BrowserTypeChannel) => {
const headers = params.headers ? headersObjectToArray(params.headers) : undefined;
const result = await channel.connectOverCDP({ const result = await channel.connectOverCDP({
sdkLanguage: 'javascript', sdkLanguage: 'javascript',
endpointURL: 'endpointURL' in params ? params.endpointURL : params.wsEndpoint, endpointURL: 'endpointURL' in params ? params.endpointURL : params.wsEndpoint,
headers,
slowMo: params.slowMo, slowMo: params.slowMo,
timeout: params.timeout timeout: params.timeout
}); });

View File

@ -70,6 +70,7 @@ export type LaunchPersistentContextOptions = Omit<LaunchOptionsBase & BrowserCon
export type ConnectOptions = { export type ConnectOptions = {
wsEndpoint: string, wsEndpoint: string,
headers?: { [key: string]: string; };
slowMo?: number, slowMo?: number,
timeout?: number, timeout?: number,
logger?: Logger, logger?: Logger,

View File

@ -401,10 +401,12 @@ export type BrowserTypeLaunchPersistentContextResult = {
export type BrowserTypeConnectOverCDPParams = { export type BrowserTypeConnectOverCDPParams = {
sdkLanguage: string, sdkLanguage: string,
endpointURL: string, endpointURL: string,
headers?: NameValue[],
slowMo?: number, slowMo?: number,
timeout?: number, timeout?: number,
}; };
export type BrowserTypeConnectOverCDPOptions = { export type BrowserTypeConnectOverCDPOptions = {
headers?: NameValue[],
slowMo?: number, slowMo?: number,
timeout?: number, timeout?: number,
}; };

View File

@ -415,6 +415,9 @@ BrowserType:
parameters: parameters:
sdkLanguage: string sdkLanguage: string
endpointURL: string endpointURL: string
headers:
type: array?
items: NameValue
slowMo: number? slowMo: number?
timeout: number? timeout: number?
returns: returns:

View File

@ -249,6 +249,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.BrowserTypeConnectOverCDPParams = tObject({ scheme.BrowserTypeConnectOverCDPParams = tObject({
sdkLanguage: tString, sdkLanguage: tString,
endpointURL: tString, endpointURL: tString,
headers: tOptional(tArray(tType('NameValue'))),
slowMo: tOptional(tNumber), slowMo: tOptional(tNumber),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
}); });

View File

@ -25,7 +25,7 @@ import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../tra
import { CRDevTools } from './crDevTools'; import { CRDevTools } from './crDevTools';
import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
import * as types from '../types'; import * as types from '../types';
import { debugMode } from '../../utils/utils'; import { debugMode, headersArrayToObject } from '../../utils/utils';
import { RecentLogsCollector } from '../../utils/debugLogger'; import { RecentLogsCollector } from '../../utils/debugLogger';
import { ProgressController } from '../progress'; import { ProgressController } from '../progress';
import { TimeoutSettings } from '../../utils/timeoutSettings'; import { TimeoutSettings } from '../../utils/timeoutSettings';
@ -50,12 +50,15 @@ export class Chromium extends BrowserType {
return super.executablePath(channel); return super.executablePath(channel);
} }
async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, sdkLanguage: string }, timeout?: number) { async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, sdkLanguage: string, headers?: types.HeadersArray }, timeout?: number) {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
controller.setLogName('browser'); controller.setLogName('browser');
const browserLogsCollector = new RecentLogsCollector(); const browserLogsCollector = new RecentLogsCollector();
return controller.run(async progress => { return controller.run(async progress => {
const chromeTransport = await WebSocketTransport.connect(progress, await urlToWSEndpoint(endpointURL)); let headersMap: { [key: string]: string; } | undefined;
if (options.headers)
headersMap = headersArrayToObject(options.headers, false);
const chromeTransport = await WebSocketTransport.connect(progress, await urlToWSEndpoint(endpointURL), headersMap);
const browserProcess: BrowserProcess = { const browserProcess: BrowserProcess = {
close: async () => { close: async () => {
await chromeTransport.closeAndWait(); await chromeTransport.closeAndWait();

View File

@ -52,9 +52,9 @@ export class WebSocketTransport implements ConnectionTransport {
onclose?: () => void; onclose?: () => void;
readonly wsEndpoint: string; readonly wsEndpoint: string;
static async connect(progress: Progress, url: string): Promise<WebSocketTransport> { static async connect(progress: Progress, url: string, headers?: { [key: string]: string; }): Promise<WebSocketTransport> {
progress.log(`<ws connecting> ${url}`); progress.log(`<ws connecting> ${url}`);
const transport = new WebSocketTransport(progress, url); const transport = new WebSocketTransport(progress, url, headers);
let success = false; let success = false;
progress.cleanupWhenAborted(async () => { progress.cleanupWhenAborted(async () => {
if (!success) if (!success)
@ -75,12 +75,13 @@ export class WebSocketTransport implements ConnectionTransport {
return transport; return transport;
} }
constructor(progress: Progress, url: string) { constructor(progress: Progress, url: string, headers?: { [key: string]: string; }) {
this.wsEndpoint = url; this.wsEndpoint = url;
this._ws = new WebSocket(url, [], { this._ws = new WebSocket(url, [], {
perMessageDeflate: false, perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb, maxPayload: 256 * 1024 * 1024, // 256Mb,
handshakeTimeout: progress.timeUntilDeadline(), handshakeTimeout: progress.timeUntilDeadline(),
headers
}); });
this._progress = progress; this._progress = progress;
// The 'ws' module in node sometimes sends us multiple messages in a single task. // The 'ws' module in node sometimes sends us multiple messages in a single task.
@ -91,8 +92,12 @@ export class WebSocketTransport implements ConnectionTransport {
this._ws.addEventListener('message', event => { this._ws.addEventListener('message', event => {
messageWrap(() => { messageWrap(() => {
try {
if (this.onmessage) if (this.onmessage)
this.onmessage.call(null, JSON.parse(event.data)); this.onmessage.call(null, JSON.parse(event.data));
} catch (e) {
this._ws.close();
}
}); });
}); });

View File

@ -60,6 +60,21 @@ test('should be able to connect two browsers at the same time', async ({browserT
await browser2.close(); await browser2.close();
}); });
test('should send extra headers with connect request', async ({browserType, startRemoteServer, server}) => {
const [request] = await Promise.all([
server.waitForWebSocketConnectionRequest(),
browserType.connect({
wsEndpoint: `ws://localhost:${server.PORT}/ws`,
headers: {
'User-Agent': 'Playwright',
'foo': 'bar',
}
}).catch(() => {})
]);
expect(request.headers['user-agent']).toBe('Playwright');
expect(request.headers['foo']).toBe('bar');
});
test('disconnected event should be emitted when browser is closed or server is closed', async ({browserType, startRemoteServer}) => { test('disconnected event should be emitted when browser is closed or server is closed', async ({browserType, startRemoteServer}) => {
const remoteServer = await startRemoteServer(); const remoteServer = await startRemoteServer();

View File

@ -219,4 +219,34 @@ playwrightTest.describe('chromium', () => {
await browserServer.close(); await browserServer.close();
} }
}); });
playwrightTest('should send extra headers with connect request', async ({browserType, browserOptions, server}, testInfo) => {
{
const [request] = await Promise.all([
server.waitForWebSocketConnectionRequest(),
browserType.connectOverCDP({
wsEndpoint: `ws://localhost:${server.PORT}/ws`,
headers: {
'User-Agent': 'Playwright',
'foo': 'bar',
}
}).catch(() => {})
]);
expect(request.headers['user-agent']).toBe('Playwright');
expect(request.headers['foo']).toBe('bar');
}
{
const [request] = await Promise.all([
server.waitForWebSocketConnectionRequest(),
browserType.connectOverCDP({
endpointURL: `ws://localhost:${server.PORT}/ws`,
headers: {
'User-Agent': 'Playwright',
'foo': 'bar',
}
}).catch(() => {})
]);
expect(request.headers['user-agent']).toBe('Playwright');
expect(request.headers['foo']).toBe('bar');
}
});
}); });

10
types/types.d.ts vendored
View File

@ -10877,6 +10877,11 @@ export interface ConnectOverCDPOptions {
*/ */
endpointURL: string; endpointURL: string;
/**
* Additional HTTP headers to be sent with connect request. Optional.
*/
headers?: { [key: string]: string; };
/** /**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0. * Defaults to 0.
@ -10901,6 +10906,11 @@ export interface ConnectOptions {
*/ */
wsEndpoint: string; wsEndpoint: string;
/**
* Additional HTTP headers to be sent with web socket connect request. Optional.
*/
headers?: { [key: string]: string; };
/** /**
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
* Defaults to 0. * Defaults to 0.

View File

@ -28,6 +28,7 @@ export class TestServer {
setRoute(path: string, handler: (message: IncomingMessage & {postBody: Buffer}, response: ServerResponse) => void); setRoute(path: string, handler: (message: IncomingMessage & {postBody: Buffer}, response: ServerResponse) => void);
setRedirect(from: string, to: string); setRedirect(from: string, to: string);
waitForRequest(path: string): Promise<IncomingMessage & {postBody: Buffer}>; waitForRequest(path: string): Promise<IncomingMessage & {postBody: Buffer}>;
waitForWebSocketConnectionRequest(): Promise<IncomingMessage>;
reset(); reset();
serveFile(request: IncomingMessage, response: ServerResponse); serveFile(request: IncomingMessage, response: ServerResponse);
serveFile(request: IncomingMessage, response: ServerResponse, filePath: string); serveFile(request: IncomingMessage, response: ServerResponse, filePath: string);

View File

@ -293,6 +293,12 @@ class TestServer {
} }
} }
waitForWebSocketConnectionRequest() {
return new Promise(fullfil => {
this._wsServer.once('connection', (ws, req) => fullfil(req));
});
}
_onWebSocketConnection(ws) { _onWebSocketConnection(ws) {
ws.send('incoming'); ws.send('incoming');
} }