From 7d68a786cb9a8b1b196f1bbdbb945d178f13f61c Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 12 Feb 2026 22:39:46 -0800 Subject: [PATCH 1/2] chore: add _connectOverCDPTransport method (#39251) --- .../playwright-core/src/client/browserType.ts | 11 +++++++ .../playwright-core/src/protocol/validator.ts | 7 +++++ .../src/protocol/validatorPrimitives.ts | 3 +- .../playwright-core/src/server/browserType.ts | 4 +++ .../src/server/chromium/chromium.ts | 20 +++++++++---- .../dispatchers/browserTypeDispatcher.ts | 9 ++++++ .../src/utils/isomorphic/protocolMetainfo.ts | 1 + packages/protocol/src/channels.d.ts | 11 +++++++ packages/protocol/src/protocol.yml | 8 +++++ .../library/chromium/connect-over-cdp.spec.ts | 30 +++++++++++++++++++ 10 files changed, 97 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 45be8e2f20166..0a41e3492a1cb 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -209,4 +209,15 @@ export class BrowserType extends ChannelOwner imple await this._instrumentation.runAfterCreateBrowserContext(BrowserContext.from(result.defaultContext)); return browser; } + + async _connectOverCDPTransport(transport: /* ConnectionTransport */ any) { + if (this.name() !== 'chromium') + throw new Error('Connecting over CDP is only supported in Chromium.'); + const result = await this._channel.connectOverCDPTransport({ transport }); + const browser = Browser.from(result.browser); + browser._connectToBrowserType(this, {}, undefined); + if (result.defaultContext) + await this._instrumentation.runAfterCreateBrowserContext(BrowserContext.from(result.defaultContext)); + return browser; + } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index bc93e70939146..06622ef91f8a3 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -630,6 +630,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.BrowserInitializer = tObject({ version: tString, name: tString, diff --git a/packages/playwright-core/src/protocol/validatorPrimitives.ts b/packages/playwright-core/src/protocol/validatorPrimitives.ts index 3c96c0748f8de..d72243957204a 100644 --- a/packages/playwright-core/src/protocol/validatorPrimitives.ts +++ b/packages/playwright-core/src/protocol/validatorPrimitives.ts @@ -84,7 +84,8 @@ export const tBinary: Validator = (arg: any, path: string, context: ValidatorCon return (arg as Buffer).toString('base64'); } if (context.binary === 'buffer') { - if (!(arg instanceof Buffer)) + // TODO: support custom binary types. + if (!(arg instanceof Buffer) && !(arg instanceof Object)) throw new ValidationError(`${path}: expected Buffer, got ${typeof arg}`); return arg; } diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index a90f4be5f8cad..2f7a89fa3d203 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -283,6 +283,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 _launchWithSeleniumHub(progress: Progress, hubUrl: string, options: types.LaunchOptions): Promise { throw new Error('Connecting to SELENIUM_REMOTE_URL is 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 39bd23fbf5702..f00d7f090904e 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -89,6 +89,13 @@ export class Chromium extends BrowserType { else if (headersMap && !Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent')) headersMap['User-Agent'] = getUserAgent(); + const wsEndpoint = await urlToWSEndpoint(progress, endpointURL, headersMap); + const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: headersMap }); + const closeAndWait = async () => await chromeTransport.closeAndWait(); + return this._connectOverCDPImpl(progress, chromeTransport, closeAndWait, options, onClose); + } + + private async _connectOverCDPImpl(progress: Progress, transport: ConnectionTransport, closeAndWait: () => Promise, options: types.LaunchOptions & { isLocal?: boolean }, onClose?: () => Promise) { const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER)); const doCleanup = async () => { await removeFolders([artifactsDir]); @@ -97,16 +104,12 @@ export class Chromium extends BrowserType { await cb?.(); }; - let chromeTransport: WebSocketTransport | undefined; const doClose = async () => { - await chromeTransport?.closeAndWait(); + await closeAndWait(); await doCleanup(); }; try { - const wsEndpoint = await urlToWSEndpoint(progress, endpointURL, headersMap); - chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: headersMap }); - const browserProcess: BrowserProcess = { close: doClose, kill: doClose }; const persistent: types.BrowserContextOptions = { noDefaultViewport: true }; const browserOptions: BrowserOptions = { @@ -123,7 +126,7 @@ export class Chromium extends BrowserType { originalLaunchOptions: {}, }; validateBrowserContextOptions(persistent, browserOptions); - const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions)); + const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, transport, browserOptions)); if (!options.isLocal) browser._isCollocatedWithServer = false; browser.on(Browser.Events.Disconnected, doCleanup); @@ -134,6 +137,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 }); + } + private _createDevTools() { // TODO: this is totally wrong when using channels. const directory = registry.findExecutable('chromium').directory; diff --git a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts index 80d92b7e821d0..5c28410a6a3a4 100644 --- a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts @@ -63,4 +63,13 @@ 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 }; + } } diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index 974e31bc335ed..f21f37956abc6 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -50,6 +50,7 @@ export const methodMetainfo = new Map; launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, progress?: Progress): Promise; connectOverCDP(params: BrowserTypeConnectOverCDPParams, progress?: Progress): Promise; + connectOverCDPTransport(params: BrowserTypeConnectOverCDPTransportParams, progress?: Progress): Promise; } export type BrowserTypeLaunchParams = { channel?: string, @@ -1126,6 +1127,16 @@ export type BrowserTypeConnectOverCDPResult = { browser: BrowserChannel, defaultContext?: BrowserContextChannel, }; +export type BrowserTypeConnectOverCDPTransportParams = { + transport: Binary, +}; +export type BrowserTypeConnectOverCDPTransportOptions = { + +}; +export type BrowserTypeConnectOverCDPTransportResult = { + browser: BrowserChannel, + defaultContext?: BrowserContextChannel, +}; export interface BrowserTypeEvents { } diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 8ae8ed0a32f3d..defad2e499f64 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1006,6 +1006,14 @@ BrowserType: browser: Browser defaultContext: BrowserContext? + connectOverCDPTransport: + title: Connect over CDP transport + parameters: + transport: binary + returns: + browser: Browser + defaultContext: BrowserContext? + Browser: type: interface diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts index 408e8262a6a1b..cf8e27dda9586 100644 --- a/tests/library/chromium/connect-over-cdp.spec.ts +++ b/tests/library/chromium/connect-over-cdp.spec.ts @@ -19,6 +19,7 @@ import { playwrightTest as test, expect } from '../../config/browserTest'; import http from 'http'; import fs from 'fs'; import { getUserAgent } from '../../../packages/playwright-core/lib/server/utils/userAgent'; +import { WebSocketTransport } from '../../../packages/playwright-core/lib/server/transport'; import { suppressCertificateWarning } from '../../config/utils'; import type { Frame } from '../../../packages/playwright-core/lib/server/frames'; @@ -643,3 +644,32 @@ test('should get title and URL of existing page', async ({ browserType, mode, se await browserServer.close(); } }); + +test('should connect over CDP using a ConnectionTransport', async ({ browserType, mode, server }, testInfo) => { + test.skip(mode !== 'default', '_connectOverCDPTransport 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 as any)._connectOverCDPTransport(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(); + } +}); From fed4a0acabd59ccf0b3d300e72a461daebf0b9d5 Mon Sep 17 00:00:00 2001 From: Holger Benl Date: Fri, 13 Feb 2026 11:29:11 +0100 Subject: [PATCH 2/2] test(bidi): always install chromium in CI (#39258) --- .github/workflows/tests_bidi.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests_bidi.yml b/.github/workflows/tests_bidi.yml index 191bf28709167..7ab656dbda8ea 100644 --- a/.github/workflows/tests_bidi.yml +++ b/.github/workflows/tests_bidi.yml @@ -53,7 +53,6 @@ jobs: # Needed for video tests - run: npx playwright install ffmpeg - run: npx playwright install --with-deps chromium - if: matrix.channel == 'bidi-chromium' - if: matrix.channel == 'moz-firefox-nightly' run: | echo "BIDI_FFPATH=$(npx -y @puppeteer/browsers install firefox@nightly | tail -n1 | sed 's/^[^ ]* *//')" >> "$GITHUB_ENV"