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: 1 addition & 0 deletions packages/isomorphic/protocolMetainfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export const methodMetainfo = new Map<string, MethodMetainfo>([
['BrowserType.launch', { title: 'Launch browser', }],
['BrowserType.launchPersistentContext', { title: 'Launch persistent context', }],
['BrowserType.connectOverCDP', { title: 'Connect over CDP', }],
['BrowserType.connectOverCDPTransport', { title: 'Connect over CDP transport', }],
['BrowserType.connectToWorker', { title: 'Connect to worker', }],
['Disposable.dispose', { internal: true, potentiallyClosesScope: true, }],
['Electron.launch', { title: 'Launch electron', }],
Expand Down
33 changes: 33 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15414,6 +15414,32 @@ export interface BrowserType<Unused = {}> {
* @param options
*/
connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<Browser>;
/**
* This method 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#browser-contexts).
*
* **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
* **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via
* [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
* If you are experiencing issues or attempting to use advanced functionality, you probably want to use
* [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
*
* **Usage**
*
* ```js
* const browser = await playwright.chromium.connectOverCDP('http://localhost:9222');
* const defaultContext = browser.contexts()[0];
* const page = defaultContext.pages()[0];
* ```
*
* @param endpointURL A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or
* `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`.
* @param options
*/
connectOverCDP(transport: ConnectionTransport): Promise<Browser>;
/**
* This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js.
*
Expand Down Expand Up @@ -16194,6 +16220,13 @@ export interface BrowserType<Unused = {}> {
name(): string;
}

export interface ConnectionTransport {
send(message: object): void;
close(): void;
onmessage?: (message: object) => void;
onclose?: (reason?: string) => void;
}

/**
* The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
* - protocol methods can be called with `session.send` method.
Expand Down
21 changes: 19 additions & 2 deletions packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,15 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple

async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<api.Browser>;
async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise<api.Browser>;
async connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string, options?: api.ConnectOverCDPOptions) {
async connectOverCDP(transport: api.ConnectionTransport): Promise<api.Browser>;
async connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string|api.ConnectionTransport, options?: api.ConnectOverCDPOptions) {
if (typeof endpointURLOrOptions === 'string')
return await this._connectOverCDP(endpointURLOrOptions, options);
if (isConnectionTransport(endpointURLOrOptions))
return await this._connectOverCDPTransport(endpointURLOrOptions);
const endpointURL = 'endpointURL' in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint;
assert(endpointURL, 'Cannot connect over CDP without wsEndpoint.');
return await this.connectOverCDP(endpointURL, endpointURLOrOptions);
return await this._connectOverCDP(endpointURL, endpointURLOrOptions);
}

async _connectOverCDP(endpointURL: string, params: api.ConnectOverCDPOptions = {}): Promise<Browser> {
Expand All @@ -159,6 +162,17 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
noDefaults: params.noDefaults,
artifactsDir: params.artifactsDir,
});
return await this._browserFromConnectResult(result);
}

async _connectOverCDPTransport(transport: api.ConnectionTransport): Promise<Browser> {
if (this.name() !== 'chromium')
throw new Error('Connecting over CDP is only supported in Chromium.');
const result = await this._channel.connectOverCDPTransport({ transport: transport as any });
return await this._browserFromConnectResult(result);
}

private async _browserFromConnectResult(result: { browser: channels.BrowserChannel, defaultContext?: channels.BrowserContextChannel }): Promise<Browser> {
const browser = Browser.from(result.browser);
browser._connectToBrowserType(this, {}, undefined);
if (result.defaultContext)
Expand All @@ -175,5 +189,8 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
});
return Worker.from(result.worker);
}
}

function isConnectionTransport(value: any): value is api.ConnectionTransport {
return !!value && typeof value === 'object' && typeof value.send === 'function' && typeof value.close === 'function';
}
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 @@ -1099,6 +1099,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.BrowserTypeConnectToWorkerParams = tObject({
endpoint: tString,
timeout: tFloat,
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 @@ -290,6 +290,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 connectToWorker(progress: Progress, endpoint: string): Promise<Worker> {
throw new Error('CDP connections are only supported by Chromium');
}
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/server/chromium/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,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 });
}

override async connectToWorker(progress: Progress, endpoint: string) {
const wsEndpoint = await urlToWSEndpoint(progress, endpoint, {});
const transport = await WebSocketTransport.connect(progress, wsEndpoint);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, channels.Brow
};
}

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 };
}

async connectToWorker(params: channels.BrowserTypeConnectToWorkerParams, progress: Progress): Promise<channels.BrowserTypeConnectToWorkerResult> {
if (this._denyLaunch)
throw new Error(`Launching more browsers is not allowed.`);
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
readonly world: types.World | null;

constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) {
super(frame, delegate, world || 'content-script');
super(frame, delegate, world || 'content-script', { noUtilityWorld: frame._page.delegate.noUtilityWorld?.() });
this.frame = frame;
this.world = world;
}
Expand Down Expand Up @@ -98,8 +98,10 @@ export class FrameExecutionContext extends js.ExecutionContext {
isUtilityWorld: this.world === 'utility',
customEngines,
};
const globalsSnapshot = this.frame._page.delegate.noUtilityWorld?.() ? js.mainWorldGlobalsSnapshotSource : '';
const source = `
(() => {
${globalsSnapshot}
const module = {};
${rawInjectedScriptSource.source}
return new (module.exports.InjectedScript())(globalThis, ${JSON.stringify(options)});
Expand Down
6 changes: 4 additions & 2 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { asLocator } from '@isomorphic/locatorGenerators';
import { assert } from '@isomorphic/assert';
import { constructURLBasedOnBaseURL } from '@isomorphic/urlMatch';
import { makeWaitForNextTask } from '@utils/task';
import { createGuid } from '@utils/crypto';
import { BrowserContext } from './browserContext';
import * as dom from './dom';
import { TimeoutError } from './errors';
Expand Down Expand Up @@ -475,7 +476,6 @@ export class Frame extends SdkObject<FrameEventMap> {
_name = '';
_inflightRequests = new Set<network.Request>();
private _networkIdleTimer: NodeJS.Timeout | undefined;
private _setContentCounter = 0;
readonly _detachedScope = new LongStandingScope();
private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>();
readonly _redirectedNavigations = new Map<string, { url: string, gotoPromise: Promise<network.Response | null> }>(); // documentId -> data
Expand Down Expand Up @@ -735,6 +735,8 @@ export class Frame extends SdkObject<FrameEventMap> {
}

context(world: types.World): Promise<dom.FrameExecutionContext> {
if (this._page.delegate.noUtilityWorld?.())
world = 'main';
return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => {
if (contextOrDestroyedReason instanceof js.ExecutionContext)
return contextOrDestroyedReason;
Expand Down Expand Up @@ -919,7 +921,7 @@ export class Frame extends SdkObject<FrameEventMap> {
}

async setContent(progress: Progress, html: string, options: types.NavigateOptions): Promise<void> {
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
const tag = `--playwright--set--content--${createGuid()}--`;
await this.raceNavigationAction(progress, async () => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.log(`setting frame content, waiting until "${waitUntil}"`);
Expand Down
31 changes: 30 additions & 1 deletion packages/playwright-core/src/server/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ export class ExecutionContext extends SdkObject {
private _utilityScriptPromise: Promise<JSHandle> | undefined;
private _contextDestroyedScope = new LongStandingScope();
readonly worldNameForTest: string;
private _noUtilityWorld: boolean | undefined;

constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string) {
constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string, options?: { noUtilityWorld?: boolean }) {
super(parent, 'execution-context');
this.worldNameForTest = worldNameForTest;
this._noUtilityWorld = options?.noUtilityWorld;
this.delegate = delegate;
}

Expand Down Expand Up @@ -102,8 +104,10 @@ export class ExecutionContext extends SdkObject {

private _utilityScript(): Promise<JSHandle<UtilityScript>> {
if (!this._utilityScriptPromise) {
const globalsSnapshot = this._noUtilityWorld ? mainWorldGlobalsSnapshotSource : '';
const source = `
(() => {
${globalsSnapshot}
const module = {};
${rawUtilityScriptSource.source}
return new (module.exports.UtilityScript())(globalThis, ${isUnderTest()});
Expand Down Expand Up @@ -357,3 +361,28 @@ export function sparseArrayToString(entries: { name: string, value?: any }[]): s

return '[' + tokens.join(', ') + ']';
}

// Builtins that are frequently replaced or polyfilled by libraries (Prototype.js, MooTools,
// core-js, es6-shim, Sentry/Bugsnag, XRegExp, Web Components / Promise polyfills, etc.).
// Snapshotting the constructor reference protects the injected bundle from those overrides.
const snapshottedFunctionBuiltins = [
// DOM
'Node', 'Element', 'NodeFilter', 'HTMLElement', 'Document', 'ShadowRoot',
'MutationObserver', 'Event', 'CustomEvent', 'EventTarget',
// JS standard
'Map', 'Set', 'WeakMap', 'WeakSet', 'Promise', 'Symbol',
'Error', 'TypeError', 'RegExp', 'Array', 'Object',
];

// Non-callable globals (objects) — Prototype.js historically replaced JSON.
const snapshottedObjectBuiltins = ['JSON', 'Math'];

export const saveGlobalsSnapshotSource = `window.__pwSnapshotGlobals = {
${[...snapshottedFunctionBuiltins, ...snapshottedObjectBuiltins].map(n => ` ${n}: window.${n}`).join(',\n')}
};`;

export const mainWorldGlobalsSnapshotSource = `
const __snap = globalThis.__pwSnapshotGlobals || {};
${snapshottedFunctionBuiltins.map(n => ` const ${n} = (typeof globalThis.${n} === 'function' ? globalThis.${n} : __snap.${n});`).join('\n')}
${snapshottedObjectBuiltins.map(n => ` const ${n} = (typeof globalThis.${n} === 'object' && globalThis.${n} ? globalThis.${n} : __snap.${n});`).join('\n')}
`;
6 changes: 6 additions & 0 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import * as rawBindingsControllerSource from '../generated/bindingsControllerSou
import { Overlay } from './overlay';
import { NonRecoverableDOMError } from './dom';
import { Screencast } from './screencast';
import { saveGlobalsSnapshotSource } from './javascript';

import type { Artifact } from './artifact';
import type { BrowserContextEventMap } from './browserContext';
Expand Down Expand Up @@ -92,6 +93,7 @@ export interface PageDelegate {

pdf?: (options: channels.PagePdfParams) => Promise<Buffer>;
coverage?: () => any;
noUtilityWorld?: () => boolean;

// Work around WebKit's raf issues on Windows.
rafCountForStablePosition(): number;
Expand Down Expand Up @@ -220,6 +222,10 @@ export class Page extends SdkObject<PageEventMap> {
}

async reportAsNew(opener: Page | undefined, error?: Error) {
if (this.delegate.noUtilityWorld?.()) {
await this._addInitScript(saveGlobalsSnapshotSource);
await this.safeNonStallingEvaluateInAllFrames(saveGlobalsSnapshotSource, 'main');
}
if (opener) {
const openerPageOrError = await opener.waitForInitializedOrError();
if (openerPageOrError instanceof Page && !openerPageOrError.isClosed())
Expand Down
33 changes: 33 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15414,6 +15414,32 @@ export interface BrowserType<Unused = {}> {
* @param options
*/
connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<Browser>;
/**
* This method 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#browser-contexts).
*
* **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers.
*
* **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via
* [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
* If you are experiencing issues or attempting to use advanced functionality, you probably want to use
* [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
*
* **Usage**
*
* ```js
* const browser = await playwright.chromium.connectOverCDP('http://localhost:9222');
* const defaultContext = browser.contexts()[0];
* const page = defaultContext.pages()[0];
* ```
*
* @param endpointURL A CDP websocket endpoint or http url to connect to. For example `http://localhost:9222/` or
* `ws://127.0.0.1:9222/devtools/browser/387adf4c-243f-4051-a181-46798f4a46f4`.
* @param options
*/
connectOverCDP(transport: ConnectionTransport): Promise<Browser>;
/**
* This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js.
*
Expand Down Expand Up @@ -16194,6 +16220,13 @@ export interface BrowserType<Unused = {}> {
name(): string;
}

export interface ConnectionTransport {
send(message: object): void;
close(): void;
onmessage?: (message: object) => void;
onclose?: (reason?: string) => void;
}

/**
* The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
* - protocol methods can be called with `session.send` method.
Expand Down
8 changes: 8 additions & 0 deletions packages/protocol/spec/browserType.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ BrowserType:
browser: Browser
defaultContext: BrowserContext?

connectOverCDPTransport:
title: Connect over CDP transport
parameters:
transport: binary
returns:
browser: Browser
defaultContext: BrowserContext?

connectToWorker:
title: Connect to worker
parameters:
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 @@ -1822,6 +1822,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>;
connectToWorker(params: BrowserTypeConnectToWorkerParams, progress?: Progress): Promise<BrowserTypeConnectToWorkerResult>;
}
export type BrowserTypeLaunchParams = {
Expand Down Expand Up @@ -2078,6 +2079,16 @@ export type BrowserTypeConnectOverCDPResult = {
browser: BrowserChannel,
defaultContext?: BrowserContextChannel,
};
export type BrowserTypeConnectOverCDPTransportParams = {
transport: Binary,
};
export type BrowserTypeConnectOverCDPTransportOptions = {

};
export type BrowserTypeConnectOverCDPTransportResult = {
browser: BrowserChannel,
defaultContext?: BrowserContextChannel,
};
export type BrowserTypeConnectToWorkerParams = {
endpoint: string,
timeout: number,
Expand Down
Loading
Loading