diff --git a/packages/isomorphic/protocolMetainfo.ts b/packages/isomorphic/protocolMetainfo.ts index b0e779d1b061a..e99ac29133016 100644 --- a/packages/isomorphic/protocolMetainfo.ts +++ b/packages/isomorphic/protocolMetainfo.ts @@ -115,6 +115,7 @@ export const methodMetainfo = new Map([ ['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', }], diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 7a032da7ad9a6..85617e01e86a5 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -15414,6 +15414,32 @@ export interface BrowserType { * @param options */ connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; + /** + * 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; /** * This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js. * @@ -16194,6 +16220,13 @@ export interface BrowserType { 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. diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 3a98bb8120099..5cbc931d710fc 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -138,12 +138,15 @@ export class BrowserType extends ChannelOwner imple async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise; - async connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string, options?: api.ConnectOverCDPOptions) { + async connectOverCDP(transport: api.ConnectionTransport): Promise; + 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 { @@ -159,6 +162,17 @@ export class BrowserType extends ChannelOwner imple noDefaults: params.noDefaults, artifactsDir: params.artifactsDir, }); + return await this._browserFromConnectResult(result); + } + + async _connectOverCDPTransport(transport: api.ConnectionTransport): Promise { + 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 { const browser = Browser.from(result.browser); browser._connectToBrowserType(this, {}, undefined); if (result.defaultContext) @@ -175,5 +189,8 @@ export class BrowserType extends ChannelOwner 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'; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index af1c8ecc2b90b..1e39b28725483 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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, diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 8096c9eedab47..d9a1e34f524d4 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -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 { + throw new Error('CDP connections are only supported by Chromium'); + } + async connectToWorker(progress: Progress, endpoint: string): Promise { throw new Error('CDP connections are only supported by Chromium'); } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index e0b4d362590d8..063701c82b144 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -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); diff --git a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts index bd5c7a3fe0d87..40633088d5d1e 100644 --- a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts @@ -65,6 +65,15 @@ export class BrowserTypeDispatcher extends Dispatcher { + 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 { if (this._denyLaunch) throw new Error(`Launching more browsers is not allowed.`); diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index bd0f24e8cc718..4ff97881ebb74 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -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; } @@ -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)}); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 3ec233045373b..92f3113f2611f 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -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'; @@ -475,7 +476,6 @@ export class Frame extends SdkObject { _name = ''; _inflightRequests = new Set(); private _networkIdleTimer: NodeJS.Timeout | undefined; - private _setContentCounter = 0; readonly _detachedScope = new LongStandingScope(); private _raceAgainstEvaluationStallingEventsPromises = new Set>(); readonly _redirectedNavigations = new Map }>(); // documentId -> data @@ -735,6 +735,8 @@ export class Frame extends SdkObject { } context(world: types.World): Promise { + if (this._page.delegate.noUtilityWorld?.()) + world = 'main'; return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => { if (contextOrDestroyedReason instanceof js.ExecutionContext) return contextOrDestroyedReason; @@ -919,7 +921,7 @@ export class Frame extends SdkObject { } async setContent(progress: Progress, html: string, options: types.NavigateOptions): Promise { - 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}"`); diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index 878964f9bee7d..20b247e31ac57 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -60,10 +60,12 @@ export class ExecutionContext extends SdkObject { private _utilityScriptPromise: Promise | 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; } @@ -102,8 +104,10 @@ export class ExecutionContext extends SdkObject { private _utilityScript(): Promise> { if (!this._utilityScriptPromise) { + const globalsSnapshot = this._noUtilityWorld ? mainWorldGlobalsSnapshotSource : ''; const source = ` (() => { + ${globalsSnapshot} const module = {}; ${rawUtilityScriptSource.source} return new (module.exports.UtilityScript())(globalThis, ${isUnderTest()}); @@ -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')} +`; diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 898adbade9a52..aa8429d37c409 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -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'; @@ -92,6 +93,7 @@ export interface PageDelegate { pdf?: (options: channels.PagePdfParams) => Promise; coverage?: () => any; + noUtilityWorld?: () => boolean; // Work around WebKit's raf issues on Windows. rafCountForStablePosition(): number; @@ -220,6 +222,10 @@ export class Page extends SdkObject { } 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()) diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 7a032da7ad9a6..85617e01e86a5 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -15414,6 +15414,32 @@ export interface BrowserType { * @param options */ connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; + /** + * 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; /** * This method attaches Playwright to an existing browser instance created via `BrowserType.launchServer` in Node.js. * @@ -16194,6 +16220,13 @@ export interface BrowserType { 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. diff --git a/packages/protocol/spec/browserType.yml b/packages/protocol/spec/browserType.yml index b86f06b7cfde9..022ed15aa3123 100644 --- a/packages/protocol/spec/browserType.yml +++ b/packages/protocol/spec/browserType.yml @@ -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: diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index c2f9cea282714..a4dd1e2bd47d9 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1822,6 +1822,7 @@ export interface BrowserTypeChannel extends BrowserTypeEventTarget, Channel { launch(params: BrowserTypeLaunchParams, progress?: Progress): Promise; launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, progress?: Progress): Promise; connectOverCDP(params: BrowserTypeConnectOverCDPParams, progress?: Progress): Promise; + connectOverCDPTransport(params: BrowserTypeConnectOverCDPTransportParams, progress?: Progress): Promise; connectToWorker(params: BrowserTypeConnectToWorkerParams, progress?: Progress): Promise; } export type BrowserTypeLaunchParams = { @@ -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, diff --git a/tests/config/ghaMarkdownReporter.ts b/tests/config/ghaMarkdownReporter.ts index 095496916d4ba..97a29864f184c 100644 --- a/tests/config/ghaMarkdownReporter.ts +++ b/tests/config/ghaMarkdownReporter.ts @@ -193,9 +193,9 @@ class GHAMarkdownReporter extends MarkdownReporter { private async collapsePreviousComments(prNumber: number) { const { owner, repo } = this.context.repo; const data = await this.octokit.graphql<{ repository: Repository }>(` - query { - repository(owner: "${owner}", name: "${repo}") { - pullRequest(number: ${prNumber}) { + query($owner: String!, $repo: String!, $prNumber: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $prNumber) { id comments(last: 100) { nodes { @@ -210,7 +210,7 @@ class GHAMarkdownReporter extends MarkdownReporter { } } } - `); + `, { owner, repo, prNumber }); const comments = data.repository.pullRequest?.comments.nodes?.filter(comment => comment?.author?.__typename === 'Bot' && comment?.author?.login === 'github-actions' && @@ -218,13 +218,15 @@ class GHAMarkdownReporter extends MarkdownReporter { const prId = data.repository.pullRequest?.id; if (!comments?.length) return prId; - const mutations = comments.map((comment, i) => - `m${i}: minimizeComment(input: { subjectId: "${comment!.id}", classifier: OUTDATED }) { clientMutationId }`); + const variableDecls = comments.map((_, i) => `$id${i}: ID!`).join(', '); + const mutations = comments.map((_, i) => + `m${i}: minimizeComment(input: { subjectId: $id${i}, classifier: OUTDATED }) { clientMutationId }`); + const subjectIds = Object.fromEntries(comments.map((comment, i) => [`id${i}`, comment!.id])); await this.octokit.graphql(` - mutation { + mutation(${variableDecls}) { ${mutations.join('\n')} } - `); + `, subjectIds); return prId; } @@ -257,8 +259,8 @@ class GHAMarkdownReporter extends MarkdownReporter { ]); const response = await this.octokit.graphql<{ addComment: { commentEdge: IssueCommentEdge } }>(` - mutation { - addComment(input: {subjectId: "${prNodeId}", body: """${body}"""}) { + mutation($subjectId: ID!, $body: String!) { + addComment(input: {subjectId: $subjectId, body: $body}) { commentEdge { node { ... on IssueComment { @@ -268,7 +270,7 @@ class GHAMarkdownReporter extends MarkdownReporter { } } } - `); + `, { subjectId: prNodeId, body }); this.core.info(`Posted comment: ${response.addComment.commentEdge.node?.url}`); } diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts index 52397b0d02030..29c6e99f5d87c 100644 --- a/tests/library/chromium/connect-over-cdp.spec.ts +++ b/tests/library/chromium/connect-over-cdp.spec.ts @@ -22,7 +22,7 @@ import path from 'path'; import { getUserAgent, server as coreServer } from '../../../packages/playwright-core/lib/coreBundle'; import { suppressCertificateWarning } from '../../config/utils'; -const { nullProgress } = coreServer; +const { WebSocketTransport, nullProgress } = coreServer; type Frame = coreServer.Frame; test('should connect to an existing cdp session', async ({ browserType, mode }, testInfo) => { @@ -750,3 +750,32 @@ test('noDefaults should not affect new contexts', async ({ browserType, mode, se await browserServer.close(); } }); + +test('should connect over CDP using a ConnectionTransport', async ({ browserType, mode, server }, testInfo) => { + test.skip(mode !== 'default', 'Passing a transport to connectOverCDP 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((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.connectOverCDP(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(); + } +}); diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 2274c79577616..09ca058974f55 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -521,6 +521,9 @@ class TypesGenerator { doNotGenerate: new Set([ ...assertionClasses, ]), + ignoreMissing: new Set([ + 'ConnectionTransport', + ]), }); let types = await generator.generateTypes(path.join(__dirname, 'overrides.d.ts')); const namedDevices = Object.keys(devices).map(name => ` ${JSON.stringify(name)}: DeviceDescriptor;`).join('\n'); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 03ca88049c18c..687f620e36984 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -216,6 +216,7 @@ export interface BrowserType { * @deprecated */ connectOverCDP(options: ConnectOverCDPOptions & { wsEndpoint?: string }): Promise; + connectOverCDP(transport: ConnectionTransport): Promise; connect(wsEndpoint: string, options?: ConnectOptions): Promise; /** * wsEndpoint in options is deprecated. Instead use `wsEndpoint`. @@ -226,6 +227,13 @@ export interface BrowserType { connect(options: ConnectOptions & { wsEndpoint?: string }): Promise; } +export interface ConnectionTransport { + send(message: object): void; + close(): void; + onmessage?: (message: object) => void; + onclose?: (reason?: string) => void; +} + export interface CDPSession { on(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; addListener(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this;