Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/isomorphic/protocolMetainfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export const methodMetainfo = new Map<string, MethodMetainfo>([
['BrowserType.launch', { title: 'Launch browser', }],
['BrowserType.launchPersistentContext', { title: 'Launch persistent context', }],
['BrowserType.connectOverCDP', { title: 'Connect over CDP', }],
['BrowserType.connectOverCDPTransport', { title: 'Connect over CDP transport', }],
['BrowserType.connectToWorker', { title: 'Connect to worker', }],
['Disposable.dispose', { internal: true, potentiallyClosesScope: true, }],
['Electron.launch', { title: 'Launch electron', }],
Expand Down
33 changes: 33 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15414,6 +15414,32 @@ export interface BrowserType<Unused = {}> {
* @param options
*/
connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<Browser>;
/**
* This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
*
* The default browser context is accessible via
* [browser.contexts()](https://playwright.dev/docs/api/class-browser#browser-contexts).
*
* **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
* **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via
* [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
* If you are experiencing issues or attempting to use advanced functionality, you probably want to use
* [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
*
* **Usage**
*
* ```js
* const browser = await playwright.chromium.connectOverCDP('http://localhost:9222');
* const defaultContext = browser.contexts()[0];
* const page = defaultContext.pages()[0];
* ```
*
* @param endpointURL 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`.
* @param options
*/
connectOverCDP(transport: ConnectionTransport): Promise<Browser>;
/**
* This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js.
*
Expand Down Expand Up @@ -16194,6 +16220,13 @@ export interface BrowserType<Unused = {}> {
name(): string;
}

export interface ConnectionTransport {
send(message: object): void;
close(): void;
onmessage?: (message: object) => void;
onclose?: (reason?: string) => void;
}

/**
* The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
* - protocol methods can be called with `session.send` method.
Expand Down
21 changes: 19 additions & 2 deletions packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,15 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple

async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<api.Browser>;
async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise<api.Browser>;
async connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string, options?: api.ConnectOverCDPOptions) {
async connectOverCDP(transport: api.ConnectionTransport): Promise<api.Browser>;
async connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string|api.ConnectionTransport, options?: api.ConnectOverCDPOptions) {
if (typeof endpointURLOrOptions === 'string')
return await this._connectOverCDP(endpointURLOrOptions, options);
if (isConnectionTransport(endpointURLOrOptions))
return await this._connectOverCDPTransport(endpointURLOrOptions);
const endpointURL = 'endpointURL' in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint;
assert(endpointURL, 'Cannot connect over CDP without wsEndpoint.');
return await this.connectOverCDP(endpointURL, endpointURLOrOptions);
return await this._connectOverCDP(endpointURL, endpointURLOrOptions);
}

async _connectOverCDP(endpointURL: string, params: api.ConnectOverCDPOptions = {}): Promise<Browser> {
Expand All @@ -159,6 +162,17 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
noDefaults: params.noDefaults,
artifactsDir: params.artifactsDir,
});
return await this._browserFromConnectResult(result);
}

async _connectOverCDPTransport(transport: api.ConnectionTransport): Promise<Browser> {
if (this.name() !== 'chromium')
throw new Error('Connecting over CDP is only supported in Chromium.');
const result = await this._channel.connectOverCDPTransport({ transport: transport as any });
return await this._browserFromConnectResult(result);
}

private async _browserFromConnectResult(result: { browser: channels.BrowserChannel, defaultContext?: channels.BrowserContextChannel }): Promise<Browser> {
const browser = Browser.from(result.browser);
browser._connectToBrowserType(this, {}, undefined);
if (result.defaultContext)
Expand All @@ -175,5 +189,8 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
});
return Worker.from(result.worker);
}
}

function isConnectionTransport(value: any): value is api.ConnectionTransport {
return !!value && typeof value === 'object' && typeof value.send === 'function' && typeof value.close === 'function';
}
7 changes: 7 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,13 @@ scheme.BrowserTypeConnectOverCDPResult = tObject({
browser: tChannel(['Browser']),
defaultContext: tOptional(tChannel(['BrowserContext'])),
});
scheme.BrowserTypeConnectOverCDPTransportParams = tObject({
transport: tBinary,
});
scheme.BrowserTypeConnectOverCDPTransportResult = tObject({
browser: tChannel(['Browser']),
defaultContext: tOptional(tChannel(['BrowserContext'])),
});
scheme.BrowserTypeConnectToWorkerParams = tObject({
endpoint: tString,
timeout: tFloat,
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/server/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ export abstract class BrowserType extends SdkObject {
throw new Error('CDP connections are only supported by Chromium');
}

async connectOverCDPTransport(progress: Progress, transport: ConnectionTransport): Promise<Browser> {
throw new Error('CDP connections are only supported by Chromium');
}

async connectToWorker(progress: Progress, endpoint: string): Promise<Worker> {
throw new Error('CDP connections are only supported by Chromium');
}
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/server/chromium/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ export class Chromium extends BrowserType {
}
}

override async connectOverCDPTransport(progress: Progress, transport: ConnectionTransport) {
const closeAndWait = async () => transport.close();
return this._connectOverCDPImpl(progress, transport, closeAndWait, { isLocal: true });
}

override async connectToWorker(progress: Progress, endpoint: string) {
const wsEndpoint = await urlToWSEndpoint(progress, endpoint, {});
const transport = await WebSocketTransport.connect(progress, wsEndpoint);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
};
}

async connectOverCDPTransport(params: channels.BrowserTypeConnectOverCDPTransportParams, progress: Progress): Promise<channels.BrowserTypeConnectOverCDPTransportResult> {
if (this._denyLaunch)
throw new Error(`Launching more browsers is not allowed.`);

const browser = await this._object.connectOverCDPTransport(progress, params.transport as any);
const browserDispatcher = new BrowserDispatcher(this, browser);
return { browser: browserDispatcher, defaultContext: browser._defaultContext ? BrowserContextDispatcher.from(browserDispatcher, browser._defaultContext) : undefined };
}

async connectToWorker(params: channels.BrowserTypeConnectToWorkerParams, progress: Progress): Promise<channels.BrowserTypeConnectToWorkerResult> {
if (this._denyLaunch)
throw new Error(`Launching more browsers is not allowed.`);
Expand Down
33 changes: 33 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15414,6 +15414,32 @@ export interface BrowserType<Unused = {}> {
* @param options
*/
connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<Browser>;
/**
* This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
*
* The default browser context is accessible via
* [browser.contexts()](https://playwright.dev/docs/api/class-browser#browser-contexts).
*
* **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
* **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via
* [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
* If you are experiencing issues or attempting to use advanced functionality, you probably want to use
* [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
*
* **Usage**
*
* ```js
* const browser = await playwright.chromium.connectOverCDP('http://localhost:9222');
* const defaultContext = browser.contexts()[0];
* const page = defaultContext.pages()[0];
* ```
*
* @param endpointURL 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`.
* @param options
*/
connectOverCDP(transport: ConnectionTransport): Promise<Browser>;
/**
* This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js.
*
Expand Down Expand Up @@ -16194,6 +16220,13 @@ export interface BrowserType<Unused = {}> {
name(): string;
}

export interface ConnectionTransport {
send(message: object): void;
close(): void;
onmessage?: (message: object) => void;
onclose?: (reason?: string) => void;
}

/**
* The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
* - protocol methods can be called with `session.send` method.
Expand Down
8 changes: 8 additions & 0 deletions packages/protocol/spec/browserType.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ BrowserType:
browser: Browser
defaultContext: BrowserContext?

connectOverCDPTransport:
title: Connect over CDP transport
parameters:
transport: binary
returns:
browser: Browser
defaultContext: BrowserContext?

connectToWorker:
title: Connect to worker
parameters:
Expand Down
11 changes: 11 additions & 0 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,7 @@ export interface BrowserTypeChannel extends BrowserTypeEventTarget, Channel {
launch(params: BrowserTypeLaunchParams, progress?: Progress): Promise<BrowserTypeLaunchResult>;
launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, progress?: Progress): Promise<BrowserTypeLaunchPersistentContextResult>;
connectOverCDP(params: BrowserTypeConnectOverCDPParams, progress?: Progress): Promise<BrowserTypeConnectOverCDPResult>;
connectOverCDPTransport(params: BrowserTypeConnectOverCDPTransportParams, progress?: Progress): Promise<BrowserTypeConnectOverCDPTransportResult>;
connectToWorker(params: BrowserTypeConnectToWorkerParams, progress?: Progress): Promise<BrowserTypeConnectToWorkerResult>;
}
export type BrowserTypeLaunchParams = {
Expand Down Expand Up @@ -2078,6 +2079,16 @@ export type BrowserTypeConnectOverCDPResult = {
browser: BrowserChannel,
defaultContext?: BrowserContextChannel,
};
export type BrowserTypeConnectOverCDPTransportParams = {
transport: Binary,
};
export type BrowserTypeConnectOverCDPTransportOptions = {

};
export type BrowserTypeConnectOverCDPTransportResult = {
browser: BrowserChannel,
defaultContext?: BrowserContextChannel,
};
export type BrowserTypeConnectToWorkerParams = {
endpoint: string,
timeout: number,
Expand Down
31 changes: 30 additions & 1 deletion tests/library/chromium/connect-over-cdp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import path from 'path';
import { getUserAgent, server as coreServer } from '../../../packages/playwright-core/lib/coreBundle';
import { suppressCertificateWarning } from '../../config/utils';

const { nullProgress } = coreServer;
const { WebSocketTransport, nullProgress } = coreServer;
type Frame = coreServer.Frame;

test('should connect to an existing cdp session', async ({ browserType, mode }, testInfo) => {
Expand Down Expand Up @@ -750,3 +750,32 @@ test('noDefaults should not affect new contexts', async ({ browserType, mode, se
await browserServer.close();
}
});

test('should connect over CDP using a ConnectionTransport', async ({ browserType, mode, server }, testInfo) => {
test.skip(mode !== 'default', 'Passing a transport to connectOverCDP is only available in-process');

const port = 9339 + testInfo.workerIndex;
const browserServer = await browserType.launch({
args: ['--remote-debugging-port=' + port]
});
try {
const json = await new Promise<string>((resolve, reject) => {
http.get(`http://127.0.0.1:${port}/json/version/`, resp => {
let data = '';
resp.on('data', chunk => data += chunk);
resp.on('end', () => resolve(data));
}).on('error', reject);
});
const wsEndpoint = JSON.parse(json).webSocketDebuggerUrl;
const transport = await WebSocketTransport.connect(undefined, wsEndpoint);
const cdpBrowser = await browserType.connectOverCDP(transport);
const contexts = cdpBrowser.contexts();
expect(contexts.length).toBe(1);
const page = await contexts[0].newPage();
await page.goto(server.EMPTY_PAGE);
expect(page.url()).toBe(server.EMPTY_PAGE);
await cdpBrowser.close();
} finally {
await browserServer.close();
}
});
3 changes: 3 additions & 0 deletions utils/generate_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,9 @@ class TypesGenerator {
doNotGenerate: new Set([
...assertionClasses,
]),
ignoreMissing: new Set([
'ConnectionTransport',
]),
});
let types = await generator.generateTypes(path.join(__dirname, 'overrides.d.ts'));
const namedDevices = Object.keys(devices).map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`).join('\n');
Expand Down
8 changes: 8 additions & 0 deletions utils/generate_types/overrides.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export interface BrowserType<Unused = {}> {
* @deprecated
*/
connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<Browser>;
connectOverCDP(transport: ConnectionTransport): Promise<Browser>;
connect(wsEndpoint: string, options?: ConnectOptions): Promise<Browser>;
/**
* wsEndpoint in options is deprecated. Instead use `wsEndpoint`.
Expand All @@ -226,6 +227,13 @@ export interface BrowserType<Unused = {}> {
connect(options: ConnectOptions & { wsEndpoint?: string }): Promise<Browser>;
}

export interface ConnectionTransport {
send(message: object): void;
close(): void;
onmessage?: (message: object) => void;
onclose?: (reason?: string) => void;
}

export interface CDPSession {
on<T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this;
addListener<T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this;
Expand Down
Loading