mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(chromium): connect to a browser over cdp (#5207)
This commit is contained in:
parent
a8ebe4d888
commit
dca70abbd3
@ -63,6 +63,27 @@ This methods attaches Playwright to an existing browser instance.
|
|||||||
- `timeout` <[float]> Maximum time in milliseconds to wait for the connection to be established. Defaults to
|
- `timeout` <[float]> Maximum time in milliseconds to wait for the connection to be established. Defaults to
|
||||||
`30000` (30 seconds). Pass `0` to disable timeout.
|
`30000` (30 seconds). Pass `0` to disable timeout.
|
||||||
|
|
||||||
|
## async method: BrowserType.connectOverCDP
|
||||||
|
* langs: js
|
||||||
|
- returns: <[Browser]>
|
||||||
|
|
||||||
|
This methods attaches Playwright to an existing browser instance using the Chrome DevTools Protocol.
|
||||||
|
|
||||||
|
The default browser context is accessible via [`method: Browser.contexts`].
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### param: BrowserType.connectOverCDP.params
|
||||||
|
- `params` <[Object]>
|
||||||
|
- `wsEndpoint` <[string]> A CDP websocket endpoint to connect to.
|
||||||
|
- `slowMo` <[float]> Slows down Playwright operations by the specified amount of milliseconds. Useful so that you
|
||||||
|
can see what is going on. Defaults to 0.
|
||||||
|
- `logger` <[Logger]> Logger sink for Playwright logging. Optional.
|
||||||
|
- `timeout` <[float]> Maximum time in milliseconds to wait for the connection to be established. Defaults to
|
||||||
|
`30000` (30 seconds). Pass `0` to disable timeout.
|
||||||
|
|
||||||
## method: BrowserType.executablePath
|
## method: BrowserType.executablePath
|
||||||
- returns: <[string]>
|
- returns: <[string]>
|
||||||
|
|
||||||
|
|||||||
@ -185,6 +185,25 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
|
|||||||
});
|
});
|
||||||
}, logger);
|
}, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectOverCDP(params: ConnectOptions): Promise<Browser> {
|
||||||
|
if (this.name() !== 'chromium')
|
||||||
|
throw new Error('Connecting over CDP is only supported in Chromium.');
|
||||||
|
const logger = params.logger;
|
||||||
|
return this._wrapApiCall('browserType.connectOverCDP', async () => {
|
||||||
|
const result = await this._channel.connectOverCDP({
|
||||||
|
wsEndpoint: params.wsEndpoint,
|
||||||
|
slowMo: params.slowMo,
|
||||||
|
timeout: params.timeout
|
||||||
|
});
|
||||||
|
const browser = Browser.from(result.browser);
|
||||||
|
if (result.defaultContext)
|
||||||
|
browser._contexts.add(BrowserContext.from(result.defaultContext));
|
||||||
|
browser._isRemote = true;
|
||||||
|
browser._logger = logger;
|
||||||
|
return browser;
|
||||||
|
}, logger);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RemoteBrowser extends ChannelOwner<channels.RemoteBrowserChannel, channels.RemoteBrowserInitializer> {
|
export class RemoteBrowser extends ChannelOwner<channels.RemoteBrowserChannel, channels.RemoteBrowserInitializer> {
|
||||||
|
|||||||
@ -38,4 +38,12 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
|
|||||||
const browserContext = await this._object.launchPersistentContext(metadata, params.userDataDir, params);
|
const browserContext = await this._object.launchPersistentContext(metadata, params.userDataDir, params);
|
||||||
return { context: new BrowserContextDispatcher(this._scope, browserContext) };
|
return { context: new BrowserContextDispatcher(this._scope, browserContext) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectOverCDP(params: channels.BrowserTypeConnectOverCDPParams, metadata: CallMetadata): Promise<channels.BrowserTypeConnectOverCDPResult> {
|
||||||
|
const browser = await this._object.connectOverCDP(metadata, params.wsEndpoint, params, params.timeout);
|
||||||
|
return {
|
||||||
|
browser: new BrowserDispatcher(this._scope, browser),
|
||||||
|
defaultContext: browser._defaultContext ? new BrowserContextDispatcher(this._scope, browser._defaultContext) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -197,6 +197,7 @@ export type BrowserTypeInitializer = {
|
|||||||
export interface BrowserTypeChannel extends Channel {
|
export interface BrowserTypeChannel extends Channel {
|
||||||
launch(params: BrowserTypeLaunchParams, metadata?: Metadata): Promise<BrowserTypeLaunchResult>;
|
launch(params: BrowserTypeLaunchParams, metadata?: Metadata): Promise<BrowserTypeLaunchResult>;
|
||||||
launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, metadata?: Metadata): Promise<BrowserTypeLaunchPersistentContextResult>;
|
launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, metadata?: Metadata): Promise<BrowserTypeLaunchPersistentContextResult>;
|
||||||
|
connectOverCDP(params: BrowserTypeConnectOverCDPParams, metadata?: Metadata): Promise<BrowserTypeConnectOverCDPResult>;
|
||||||
}
|
}
|
||||||
export type BrowserTypeLaunchParams = {
|
export type BrowserTypeLaunchParams = {
|
||||||
executablePath?: string,
|
executablePath?: string,
|
||||||
@ -377,6 +378,19 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
|||||||
export type BrowserTypeLaunchPersistentContextResult = {
|
export type BrowserTypeLaunchPersistentContextResult = {
|
||||||
context: BrowserContextChannel,
|
context: BrowserContextChannel,
|
||||||
};
|
};
|
||||||
|
export type BrowserTypeConnectOverCDPParams = {
|
||||||
|
wsEndpoint: string,
|
||||||
|
slowMo?: number,
|
||||||
|
timeout?: number,
|
||||||
|
};
|
||||||
|
export type BrowserTypeConnectOverCDPOptions = {
|
||||||
|
slowMo?: number,
|
||||||
|
timeout?: number,
|
||||||
|
};
|
||||||
|
export type BrowserTypeConnectOverCDPResult = {
|
||||||
|
browser: BrowserChannel,
|
||||||
|
defaultContext?: BrowserContextChannel,
|
||||||
|
};
|
||||||
|
|
||||||
// ----------- Browser -----------
|
// ----------- Browser -----------
|
||||||
export type BrowserInitializer = {
|
export type BrowserInitializer = {
|
||||||
|
|||||||
@ -391,6 +391,14 @@ BrowserType:
|
|||||||
returns:
|
returns:
|
||||||
context: BrowserContext
|
context: BrowserContext
|
||||||
|
|
||||||
|
connectOverCDP:
|
||||||
|
parameters:
|
||||||
|
wsEndpoint: string
|
||||||
|
slowMo: number?
|
||||||
|
timeout: number?
|
||||||
|
returns:
|
||||||
|
browser: Browser
|
||||||
|
defaultContext: BrowserContext?
|
||||||
|
|
||||||
Browser:
|
Browser:
|
||||||
type: interface
|
type: interface
|
||||||
|
|||||||
@ -225,6 +225,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||||||
path: tString,
|
path: tString,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
scheme.BrowserTypeConnectOverCDPParams = tObject({
|
||||||
|
wsEndpoint: tString,
|
||||||
|
slowMo: tOptional(tNumber),
|
||||||
|
timeout: tOptional(tNumber),
|
||||||
|
});
|
||||||
scheme.BrowserCloseParams = tOptional(tObject({}));
|
scheme.BrowserCloseParams = tOptional(tObject({}));
|
||||||
scheme.BrowserNewContextParams = tObject({
|
scheme.BrowserNewContextParams = tObject({
|
||||||
noDefaultViewport: tOptional(tBoolean),
|
noDefaultViewport: tOptional(tBoolean),
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import * as registry from '../utils/registry';
|
|||||||
import { SdkObject } from './instrumentation';
|
import { SdkObject } from './instrumentation';
|
||||||
|
|
||||||
export interface BrowserProcess {
|
export interface BrowserProcess {
|
||||||
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
onclose?: ((exitCode: number | null, signal: string | null) => void);
|
||||||
process?: ChildProcess;
|
process?: ChildProcess;
|
||||||
kill(): Promise<void>;
|
kill(): Promise<void>;
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
@ -37,7 +37,7 @@ export type PlaywrightOptions = {
|
|||||||
rootSdkObject: SdkObject,
|
rootSdkObject: SdkObject,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BrowserOptions = PlaywrightOptions & {
|
export type BrowserOptions = PlaywrightOptions & types.UIOptions & {
|
||||||
name: string,
|
name: string,
|
||||||
isChromium: boolean,
|
isChromium: boolean,
|
||||||
downloadsPath?: string,
|
downloadsPath?: string,
|
||||||
@ -47,7 +47,6 @@ export type BrowserOptions = PlaywrightOptions & {
|
|||||||
proxy?: ProxySettings,
|
proxy?: ProxySettings,
|
||||||
protocolLogger: types.ProtocolLogger,
|
protocolLogger: types.ProtocolLogger,
|
||||||
browserLogsCollector: RecentLogsCollector,
|
browserLogsCollector: RecentLogsCollector,
|
||||||
slowMo?: number,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class Browser extends SdkObject {
|
export abstract class Browser extends SdkObject {
|
||||||
|
|||||||
@ -223,6 +223,10 @@ export abstract class BrowserType extends SdkObject {
|
|||||||
return { browserProcess, downloadsPath, transport };
|
return { browserProcess, downloadsPath, transport };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, uiOptions: types.UIOptions, timeout?: number): Promise<Browser> {
|
||||||
|
throw new Error('CDP connections are only supported by Chromium');
|
||||||
|
}
|
||||||
|
|
||||||
abstract _defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[];
|
abstract _defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[];
|
||||||
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<Browser>;
|
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<Browser>;
|
||||||
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
|
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
|
||||||
|
|||||||
@ -21,11 +21,16 @@ import { Env } from '../processLauncher';
|
|||||||
import { kBrowserCloseMessageId } from './crConnection';
|
import { kBrowserCloseMessageId } from './crConnection';
|
||||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
import { rewriteErrorMessage } from '../../utils/stackTrace';
|
||||||
import { BrowserType } from '../browserType';
|
import { BrowserType } from '../browserType';
|
||||||
import { ConnectionTransport, ProtocolRequest } from '../transport';
|
import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport';
|
||||||
import { CRDevTools } from './crDevTools';
|
import { CRDevTools } from './crDevTools';
|
||||||
import { BrowserOptions, PlaywrightOptions } from '../browser';
|
import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { isDebugMode } from '../../utils/utils';
|
import { isDebugMode } from '../../utils/utils';
|
||||||
|
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
|
import { ProgressController } from '../progress';
|
||||||
|
import { TimeoutSettings } from '../../utils/timeoutSettings';
|
||||||
|
import { helper } from '../helper';
|
||||||
|
import { CallMetadata } from '../instrumentation';
|
||||||
|
|
||||||
export class Chromium extends BrowserType {
|
export class Chromium extends BrowserType {
|
||||||
private _devtools: CRDevTools | undefined;
|
private _devtools: CRDevTools | undefined;
|
||||||
@ -37,6 +42,34 @@ export class Chromium extends BrowserType {
|
|||||||
this._devtools = this._createDevTools();
|
this._devtools = this._createDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, uiOptions: types.UIOptions, timeout?: number) {
|
||||||
|
const controller = new ProgressController(metadata, this);
|
||||||
|
controller.setLogName('browser');
|
||||||
|
const browserLogsCollector = new RecentLogsCollector();
|
||||||
|
return controller.run(async progress => {
|
||||||
|
const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint);
|
||||||
|
const browserProcess: BrowserProcess = {
|
||||||
|
close: async () => {
|
||||||
|
await chromeTransport.closeAndWait();
|
||||||
|
},
|
||||||
|
kill: async () => {
|
||||||
|
await chromeTransport.closeAndWait();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const browserOptions: BrowserOptions = {
|
||||||
|
...this._playwrightOptions,
|
||||||
|
...uiOptions,
|
||||||
|
name: 'chromium',
|
||||||
|
isChromium: true,
|
||||||
|
persistent: { noDefaultViewport: true },
|
||||||
|
browserProcess,
|
||||||
|
protocolLogger: helper.debugProtocolLogger(),
|
||||||
|
browserLogsCollector,
|
||||||
|
};
|
||||||
|
return await CRBrowser.connect(chromeTransport, browserOptions);
|
||||||
|
}, TimeoutSettings.timeout({timeout}));
|
||||||
|
}
|
||||||
|
|
||||||
private _createDevTools() {
|
private _createDevTools() {
|
||||||
return new CRDevTools(path.join(this._registry.browserDirectory('chromium'), 'devtools-preferences.json'));
|
return new CRDevTools(path.join(this._registry.browserDirectory('chromium'), 'devtools-preferences.json'));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,7 +156,8 @@ export class CRBrowser extends Browser {
|
|||||||
if (targetInfo.type === 'background_page') {
|
if (targetInfo.type === 'background_page') {
|
||||||
const backgroundPage = new CRPage(session, targetInfo.targetId, context, null, false);
|
const backgroundPage = new CRPage(session, targetInfo.targetId, context, null, false);
|
||||||
this._backgroundPages.set(targetInfo.targetId, backgroundPage);
|
this._backgroundPages.set(targetInfo.targetId, backgroundPage);
|
||||||
backgroundPage.pageOrError().then(() => {
|
backgroundPage.pageOrError().then(pageOrError => {
|
||||||
|
if (pageOrError instanceof Page)
|
||||||
context!.emit(CRBrowserContext.CREvents.BackgroundPage, backgroundPage._page);
|
context!.emit(CRBrowserContext.CREvents.BackgroundPage, backgroundPage._page);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -113,8 +113,8 @@ export class WebSocketTransport implements ConnectionTransport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async closeAndWait() {
|
async closeAndWait() {
|
||||||
const promise = new Promise(f => this.onclose = f);
|
const promise = new Promise(f => this._ws.once('close', f));
|
||||||
this.close();
|
this.close();
|
||||||
return promise; // Make sure to await the actual disconnect.
|
await promise; // Make sure to await the actual disconnect.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -254,7 +254,7 @@ export type BrowserContextOptions = {
|
|||||||
|
|
||||||
export type EnvArray = { name: string, value: string }[];
|
export type EnvArray = { name: string, value: string }[];
|
||||||
|
|
||||||
type LaunchOptionsBase = {
|
type LaunchOptionsBase = UIOptions & {
|
||||||
executablePath?: string,
|
executablePath?: string,
|
||||||
args?: string[],
|
args?: string[],
|
||||||
ignoreDefaultArgs?: string[],
|
ignoreDefaultArgs?: string[],
|
||||||
@ -269,7 +269,6 @@ type LaunchOptionsBase = {
|
|||||||
proxy?: ProxySettings,
|
proxy?: ProxySettings,
|
||||||
downloadsPath?: string,
|
downloadsPath?: string,
|
||||||
chromiumSandbox?: boolean,
|
chromiumSandbox?: boolean,
|
||||||
slowMo?: number,
|
|
||||||
};
|
};
|
||||||
export type LaunchOptions = LaunchOptionsBase & {
|
export type LaunchOptions = LaunchOptionsBase & {
|
||||||
firefoxUserPrefs?: { [key: string]: string | number | boolean },
|
firefoxUserPrefs?: { [key: string]: string | number | boolean },
|
||||||
@ -345,3 +344,7 @@ export type SetStorageState = {
|
|||||||
cookies?: SetNetworkCookieParam[],
|
cookies?: SetNetworkCookieParam[],
|
||||||
origins?: OriginStorage[]
|
origins?: OriginStorage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UIOptions = {
|
||||||
|
slowMo?: number;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright 2018 Google Inc. All rights reserved.
|
* Copyright 2018 Google Inc. All rights reserved.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
import { it, expect, describe } from '../fixtures';
|
import { it, expect, describe } from '../fixtures';
|
||||||
import type { ChromiumBrowserContext } from '../..';
|
import type { ChromiumBrowserContext } from '../..';
|
||||||
|
import http from 'http';
|
||||||
|
|
||||||
describe('chromium', (suite, { browserName }) => {
|
describe('chromium', (suite, { browserName }) => {
|
||||||
suite.skip(browserName !== 'chromium');
|
suite.skip(browserName !== 'chromium');
|
||||||
@ -88,4 +90,31 @@ describe('chromium', (suite, { browserName }) => {
|
|||||||
// make it work with Edgium.
|
// make it work with Edgium.
|
||||||
expect(serverRequest.headers.intervention).toContain('feature/5718547946799104');
|
expect(serverRequest.headers.intervention).toContain('feature/5718547946799104');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should connect to an existing cdp session 2', (test, {headful}) => {
|
||||||
|
test.skip(headful, 'Chromium currently doesn\'t support --remote-debugging-port and --remote-debugging-pipe at the same time.');
|
||||||
|
}, async ({browserType, testWorkerIndex, browserOptions, createUserDataDir }) => {
|
||||||
|
const port = 9339 + testWorkerIndex;
|
||||||
|
const browserServer = await browserType.launch({
|
||||||
|
...browserOptions,
|
||||||
|
args: ['--remote-debugging-port=' + port]
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const json = await new Promise<string>((resolve, reject) => {
|
||||||
|
http.get(`http://localhost:${port}/json/version/`, resp => {
|
||||||
|
let data = '';
|
||||||
|
resp.on('data', chunk => data += chunk);
|
||||||
|
resp.on('end', () => resolve(data));
|
||||||
|
}).on('error', reject);
|
||||||
|
});
|
||||||
|
const cdpBrowser = await browserType.connectOverCDP({
|
||||||
|
wsEndpoint: JSON.parse(json).webSocketDebuggerUrl,
|
||||||
|
});
|
||||||
|
const contexts = cdpBrowser.contexts();
|
||||||
|
expect(contexts.length).toBe(1);
|
||||||
|
await cdpBrowser.close();
|
||||||
|
} finally {
|
||||||
|
await browserServer.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
33
types/types.d.ts
vendored
33
types/types.d.ts
vendored
@ -6262,6 +6262,39 @@ export interface BrowserType<Browser> {
|
|||||||
*/
|
*/
|
||||||
connect(params: ConnectOptions): Promise<Browser>;
|
connect(params: ConnectOptions): Promise<Browser>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This methods 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#browsercontexts).
|
||||||
|
*
|
||||||
|
* > NOTE: Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
connectOverCDP(params: {
|
||||||
|
/**
|
||||||
|
* A CDP websocket endpoint to connect to.
|
||||||
|
*/
|
||||||
|
wsEndpoint: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
|
||||||
|
* Defaults to 0.
|
||||||
|
*/
|
||||||
|
slowMo?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger sink for Playwright logging. Optional.
|
||||||
|
*/
|
||||||
|
logger?: Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum time in milliseconds to wait for the connection to be established. Defaults to `30000` (30 seconds). Pass `0` to
|
||||||
|
* disable timeout.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
}): Promise<Browser>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A path where Playwright expects to find a bundled browser executable.
|
* A path where Playwright expects to find a bundled browser executable.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user