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
6 changes: 6 additions & 0 deletions docs/src/api/class-browsertype.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ emulation is not enabled, and media emulation options (such as [`option: Browser
browser where these overrides would interfere with existing browser state. New contexts created via
[`method: Browser.newContext`] are not affected. Defaults to `false`.

### option: BrowserType.connectOverCDP.artifactsDir
* since: v1.61
- `artifactsDir` <[path]>

If specified, browser artifacts (such as traces and downloads) are saved into this directory.


## method: BrowserType.executablePath
* since: v1.8
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23531,6 +23531,11 @@ export interface LaunchOptions {
}

export interface ConnectOverCDPOptions {
/**
* If specified, browser artifacts (such as traces and downloads) are saved into this directory.
*/
artifactsDir?: string;

/**
* @deprecated Use the first argument instead.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
timeout: new TimeoutSettings(this._platform).timeout(params),
isLocal: params.isLocal,
noDefaults: params.noDefaults,
artifactsDir: params.artifactsDir,
});
const browser = Browser.from(result.browser);
browser._connectToBrowserType(this, {}, undefined);
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,7 @@ scheme.BrowserTypeConnectOverCDPParams = tObject({
timeout: tFloat,
isLocal: tOptional(tBoolean),
noDefaults: tOptional(tBoolean),
artifactsDir: tOptional(tString),
});
scheme.BrowserTypeConnectOverCDPResult = tObject({
browser: tChannel(['Browser']),
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export abstract class BrowserType extends SdkObject {
}
}

async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, timeout?: number, headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean }): Promise<Browser> {
async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, timeout?: number, headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean, artifactsDir?: string }): Promise<Browser> {
throw new Error('CDP connections are only supported by Chromium');
}

Expand Down
17 changes: 12 additions & 5 deletions packages/playwright-core/src/server/chromium/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ export class Chromium extends BrowserType {
return super.launchPersistentContext(progress, userDataDir, options);
}

override async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean }) {
override async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean, artifactsDir?: string }) {
return await this._connectOverCDPInternal(progress, endpointURL, options);
}

async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean }, onClose?: () => Promise<void>) {
async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray, isLocal?: boolean, noDefaults?: boolean, artifactsDir?: string }, onClose?: () => Promise<void>) {
let headersMap: { [key: string]: string; } | undefined;
if (options.headers)
headersMap = headersArrayToObject(options.headers, false);
Expand Down Expand Up @@ -113,10 +113,17 @@ export class Chromium extends BrowserType {
return this._connectOverCDPImpl(progress, chromeTransport, closeAndWait, options, onClose);
}

private async _connectOverCDPImpl(progress: Progress, transport: ConnectionTransport, closeAndWait: () => Promise<void>, options: types.LaunchOptions & { isLocal?: boolean, noDefaults?: boolean }, onClose?: () => Promise<void>) {
const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER));
private async _connectOverCDPImpl(progress: Progress, transport: ConnectionTransport, closeAndWait: () => Promise<void>, options: types.LaunchOptions & { isLocal?: boolean, noDefaults?: boolean, artifactsDir?: string }, onClose?: () => Promise<void>) {
let artifactsDir: string;
const tempDirectories: string[] = [];
if (options.artifactsDir) {
artifactsDir = options.artifactsDir;
} else {
artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER));
tempDirectories.push(artifactsDir);
}
const doCleanup = async () => {
await removeFolders([artifactsDir]);
await removeFolders(tempDirectories);
const cb = onClose;
onClose = undefined; // Make sure to only call onClose once.
await cb?.();
Expand Down
8 changes: 5 additions & 3 deletions packages/playwright-core/src/tools/mcp/browserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export async function createBrowserWithInfo(config: FullConfig, clientInfo: Clie
let canBind = false;
let ownership: 'attached' | 'own' = 'own';
if (config.browser.cdpEndpoint) {
browser = await createCDPBrowser(config);
browser = await createCDPBrowser(config, clientInfo);
canBind = true;
ownership = 'attached';
} else if (config.browser.isolated) {
Expand Down Expand Up @@ -103,11 +103,13 @@ async function createIsolatedBrowser(config: FullConfig, clientInfo: ClientInfo)
return browser;
}

async function createCDPBrowser(config: FullConfig): Promise<playwrightTypes.Browser> {
async function createCDPBrowser(config: FullConfig, clientInfo: ClientInfo): Promise<playwrightTypes.Browser> {
testDebug('create browser (cdp)');
const artifactsDir = await computeTracesDir(config, clientInfo);
const browser = await playwright.chromium.connectOverCDP(config.browser.cdpEndpoint!, {
headers: config.browser.cdpHeaders,
timeout: config.browser.cdpTimeout
timeout: config.browser.cdpTimeout,
artifactsDir,
});
return browser;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23531,6 +23531,11 @@ export interface LaunchOptions {
}

export interface ConnectOverCDPOptions {
/**
* If specified, browser artifacts (such as traces and downloads) are saved into this directory.
*/
artifactsDir?: string;

/**
* @deprecated Use the first argument instead.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/spec/browserType.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ BrowserType:
timeout: float
isLocal: boolean?
noDefaults: boolean?
artifactsDir: string?
returns:
browser: Browser
defaultContext: BrowserContext?
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2005,12 +2005,14 @@ export type BrowserTypeConnectOverCDPParams = {
timeout: number,
isLocal?: boolean,
noDefaults?: boolean,
artifactsDir?: string,
};
export type BrowserTypeConnectOverCDPOptions = {
headers?: NameValue[],
slowMo?: number,
isLocal?: boolean,
noDefaults?: boolean,
artifactsDir?: string,
};
export type BrowserTypeConnectOverCDPResult = {
browser: BrowserChannel,
Expand Down
36 changes: 36 additions & 0 deletions tests/library/chromium/connect-over-cdp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { playwrightTest as test, expect } from '../../config/browserTest';
import http from 'http';
import fs from 'fs';
import path from 'path';
import { getUserAgent, server as coreServer } from '../../../packages/playwright-core/lib/coreBundle';
import { suppressCertificateWarning } from '../../config/utils';

Expand Down Expand Up @@ -60,6 +61,41 @@ test('should cleanup artifacts dir after connectOverCDP disconnects due to ws cl
expect(exists2).toBe(false);
});

test('should write traces to provided artifactsDir on connectOverCDP', async ({ browserType, toImpl }, testInfo) => {
const port = 9339 + testInfo.workerIndex;
const browserServer = await browserType.launch({
args: ['--remote-debugging-port=' + port]
});
const artifactsDir = testInfo.outputPath('custom-artifacts');
try {
const cdpBrowser = await browserType.connectOverCDP({
endpointURL: `http://127.0.0.1:${port}/`,
artifactsDir,
});
expect(toImpl(cdpBrowser).options.artifactsDir).toBe(artifactsDir);
expect(toImpl(cdpBrowser).options.tracesDir).toBe(artifactsDir);

const context = cdpBrowser.contexts()[0];
await context.tracing.start({ name: 'cdp-trace', snapshots: true, screenshots: true });
const page = await context.newPage();
await page.setContent('<button>Hello</button>');
await context.tracing.stopChunk();

expect(fs.existsSync(path.join(artifactsDir, 'cdp-trace.trace'))).toBe(true);
expect(fs.existsSync(path.join(artifactsDir, 'cdp-trace.network'))).toBe(true);
expect(fs.existsSync(path.join(artifactsDir, 'resources'))).toBe(true);

await Promise.all([
new Promise(f => cdpBrowser.on('disconnected', f)),
browserServer.close()
]);

expect(fs.existsSync(artifactsDir)).toBe(true);
} finally {
await browserServer.close().catch(() => {});
}
});

test('should connectOverCDP and manage downloads in default context', async ({ browserType, mode, server }, testInfo) => {
server.setRoute('/downloadWithFilename', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
Expand Down
2 changes: 1 addition & 1 deletion tests/mcp/cdp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ test('should throw connection error and allow re-connecting', async ({ cdpServer
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
error: expect.stringContaining(`Error: connect ECONNREFUSED`),
error: expect.stringContaining(`connect ECONNREFUSED`),
isError: true,
});
await cdpServer.start();
Expand Down
20 changes: 20 additions & 0 deletions tests/mcp/cli-cdp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,23 @@ test('attach via cdp', async ({ cdpServer, cli, server }) => {
const { inlineSnapshot } = await cli('snapshot');
expect(inlineSnapshot).toContain(`- generic [active] [ref=e1]: Hello, world!`);
});

test('tracing-start-stop over cdp', async ({ cdpServer, cli, server }, testInfo) => {
const browserContext = await cdpServer.start();
const [page] = browserContext.pages();
await page.goto(server.HELLO_WORLD);

await cli('attach', `--cdp=${cdpServer.endpoint}`);

const { output } = await cli('tracing-start');
expect(output).toContain('Trace recording started');
await cli('eval', '() => fetch("/hello-world")');

const { output: tracingStopOutput } = await cli('tracing-stop');
expect(tracingStopOutput).toContain('Trace recording stopped');
const [, timestamp] = tracingStopOutput.match(/trace-(\d+)\.trace/);

expect(fs.existsSync(testInfo.outputPath('.playwright-cli', 'traces', 'resources'))).toBeTruthy();
expect(fs.existsSync(testInfo.outputPath('.playwright-cli', 'traces', `trace-${timestamp}.trace`))).toBeTruthy();
expect(fs.existsSync(testInfo.outputPath('.playwright-cli', 'traces', `trace-${timestamp}.network`))).toBeTruthy();
});
Loading