Skip to content

Commit 7d68a78

Browse files
authored
chore: add _connectOverCDPTransport method (#39251)
1 parent c4a698f commit 7d68a78

File tree

10 files changed

+97
-7
lines changed

10 files changed

+97
-7
lines changed

packages/playwright-core/src/client/browserType.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,15 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
209209
await this._instrumentation.runAfterCreateBrowserContext(BrowserContext.from(result.defaultContext));
210210
return browser;
211211
}
212+
213+
async _connectOverCDPTransport(transport: /* ConnectionTransport */ any) {
214+
if (this.name() !== 'chromium')
215+
throw new Error('Connecting over CDP is only supported in Chromium.');
216+
const result = await this._channel.connectOverCDPTransport({ transport });
217+
const browser = Browser.from(result.browser);
218+
browser._connectToBrowserType(this, {}, undefined);
219+
if (result.defaultContext)
220+
await this._instrumentation.runAfterCreateBrowserContext(BrowserContext.from(result.defaultContext));
221+
return browser;
222+
}
212223
}

packages/playwright-core/src/protocol/validator.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,13 @@ scheme.BrowserTypeConnectOverCDPResult = tObject({
630630
browser: tChannel(['Browser']),
631631
defaultContext: tOptional(tChannel(['BrowserContext'])),
632632
});
633+
scheme.BrowserTypeConnectOverCDPTransportParams = tObject({
634+
transport: tBinary,
635+
});
636+
scheme.BrowserTypeConnectOverCDPTransportResult = tObject({
637+
browser: tChannel(['Browser']),
638+
defaultContext: tOptional(tChannel(['BrowserContext'])),
639+
});
633640
scheme.BrowserInitializer = tObject({
634641
version: tString,
635642
name: tString,

packages/playwright-core/src/protocol/validatorPrimitives.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ export const tBinary: Validator = (arg: any, path: string, context: ValidatorCon
8484
return (arg as Buffer).toString('base64');
8585
}
8686
if (context.binary === 'buffer') {
87-
if (!(arg instanceof Buffer))
87+
// TODO: support custom binary types.
88+
if (!(arg instanceof Buffer) && !(arg instanceof Object))
8889
throw new ValidationError(`${path}: expected Buffer, got ${typeof arg}`);
8990
return arg;
9091
}

packages/playwright-core/src/server/browserType.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ export abstract class BrowserType extends SdkObject {
283283
throw new Error('CDP connections are only supported by Chromium');
284284
}
285285

286+
async connectOverCDPTransport(progress: Progress, transport: ConnectionTransport): Promise<Browser> {
287+
throw new Error('CDP connections are only supported by Chromium');
288+
}
289+
286290
async _launchWithSeleniumHub(progress: Progress, hubUrl: string, options: types.LaunchOptions): Promise<Browser> {
287291
throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium');
288292
}

packages/playwright-core/src/server/chromium/chromium.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ export class Chromium extends BrowserType {
8989
else if (headersMap && !Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent'))
9090
headersMap['User-Agent'] = getUserAgent();
9191

92+
const wsEndpoint = await urlToWSEndpoint(progress, endpointURL, headersMap);
93+
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: headersMap });
94+
const closeAndWait = async () => await chromeTransport.closeAndWait();
95+
return this._connectOverCDPImpl(progress, chromeTransport, closeAndWait, options, onClose);
96+
}
97+
98+
private async _connectOverCDPImpl(progress: Progress, transport: ConnectionTransport, closeAndWait: () => Promise<void>, options: types.LaunchOptions & { isLocal?: boolean }, onClose?: () => Promise<void>) {
9299
const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER));
93100
const doCleanup = async () => {
94101
await removeFolders([artifactsDir]);
@@ -97,16 +104,12 @@ export class Chromium extends BrowserType {
97104
await cb?.();
98105
};
99106

100-
let chromeTransport: WebSocketTransport | undefined;
101107
const doClose = async () => {
102-
await chromeTransport?.closeAndWait();
108+
await closeAndWait();
103109
await doCleanup();
104110
};
105111

106112
try {
107-
const wsEndpoint = await urlToWSEndpoint(progress, endpointURL, headersMap);
108-
chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: headersMap });
109-
110113
const browserProcess: BrowserProcess = { close: doClose, kill: doClose };
111114
const persistent: types.BrowserContextOptions = { noDefaultViewport: true };
112115
const browserOptions: BrowserOptions = {
@@ -123,7 +126,7 @@ export class Chromium extends BrowserType {
123126
originalLaunchOptions: {},
124127
};
125128
validateBrowserContextOptions(persistent, browserOptions);
126-
const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions));
129+
const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, transport, browserOptions));
127130
if (!options.isLocal)
128131
browser._isCollocatedWithServer = false;
129132
browser.on(Browser.Events.Disconnected, doCleanup);
@@ -134,6 +137,11 @@ export class Chromium extends BrowserType {
134137
}
135138
}
136139

140+
override async connectOverCDPTransport(progress: Progress, transport: ConnectionTransport) {
141+
const closeAndWait = async () => transport.close();
142+
return this._connectOverCDPImpl(progress, transport, closeAndWait, { isLocal: true });
143+
}
144+
137145
private _createDevTools() {
138146
// TODO: this is totally wrong when using channels.
139147
const directory = registry.findExecutable('chromium').directory;

packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,13 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
6363
defaultContext: browser._defaultContext ? BrowserContextDispatcher.from(browserDispatcher, browser._defaultContext) : undefined,
6464
};
6565
}
66+
67+
async connectOverCDPTransport(params: channels.BrowserTypeConnectOverCDPTransportParams, progress: Progress): Promise<channels.BrowserTypeConnectOverCDPTransportResult> {
68+
if (this._denyLaunch)
69+
throw new Error(`Launching more browsers is not allowed.`);
70+
71+
const browser = await this._object.connectOverCDPTransport(progress, params.transport as any);
72+
const browserDispatcher = new BrowserDispatcher(this, browser);
73+
return { browser: browserDispatcher, defaultContext: browser._defaultContext ? BrowserContextDispatcher.from(browserDispatcher, browser._defaultContext) : undefined };
74+
}
6675
}

packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
5050
['BrowserType.launch', { title: 'Launch browser', }],
5151
['BrowserType.launchPersistentContext', { title: 'Launch persistent context', }],
5252
['BrowserType.connectOverCDP', { title: 'Connect over CDP', }],
53+
['BrowserType.connectOverCDPTransport', { title: 'Connect over CDP transport', }],
5354
['Browser.close', { title: 'Close browser', pausesBeforeAction: true, }],
5455
['Browser.killForTests', { internal: true, }],
5556
['Browser.defaultUserAgentForTest', { internal: true, }],

packages/protocol/src/channels.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ export interface BrowserTypeChannel extends BrowserTypeEventTarget, Channel {
887887
launch(params: BrowserTypeLaunchParams, progress?: Progress): Promise<BrowserTypeLaunchResult>;
888888
launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, progress?: Progress): Promise<BrowserTypeLaunchPersistentContextResult>;
889889
connectOverCDP(params: BrowserTypeConnectOverCDPParams, progress?: Progress): Promise<BrowserTypeConnectOverCDPResult>;
890+
connectOverCDPTransport(params: BrowserTypeConnectOverCDPTransportParams, progress?: Progress): Promise<BrowserTypeConnectOverCDPTransportResult>;
890891
}
891892
export type BrowserTypeLaunchParams = {
892893
channel?: string,
@@ -1126,6 +1127,16 @@ export type BrowserTypeConnectOverCDPResult = {
11261127
browser: BrowserChannel,
11271128
defaultContext?: BrowserContextChannel,
11281129
};
1130+
export type BrowserTypeConnectOverCDPTransportParams = {
1131+
transport: Binary,
1132+
};
1133+
export type BrowserTypeConnectOverCDPTransportOptions = {
1134+
1135+
};
1136+
export type BrowserTypeConnectOverCDPTransportResult = {
1137+
browser: BrowserChannel,
1138+
defaultContext?: BrowserContextChannel,
1139+
};
11291140

11301141
export interface BrowserTypeEvents {
11311142
}

packages/protocol/src/protocol.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,14 @@ BrowserType:
10061006
browser: Browser
10071007
defaultContext: BrowserContext?
10081008

1009+
connectOverCDPTransport:
1010+
title: Connect over CDP transport
1011+
parameters:
1012+
transport: binary
1013+
returns:
1014+
browser: Browser
1015+
defaultContext: BrowserContext?
1016+
10091017
Browser:
10101018
type: interface
10111019

tests/library/chromium/connect-over-cdp.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { playwrightTest as test, expect } from '../../config/browserTest';
1919
import http from 'http';
2020
import fs from 'fs';
2121
import { getUserAgent } from '../../../packages/playwright-core/lib/server/utils/userAgent';
22+
import { WebSocketTransport } from '../../../packages/playwright-core/lib/server/transport';
2223
import { suppressCertificateWarning } from '../../config/utils';
2324
import type { Frame } from '../../../packages/playwright-core/lib/server/frames';
2425

@@ -643,3 +644,32 @@ test('should get title and URL of existing page', async ({ browserType, mode, se
643644
await browserServer.close();
644645
}
645646
});
647+
648+
test('should connect over CDP using a ConnectionTransport', async ({ browserType, mode, server }, testInfo) => {
649+
test.skip(mode !== 'default', '_connectOverCDPTransport is only available in-process');
650+
651+
const port = 9339 + testInfo.workerIndex;
652+
const browserServer = await browserType.launch({
653+
args: ['--remote-debugging-port=' + port]
654+
});
655+
try {
656+
const json = await new Promise<string>((resolve, reject) => {
657+
http.get(`http://127.0.0.1:${port}/json/version/`, resp => {
658+
let data = '';
659+
resp.on('data', chunk => data += chunk);
660+
resp.on('end', () => resolve(data));
661+
}).on('error', reject);
662+
});
663+
const wsEndpoint = JSON.parse(json).webSocketDebuggerUrl;
664+
const transport = await WebSocketTransport.connect(undefined, wsEndpoint);
665+
const cdpBrowser = await (browserType as any)._connectOverCDPTransport(transport);
666+
const contexts = cdpBrowser.contexts();
667+
expect(contexts.length).toBe(1);
668+
const page = await contexts[0].newPage();
669+
await page.goto(server.EMPTY_PAGE);
670+
expect(page.url()).toBe(server.EMPTY_PAGE);
671+
await cdpBrowser.close();
672+
} finally {
673+
await browserServer.close();
674+
}
675+
});

0 commit comments

Comments
 (0)