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: 0 additions & 1 deletion .github/workflows/tests_bidi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,15 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> 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;
}
}
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 @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion packages/playwright-core/src/protocol/validatorPrimitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
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 @@ -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<Browser> {
throw new Error('CDP connections are only supported by Chromium');
}

async _launchWithSeleniumHub(progress: Progress, hubUrl: string, options: types.LaunchOptions): Promise<Browser> {
throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium');
}
Expand Down
20 changes: 14 additions & 6 deletions packages/playwright-core/src/server/chromium/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>, options: types.LaunchOptions & { isLocal?: boolean }, onClose?: () => Promise<void>) {
const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER));
const doCleanup = async () => {
await removeFolders([artifactsDir]);
Expand All @@ -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 = {
Expand All @@ -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);
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,13 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
defaultContext: browser._defaultContext ? BrowserContextDispatcher.from(browserDispatcher, browser._defaultContext) : undefined,
};
}

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 };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
['BrowserType.launch', { title: 'Launch browser', }],
['BrowserType.launchPersistentContext', { title: 'Launch persistent context', }],
['BrowserType.connectOverCDP', { title: 'Connect over CDP', }],
['BrowserType.connectOverCDPTransport', { title: 'Connect over CDP transport', }],
['Browser.close', { title: 'Close browser', pausesBeforeAction: true, }],
['Browser.killForTests', { internal: true, }],
['Browser.defaultUserAgentForTest', { internal: true, }],
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 @@ -887,6 +887,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>;
}
export type BrowserTypeLaunchParams = {
channel?: string,
Expand Down Expand Up @@ -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 {
}
Expand Down
8 changes: 8 additions & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 30 additions & 0 deletions tests/library/chromium/connect-over-cdp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<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 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();
}
});
Loading