From f7e3446751d63780a841a80696abb6a3400e86f3 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 22 May 2026 12:24:38 -0700 Subject: [PATCH 1/2] feat(browserType): allow passing a ConnectionTransport to connectOverCDP Adds a new overload to `BrowserType.connectOverCDP` that accepts a `ConnectionTransport` directly. The client detects the transport via duck-typing (presence of `send` and `close` methods) and dispatches through a separate internal channel method to the chromium-only implementation. The `ConnectionTransport` interface is only defined in the type overrides, not in the docs. This restores the functionality removed in #40195, but exposed via the existing public `connectOverCDP` method instead of a new method. --- packages/isomorphic/protocolMetainfo.ts | 1 + packages/playwright-client/types/types.d.ts | 33 +++++++++++++++++++ .../playwright-core/src/client/browserType.ts | 27 ++++++++++++--- .../playwright-core/src/protocol/validator.ts | 7 ++++ .../playwright-core/src/server/browserType.ts | 4 +++ .../src/server/chromium/chromium.ts | 5 +++ .../dispatchers/browserTypeDispatcher.ts | 9 +++++ packages/playwright-core/types/types.d.ts | 33 +++++++++++++++++++ packages/protocol/spec/browserType.yml | 8 +++++ packages/protocol/src/channels.d.ts | 11 +++++++ .../library/chromium/connect-over-cdp.spec.ts | 31 ++++++++++++++++- utils/generate_types/index.js | 3 ++ utils/generate_types/overrides.d.ts | 8 +++++ 13 files changed, 174 insertions(+), 6 deletions(-) diff --git a/packages/isomorphic/protocolMetainfo.ts b/packages/isomorphic/protocolMetainfo.ts index b0e779d1b061a..e99ac29133016 100644 --- a/packages/isomorphic/protocolMetainfo.ts +++ b/packages/isomorphic/protocolMetainfo.ts @@ -115,6 +115,7 @@ export const methodMetainfo = new Map([ ['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', }], diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 7a032da7ad9a6..85617e01e86a5 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -15414,6 +15414,32 @@ export interface BrowserType { * @param options */ connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; + /** + * 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; /** * This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js. * @@ -16194,6 +16220,13 @@ export interface BrowserType { 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. diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 3a98bb8120099..9125aa9e1d868 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -138,12 +138,15 @@ export class BrowserType extends ChannelOwner imple async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise; - async connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string, options?: api.ConnectOverCDPOptions) { - if (typeof endpointURLOrOptions === 'string') - return await this._connectOverCDP(endpointURLOrOptions, options); - const endpointURL = 'endpointURL' in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint; + async connectOverCDP(transport: api.ConnectionTransport): Promise; + async connectOverCDP(endpointURLOrOptionsOrTransport: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string|api.ConnectionTransport, options?: api.ConnectOverCDPOptions) { + if (typeof endpointURLOrOptionsOrTransport === 'string') + return await this._connectOverCDP(endpointURLOrOptionsOrTransport, options); + if (isConnectionTransport(endpointURLOrOptionsOrTransport)) + return await this._connectOverCDPTransport(endpointURLOrOptionsOrTransport); + const endpointURL = 'endpointURL' in endpointURLOrOptionsOrTransport ? endpointURLOrOptionsOrTransport.endpointURL : endpointURLOrOptionsOrTransport.wsEndpoint; assert(endpointURL, 'Cannot connect over CDP without wsEndpoint.'); - return await this.connectOverCDP(endpointURL, endpointURLOrOptions); + return await this.connectOverCDP(endpointURL, endpointURLOrOptionsOrTransport); } async _connectOverCDP(endpointURL: string, params: api.ConnectOverCDPOptions = {}): Promise { @@ -176,4 +179,18 @@ export class BrowserType extends ChannelOwner imple return Worker.from(result.worker); } + async _connectOverCDPTransport(transport: api.ConnectionTransport): Promise { + 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 }); + const browser = Browser.from(result.browser); + browser._connectToBrowserType(this, {}, undefined); + if (result.defaultContext) + await this._instrumentation.runAfterCreateBrowserContext(BrowserContext.from(result.defaultContext)); + return browser; + } +} + +function isConnectionTransport(value: any): value is api.ConnectionTransport { + return !!value && typeof value === 'object' && typeof value.send === 'function' && typeof value.close === 'function'; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index af1c8ecc2b90b..1e39b28725483 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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, diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 8096c9eedab47..d9a1e34f524d4 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -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 { + throw new Error('CDP connections are only supported by Chromium'); + } + async connectToWorker(progress: Progress, endpoint: string): Promise { throw new Error('CDP connections are only supported by Chromium'); } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index e0b4d362590d8..063701c82b144 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -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); diff --git a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts index bd5c7a3fe0d87..40633088d5d1e 100644 --- a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts @@ -65,6 +65,15 @@ export class BrowserTypeDispatcher extends Dispatcher { + 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 { if (this._denyLaunch) throw new Error(`Launching more browsers is not allowed.`); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 7a032da7ad9a6..85617e01e86a5 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -15414,6 +15414,32 @@ export interface BrowserType { * @param options */ connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; + /** + * 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; /** * This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js. * @@ -16194,6 +16220,13 @@ export interface BrowserType { 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. diff --git a/packages/protocol/spec/browserType.yml b/packages/protocol/spec/browserType.yml index b86f06b7cfde9..022ed15aa3123 100644 --- a/packages/protocol/spec/browserType.yml +++ b/packages/protocol/spec/browserType.yml @@ -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: diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index c2f9cea282714..a4dd1e2bd47d9 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1822,6 +1822,7 @@ export interface BrowserTypeChannel extends BrowserTypeEventTarget, Channel { launch(params: BrowserTypeLaunchParams, progress?: Progress): Promise; launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, progress?: Progress): Promise; connectOverCDP(params: BrowserTypeConnectOverCDPParams, progress?: Progress): Promise; + connectOverCDPTransport(params: BrowserTypeConnectOverCDPTransportParams, progress?: Progress): Promise; connectToWorker(params: BrowserTypeConnectToWorkerParams, progress?: Progress): Promise; } export type BrowserTypeLaunchParams = { @@ -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, diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts index 52397b0d02030..29c6e99f5d87c 100644 --- a/tests/library/chromium/connect-over-cdp.spec.ts +++ b/tests/library/chromium/connect-over-cdp.spec.ts @@ -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) => { @@ -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((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(); + } +}); diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 2274c79577616..09ca058974f55 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -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'); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 03ca88049c18c..687f620e36984 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -216,6 +216,7 @@ export interface BrowserType { * @deprecated */ connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; + connectOverCDP(transport: ConnectionTransport): Promise; connect(wsEndpoint: string, options?: ConnectOptions): Promise; /** * wsEndpoint in options is deprecated. Instead use `wsEndpoint`. @@ -226,6 +227,13 @@ export interface BrowserType { connect(options: ConnectOptions & { wsEndpoint?: string }): Promise; } +export interface ConnectionTransport { + send(message: object): void; + close(): void; + onmessage?: (message: object) => void; + onclose?: (reason?: string) => void; +} + export interface CDPSession { on(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; addListener(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; From d4deba004e00ebc4872463a966b06d8e5b8778e5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 22 May 2026 12:31:31 -0700 Subject: [PATCH 2/2] chore(browserType): clean up connectOverCDP client helpers - Extract `_browserFromConnectResult` to share the post-channel Browser wiring between `_connectOverCDP` and `_connectOverCDPTransport`. - Replace the recursive `connectOverCDP` call in the options branch with a direct `_connectOverCDP` call. - Shorten the parameter name back to `endpointURLOrOptions`. --- .../playwright-core/src/client/browserType.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 9125aa9e1d868..5cbc931d710fc 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -139,14 +139,14 @@ export class BrowserType extends ChannelOwner imple async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise; async connectOverCDP(transport: api.ConnectionTransport): Promise; - async connectOverCDP(endpointURLOrOptionsOrTransport: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string|api.ConnectionTransport, options?: api.ConnectOverCDPOptions) { - if (typeof endpointURLOrOptionsOrTransport === 'string') - return await this._connectOverCDP(endpointURLOrOptionsOrTransport, options); - if (isConnectionTransport(endpointURLOrOptionsOrTransport)) - return await this._connectOverCDPTransport(endpointURLOrOptionsOrTransport); - const endpointURL = 'endpointURL' in endpointURLOrOptionsOrTransport ? endpointURLOrOptionsOrTransport.endpointURL : endpointURLOrOptionsOrTransport.wsEndpoint; + 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, endpointURLOrOptionsOrTransport); + return await this._connectOverCDP(endpointURL, endpointURLOrOptions); } async _connectOverCDP(endpointURL: string, params: api.ConnectOverCDPOptions = {}): Promise { @@ -162,6 +162,17 @@ export class BrowserType extends ChannelOwner imple noDefaults: params.noDefaults, artifactsDir: params.artifactsDir, }); + return await this._browserFromConnectResult(result); + } + + async _connectOverCDPTransport(transport: api.ConnectionTransport): Promise { + 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 { const browser = Browser.from(result.browser); browser._connectToBrowserType(this, {}, undefined); if (result.defaultContext) @@ -178,17 +189,6 @@ export class BrowserType extends ChannelOwner imple }); return Worker.from(result.worker); } - - async _connectOverCDPTransport(transport: api.ConnectionTransport): Promise { - 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 }); - const browser = Browser.from(result.browser); - browser._connectToBrowserType(this, {}, undefined); - if (result.defaultContext) - await this._instrumentation.runAfterCreateBrowserContext(BrowserContext.from(result.defaultContext)); - return browser; - } } function isConnectionTransport(value: any): value is api.ConnectionTransport {