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
 | 
			
		||||
    `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
 | 
			
		||||
- returns: <[string]>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -185,6 +185,25 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
 | 
			
		||||
      });
 | 
			
		||||
    }, 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> {
 | 
			
		||||
 | 
			
		||||
@ -38,4 +38,12 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
 | 
			
		||||
    const browserContext = await this._object.launchPersistentContext(metadata, params.userDataDir, params);
 | 
			
		||||
    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 {
 | 
			
		||||
  launch(params: BrowserTypeLaunchParams, metadata?: Metadata): Promise<BrowserTypeLaunchResult>;
 | 
			
		||||
  launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, metadata?: Metadata): Promise<BrowserTypeLaunchPersistentContextResult>;
 | 
			
		||||
  connectOverCDP(params: BrowserTypeConnectOverCDPParams, metadata?: Metadata): Promise<BrowserTypeConnectOverCDPResult>;
 | 
			
		||||
}
 | 
			
		||||
export type BrowserTypeLaunchParams = {
 | 
			
		||||
  executablePath?: string,
 | 
			
		||||
@ -377,6 +378,19 @@ export type BrowserTypeLaunchPersistentContextOptions = {
 | 
			
		||||
export type BrowserTypeLaunchPersistentContextResult = {
 | 
			
		||||
  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 -----------
 | 
			
		||||
export type BrowserInitializer = {
 | 
			
		||||
 | 
			
		||||
@ -391,6 +391,14 @@ BrowserType:
 | 
			
		||||
      returns:
 | 
			
		||||
        context: BrowserContext
 | 
			
		||||
 | 
			
		||||
    connectOverCDP:
 | 
			
		||||
      parameters:
 | 
			
		||||
        wsEndpoint: string
 | 
			
		||||
        slowMo: number?
 | 
			
		||||
        timeout: number?
 | 
			
		||||
      returns:
 | 
			
		||||
        browser: Browser
 | 
			
		||||
        defaultContext: BrowserContext?
 | 
			
		||||
 | 
			
		||||
Browser:
 | 
			
		||||
  type: interface
 | 
			
		||||
 | 
			
		||||
@ -225,6 +225,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
 | 
			
		||||
      path: tString,
 | 
			
		||||
    })),
 | 
			
		||||
  });
 | 
			
		||||
  scheme.BrowserTypeConnectOverCDPParams = tObject({
 | 
			
		||||
    wsEndpoint: tString,
 | 
			
		||||
    slowMo: tOptional(tNumber),
 | 
			
		||||
    timeout: tOptional(tNumber),
 | 
			
		||||
  });
 | 
			
		||||
  scheme.BrowserCloseParams = tOptional(tObject({}));
 | 
			
		||||
  scheme.BrowserNewContextParams = tObject({
 | 
			
		||||
    noDefaultViewport: tOptional(tBoolean),
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ import * as registry from '../utils/registry';
 | 
			
		||||
import { SdkObject } from './instrumentation';
 | 
			
		||||
 | 
			
		||||
export interface BrowserProcess {
 | 
			
		||||
  onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
 | 
			
		||||
  onclose?: ((exitCode: number | null, signal: string | null) => void);
 | 
			
		||||
  process?: ChildProcess;
 | 
			
		||||
  kill(): Promise<void>;
 | 
			
		||||
  close(): Promise<void>;
 | 
			
		||||
@ -37,7 +37,7 @@ export type PlaywrightOptions = {
 | 
			
		||||
  rootSdkObject: SdkObject,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type BrowserOptions = PlaywrightOptions & {
 | 
			
		||||
export type BrowserOptions = PlaywrightOptions & types.UIOptions & {
 | 
			
		||||
  name: string,
 | 
			
		||||
  isChromium: boolean,
 | 
			
		||||
  downloadsPath?: string,
 | 
			
		||||
@ -47,7 +47,6 @@ export type BrowserOptions = PlaywrightOptions & {
 | 
			
		||||
  proxy?: ProxySettings,
 | 
			
		||||
  protocolLogger: types.ProtocolLogger,
 | 
			
		||||
  browserLogsCollector: RecentLogsCollector,
 | 
			
		||||
  slowMo?: number,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export abstract class Browser extends SdkObject {
 | 
			
		||||
 | 
			
		||||
@ -223,6 +223,10 @@ export abstract class BrowserType extends SdkObject {
 | 
			
		||||
    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 _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<Browser>;
 | 
			
		||||
  abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
 | 
			
		||||
 | 
			
		||||
@ -21,11 +21,16 @@ import { Env } from '../processLauncher';
 | 
			
		||||
import { kBrowserCloseMessageId } from './crConnection';
 | 
			
		||||
import { rewriteErrorMessage } from '../../utils/stackTrace';
 | 
			
		||||
import { BrowserType } from '../browserType';
 | 
			
		||||
import { ConnectionTransport, ProtocolRequest } from '../transport';
 | 
			
		||||
import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport';
 | 
			
		||||
import { CRDevTools } from './crDevTools';
 | 
			
		||||
import { BrowserOptions, PlaywrightOptions } from '../browser';
 | 
			
		||||
import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
 | 
			
		||||
import * as types from '../types';
 | 
			
		||||
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 {
 | 
			
		||||
  private _devtools: CRDevTools | undefined;
 | 
			
		||||
@ -37,6 +42,34 @@ export class Chromium extends BrowserType {
 | 
			
		||||
      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() {
 | 
			
		||||
    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') {
 | 
			
		||||
      const backgroundPage = new CRPage(session, targetInfo.targetId, context, null, false);
 | 
			
		||||
      this._backgroundPages.set(targetInfo.targetId, backgroundPage);
 | 
			
		||||
      backgroundPage.pageOrError().then(() => {
 | 
			
		||||
      backgroundPage.pageOrError().then(pageOrError => {
 | 
			
		||||
        if (pageOrError instanceof Page)
 | 
			
		||||
          context!.emit(CRBrowserContext.CREvents.BackgroundPage, backgroundPage._page);
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
@ -113,8 +113,8 @@ export class WebSocketTransport implements ConnectionTransport {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async closeAndWait() {
 | 
			
		||||
    const promise = new Promise(f => this.onclose = f);
 | 
			
		||||
    const promise = new Promise(f => this._ws.once('close', f));
 | 
			
		||||
    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 }[];
 | 
			
		||||
 | 
			
		||||
type LaunchOptionsBase = {
 | 
			
		||||
type LaunchOptionsBase = UIOptions & {
 | 
			
		||||
  executablePath?: string,
 | 
			
		||||
  args?: string[],
 | 
			
		||||
  ignoreDefaultArgs?: string[],
 | 
			
		||||
@ -269,7 +269,6 @@ type LaunchOptionsBase = {
 | 
			
		||||
  proxy?: ProxySettings,
 | 
			
		||||
  downloadsPath?: string,
 | 
			
		||||
  chromiumSandbox?: boolean,
 | 
			
		||||
  slowMo?: number,
 | 
			
		||||
};
 | 
			
		||||
export type LaunchOptions = LaunchOptionsBase & {
 | 
			
		||||
  firefoxUserPrefs?: { [key: string]: string | number | boolean },
 | 
			
		||||
@ -345,3 +344,7 @@ export type SetStorageState = {
 | 
			
		||||
  cookies?: SetNetworkCookieParam[],
 | 
			
		||||
  origins?: OriginStorage[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type UIOptions = {
 | 
			
		||||
  slowMo?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright 2018 Google Inc. All rights reserved.
 | 
			
		||||
 * Modifications copyright (c) Microsoft Corporation.
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (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 type { ChromiumBrowserContext } from '../..';
 | 
			
		||||
import http from 'http';
 | 
			
		||||
 | 
			
		||||
describe('chromium', (suite, { browserName }) => {
 | 
			
		||||
  suite.skip(browserName !== 'chromium');
 | 
			
		||||
@ -88,4 +90,31 @@ describe('chromium', (suite, { browserName }) => {
 | 
			
		||||
    // make it work with Edgium.
 | 
			
		||||
    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>;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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.
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user