From e851adfcb64275bbf62bd5c0fa4150d38ad4f296 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Fri, 6 Feb 2026 18:38:26 +0100 Subject: [PATCH] refactor: replace CDPSession class with type-safe ICDPSession backed by Playwright's internal CDP session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the custom WebSocket-based CDPSession class entirely. All CDP communication now goes through PlaywrightCDPSessionAdapter which wraps Playwright's existing CDP session, avoiding duplicate WebSocket connections and Target.attachToTarget calls that the relay intercepts. ICDPSession interface uses ProtocolMapping generics from devtools-protocol so send/on/off methods are fully type-safe — return types are inferred from the CDP command string (e.g. 'Page.getLayoutMetrics' returns Protocol.Page.GetLayoutMetricsResponse). - Remove CDPSession class and getCDPSessionForPage WebSocket factory - Remove wsUrl parameter from all snapshot/label functions - Remove CDPSession cache from executor (adapter is stateless) - Remove 'as CDPSession' casts from debugger, editor, styles, react-source - Rename getExistingCDPSessionForPage → getCDPSessionForPage - Remove test for old getCDPSession through relay --- playwriter/src/aria-snapshot.test.ts | 6 +- playwriter/src/aria-snapshot.ts | 23 +- playwriter/src/cdp-session.ts | 250 ++-------------------- playwriter/src/debugger-examples-types.ts | 8 +- playwriter/src/debugger.ts | 7 +- playwriter/src/editor.ts | 7 +- playwriter/src/executor.ts | 31 +-- playwriter/src/index.ts | 2 +- playwriter/src/react-source.ts | 5 +- playwriter/src/relay-core.test.ts | 4 +- playwriter/src/relay-session.test.ts | 58 ++--- playwriter/src/snapshot-tools.test.ts | 44 +--- playwriter/src/styles.ts | 7 +- 13 files changed, 85 insertions(+), 367 deletions(-) diff --git a/playwriter/src/aria-snapshot.test.ts b/playwriter/src/aria-snapshot.test.ts index 3c20077..6970cfb 100644 --- a/playwriter/src/aria-snapshot.test.ts +++ b/playwriter/src/aria-snapshot.test.ts @@ -103,7 +103,7 @@ describe('aria-snapshot', () => { await page.waitForTimeout(1000) if (SHOULD_DUMP_AX) { - const cdp = await getCDPSessionForPage({ page, wsUrl: getCdpUrl({ port: TEST_PORT }) }) + const cdp = await getCDPSessionForPage({ page }) try { await cdp.send('DOM.enable') await cdp.send('Accessibility.enable') @@ -116,7 +116,7 @@ describe('aria-snapshot', () => { } } - const { snapshot } = await getAriaSnapshot({ page, wsUrl: getCdpUrl({ port: TEST_PORT }) }) + const { snapshot } = await getAriaSnapshot({ page }) expect(snapshot.length).toBeGreaterThan(0) // Check for locator format: attribute selector or role selector expect(snapshot).toMatch(/(?:\[id="|\[data-[\w-]+="|role=)/) @@ -130,7 +130,6 @@ describe('aria-snapshot', () => { const { snapshot: interactiveSnapshot } = await getAriaSnapshot({ page, - wsUrl: getCdpUrl({ port: TEST_PORT }), interactiveOnly: true, }) expect(interactiveSnapshot).not.toMatch(/^-\s+heading\b/m) @@ -185,7 +184,6 @@ describe('aria-snapshot', () => { const { snapshot } = await getAriaSnapshot({ page, frame: iframeFrame!, - wsUrl: getCdpUrl({ port: TEST_PORT }), }) expect(snapshot).toContain('Iframe Heading') diff --git a/playwriter/src/aria-snapshot.ts b/playwriter/src/aria-snapshot.ts index 4022037..d043d0d 100644 --- a/playwriter/src/aria-snapshot.ts +++ b/playwriter/src/aria-snapshot.ts @@ -784,16 +784,15 @@ async function resolveFrame({ frame, page }: { frame?: Frame | FrameLocator; pag * await page.locator(selector).click() * ``` */ -export async function getAriaSnapshot({ page, frame, locator, refFilter, wsUrl, interactiveOnly = false, cdp }: { +export async function getAriaSnapshot({ page, frame, locator, refFilter, interactiveOnly = false, cdp }: { page: Page frame?: Frame | FrameLocator locator?: Locator refFilter?: (info: { role: string; name: string }) => boolean - wsUrl?: string interactiveOnly?: boolean cdp?: ICDPSession }): Promise { - const session = cdp || await getCDPSessionForPage({ page, wsUrl }) + const session = cdp || await getCDPSessionForPage({ page }) // Resolve FrameLocator to an actual Frame. FrameLocator (from locator.contentFrame()) // is a scoping helper without CDP access. We need the real Frame from page.frames() @@ -1128,20 +1127,18 @@ function isTruthy(value: T): value is NonNullable { async function getLabelBoxesForRefs({ page, refs, - wsUrl, maxConcurrency = MAX_LABEL_POSITION_CONCURRENCY, logger, cdp, }: { page: Page refs: AriaRef[] - wsUrl?: string maxConcurrency?: number logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } cdp?: ICDPSession }): Promise { const log = logger?.info ?? logger?.error ?? console.error - const session = cdp || await getCDPSessionForPage({ page, wsUrl }) + const session = cdp || await getCDPSessionForPage({ page }) const sema = new Sema(maxConcurrency) const labelRefs = refs.filter((ref) => { return Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role) @@ -1218,11 +1215,10 @@ async function getLabelBoxesForRefs({ * await page.locator('[data-testid="submit-btn"]').click() * ``` */ -export async function showAriaRefLabels({ page, locator, interactiveOnly = true, wsUrl, logger }: { +export async function showAriaRefLabels({ page, locator, interactiveOnly = true, logger }: { page: Page locator?: Locator interactiveOnly?: boolean - wsUrl?: string logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } }): Promise<{ snapshot: string @@ -1236,12 +1232,12 @@ export async function showAriaRefLabels({ page, locator, interactiveOnly = true, log(`[showAriaRefLabels] ensureA11yClient: ${Date.now() - startTime}ms`) const cdpStart = Date.now() - const cdp = await getCDPSessionForPage({ page, wsUrl }) + const cdp = await getCDPSessionForPage({ page }) log(`[showAriaRefLabels] getCDPSessionForPage: ${Date.now() - cdpStart}ms`) try { const snapshotStart = Date.now() - const { snapshot, refs } = await getAriaSnapshot({ page, locator, interactiveOnly, wsUrl, cdp }) + const { snapshot, refs } = await getAriaSnapshot({ page, locator, interactiveOnly, cdp }) const shortRefMap = new Map(refs.map((entry) => { return [entry.ref, entry.shortRef] })) @@ -1251,7 +1247,7 @@ export async function showAriaRefLabels({ page, locator, interactiveOnly = true, const rootHandle = locator ? await locator.elementHandle() : null const labelsStart = Date.now() - const labels = await getLabelBoxesForRefs({ page, refs, wsUrl, logger, cdp }) + const labels = await getLabelBoxesForRefs({ page, refs, logger, cdp }) const shortLabels = labels.map((label) => { return { ...label, @@ -1326,17 +1322,16 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise * await page.locator('[data-testid="submit-btn"]').click() * ``` */ -export async function screenshotWithAccessibilityLabels({ page, locator, interactiveOnly = true, wsUrl, collector, logger }: { +export async function screenshotWithAccessibilityLabels({ page, locator, interactiveOnly = true, collector, logger }: { page: Page locator?: Locator interactiveOnly?: boolean - wsUrl?: string collector: ScreenshotResult[] logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } }): Promise { const log = logger?.info ?? logger?.error const showLabelsStart = Date.now() - const { snapshot, labelCount } = await showAriaRefLabels({ page, locator, interactiveOnly, wsUrl, logger }) + const { snapshot, labelCount } = await showAriaRefLabels({ page, locator, interactiveOnly, logger }) if (log) { log(`showAriaRefLabels: ${Date.now() - showLabelsStart}ms`) } diff --git a/playwriter/src/cdp-session.ts b/playwriter/src/cdp-session.ts index 0d9022a..40c7870 100644 --- a/playwriter/src/cdp-session.ts +++ b/playwriter/src/cdp-session.ts @@ -1,247 +1,31 @@ -import WebSocket from 'ws' import type { Page, CDPSession as PlaywrightCDPSession } from '@xmorse/playwright-core' import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js' -import type { CDPResponseBase, CDPEventBase } from './cdp-types.js' -import { getCdpUrl } from './utils.js' /** - * Common interface for CDP sessions that works with both our CDPSession - * and Playwright's CDPSession. Use this type when you want to accept either. - * - * Uses loose types so Playwright's CDPSession (which uses Protocol.Events) - * is assignable to this interface. + * Type-safe CDP session interface using devtools-protocol ProtocolMapping. + * Provides autocomplete and type checking for CDP commands and events. + * Return types are inferred from the command string (e.g. 'Page.getLayoutMetrics' + * returns Protocol.Page.GetLayoutMetricsResponse). */ export interface ICDPSession { - send(method: string, params?: object, sessionId?: string | null): Promise - on(event: string, callback: (params: any) => void): unknown - off(event: string, callback: (params: any) => void): unknown - detach(): Promise - getSessionId?(): string | null -} - -interface PendingRequest { - resolve: (result: unknown) => void - reject: (error: Error) => void -} - -export class CDPSession implements ICDPSession { - private ws: WebSocket - private pendingRequests = new Map() - private eventListeners = new Map void>>() - private messageId = 0 - private sessionId: string | null = null - - constructor(ws: WebSocket) { - this.ws = ws - this.ws.on('message', (data) => { - try { - const message = JSON.parse(data.toString()) as CDPResponseBase | CDPEventBase - - if ('id' in message) { - const response = message as CDPResponseBase - const pending = this.pendingRequests.get(response.id) - if (pending) { - this.pendingRequests.delete(response.id) - if (response.error) { - pending.reject(new Error(response.error.message)) - } else { - pending.resolve(response.result) - } - } - } else if ('method' in message) { - const event = message as CDPEventBase - if (event.sessionId === this.sessionId || !event.sessionId) { - const listeners = this.eventListeners.get(event.method) - if (listeners) { - for (const listener of listeners) { - listener(event.params) - } - } - } - } - } catch (e) { - console.error('[CDPSession] Message handling error:', e) - } - }) - } - - setSessionId(sessionId: string) { - this.sessionId = sessionId - } - - getSessionId(): string | null { - return this.sessionId - } - send( method: K, params?: ProtocolMapping.Commands[K]['paramsType'][0], - // Some iframes are OOPIF targets with their own CDP session. Their frameId - // only exists inside that target session, so AX/DOM commands must be sent - // with the iframe's Target.attachToTarget sessionId instead of the page - // sessionId. This override lets us reuse the same websocket while routing - // a single command to the correct target session. sessionId?: string | null, - ): Promise { - const id = ++this.messageId - const message: { - id: number - method: K - params?: ProtocolMapping.Commands[K]['paramsType'][0] - sessionId?: string - source?: 'playwriter' - } = { id, method, params, source: 'playwriter' } - const resolvedSessionId = sessionId ?? this.sessionId - if (resolvedSessionId) { - message.sessionId = resolvedSessionId - } + ): Promise - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(id) - reject(new Error(`CDP command timeout: ${method}`)) - }, 30000) + on(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): unknown - this.pendingRequests.set(id, { - resolve: (result) => { - clearTimeout(timeout) - resolve(result as ProtocolMapping.Commands[K]['returnType']) - }, - reject: (error) => { - clearTimeout(timeout) - reject(error) - }, - }) + off(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): unknown - try { - this.ws.send(JSON.stringify(message)) - } catch (error) { - clearTimeout(timeout) - this.pendingRequests.delete(id) - reject(error instanceof Error ? error : new Error(String(error))) - } - }) - } - - on(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): this { - if (!this.eventListeners.has(event)) { - this.eventListeners.set(event, new Set()) - } - this.eventListeners.get(event)!.add(callback as (params: unknown) => void) - return this - } - - /** Alias for `on` - matches Playwright's CDPSession interface */ - addListener( - event: K, - callback: (params: ProtocolMapping.Events[K][0]) => void, - ): this { - return this.on(event, callback) - } - - off(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): this { - this.eventListeners.get(event)?.delete(callback as (params: unknown) => void) - return this - } - - /** Alias for `off` - matches Playwright's CDPSession interface */ - removeListener( - event: K, - callback: (params: ProtocolMapping.Events[K][0]) => void, - ): this { - return this.off(event, callback) - } - - /** Listen for an event once, then automatically remove the listener */ - once(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): this { - const onceCallback = (params: ProtocolMapping.Events[K][0]) => { - this.off(event, onceCallback) - callback(params) - } - return this.on(event, onceCallback) - } - - /** Alias for `close` - matches Playwright's CDPSession interface */ - detach(): Promise { - this.close() - return Promise.resolve() - } - - close() { - try { - for (const pending of this.pendingRequests.values()) { - pending.reject(new Error('CDPSession detached')) - } - this.pendingRequests.clear() - this.eventListeners.clear() - this.ws.close() - } catch (e) { - console.error('[CDPSession] WebSocket close error:', e) - } - } -} - -export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl?: string }): Promise { - const resolvedWsUrl = wsUrl || getCdpUrl() - const ws = new WebSocket(resolvedWsUrl) - - await new Promise((resolve, reject) => { - ws.on('open', resolve) - ws.on('error', reject) - }) - - const cdp = new CDPSession(ws) - - const pages = page.context().pages() - const pageIndex = pages.indexOf(page) - if (pageIndex === -1) { - cdp.close() - throw new Error('Page not found in context') - } - - const { targetInfos } = await cdp.send('Target.getTargets') - const pageTargets = targetInfos.filter((t) => t.type === 'page') - - const pageUrl = page.url() - let target = pageTargets[pageIndex] - - if (!target || target.url !== pageUrl) { - const matchingTargets = pageTargets.filter((candidate) => { - return candidate.url === pageUrl - }) - if (matchingTargets.length > 0) { - const fallbackIndex = pageIndex < matchingTargets.length ? pageIndex : 0 - target = matchingTargets[fallbackIndex] - } - } - - if (!target) { - cdp.close() - throw new Error(`Page index ${pageIndex} out of bounds (${pageTargets.length} targets)`) - } - - if (target.url !== pageUrl) { - cdp.close() - throw new Error(`URL mismatch: page has "${pageUrl}" but target has "${target.url}"`) - } - - const { sessionId } = await cdp.send('Target.attachToTarget', { - targetId: target.targetId, - flatten: true, - }) - cdp.setSessionId(sessionId) - - return cdp + detach(): Promise + getSessionId?(): string | null } /** * Wraps Playwright's CDPSession (from context.getExistingCDPSession) into an ICDPSession. * This reuses Playwright's internal CDP WebSocket instead of creating a new one, * which is important for the relay server where Target.attachToTarget is intercepted. - * - * The adapter maps between devtools-protocol's ProtocolMapping types (used by our CDPSession) - * and Playwright's Protocol types (used by their CDPSession). Both are compatible since - * ICDPSession uses loose string-based method names. */ export class PlaywrightCDPSessionAdapter implements ICDPSession { private _playwrightSession: PlaywrightCDPSession @@ -250,16 +34,19 @@ export class PlaywrightCDPSessionAdapter implements ICDPSession { this._playwrightSession = playwrightSession } - async send(method: string, params?: object): Promise { + async send( + method: K, + params?: ProtocolMapping.Commands[K]['paramsType'][0], + ): Promise { return await this._playwrightSession.send(method as never, params as never) } - on(event: string, callback: (params: unknown) => void): this { + on(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): this { this._playwrightSession.on(event as never, callback as never) return this } - off(event: string, callback: (params: unknown) => void): this { + off(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): this { this._playwrightSession.off(event as never, callback as never) return this } @@ -271,11 +58,10 @@ export class PlaywrightCDPSessionAdapter implements ICDPSession { /** * Gets a CDP session for a page by reusing Playwright's internal existing CDP session. - * Unlike getCDPSessionForPage which creates a new WebSocket, this uses the same WS - * Playwright already has. Works through the relay because it doesn't call - * Target.attachToTarget. + * This uses the same WebSocket Playwright already has, avoiding new connections. + * Works through the relay because it doesn't call Target.attachToTarget. */ -export async function getExistingCDPSessionForPage({ page }: { page: Page }): Promise { +export async function getCDPSessionForPage({ page }: { page: Page }): Promise { const context = page.context() const playwrightSession = await context.getExistingCDPSession(page) return new PlaywrightCDPSessionAdapter(playwrightSession) diff --git a/playwriter/src/debugger-examples-types.ts b/playwriter/src/debugger-examples-types.ts index c90e8da..d8c25f6 100644 --- a/playwriter/src/debugger-examples-types.ts +++ b/playwriter/src/debugger-examples-types.ts @@ -1,13 +1,13 @@ import type { Page, Locator } from '@xmorse/playwright-core' -import type { CDPSession } from './cdp-session.js' +import type { ICDPSession } from './cdp-session.js' import type { Debugger } from './debugger.js' import type { Editor } from './editor.js' import type { StylesResult } from './styles.js' export declare const page: Page -export declare const getCDPSession: (options: { page: Page }) => Promise -export declare const createDebugger: (options: { cdp: CDPSession }) => Debugger -export declare const createEditor: (options: { cdp: CDPSession }) => Editor +export declare const getCDPSession: (options: { page: Page }) => Promise +export declare const createDebugger: (options: { cdp: ICDPSession }) => Debugger +export declare const createEditor: (options: { cdp: ICDPSession }) => Editor export declare const getStylesForLocator: (options: { locator: Locator; includeUserAgentStyles?: boolean }) => Promise export declare const formatStylesAsText: (styles: StylesResult) => string export declare const console: { log: (...args: unknown[]) => void } diff --git a/playwriter/src/debugger.ts b/playwriter/src/debugger.ts index c442065..3f98a63 100644 --- a/playwriter/src/debugger.ts +++ b/playwriter/src/debugger.ts @@ -1,4 +1,4 @@ -import type { ICDPSession, CDPSession } from './cdp-session.js' +import type { ICDPSession } from './cdp-session.js' import type { Protocol } from 'devtools-protocol' export interface BreakpointInfo { @@ -48,7 +48,7 @@ export interface ScriptInfo { * ``` */ export class Debugger { - private cdp: CDPSession + private cdp: ICDPSession private debuggerEnabled = false private paused = false private currentCallFrames: Protocol.Debugger.CallFrame[] = [] @@ -71,8 +71,7 @@ export class Debugger { * ``` */ constructor({ cdp }: { cdp: ICDPSession }) { - // Cast to CDPSession for internal type safety - at runtime both are compatible - this.cdp = cdp as CDPSession + this.cdp = cdp this.setupEventListeners() } diff --git a/playwriter/src/editor.ts b/playwriter/src/editor.ts index 6faa26c..baf4d55 100644 --- a/playwriter/src/editor.ts +++ b/playwriter/src/editor.ts @@ -1,4 +1,4 @@ -import type { ICDPSession, CDPSession } from './cdp-session.js' +import type { ICDPSession } from './cdp-session.js' export interface ReadResult { content: string @@ -46,15 +46,14 @@ export interface EditResult { * ``` */ export class Editor { - private cdp: CDPSession + private cdp: ICDPSession private enabled = false private scripts = new Map() private stylesheets = new Map() private sourceCache = new Map() constructor({ cdp }: { cdp: ICDPSession }) { - // Cast to CDPSession for internal type safety - at runtime both are compatible - this.cdp = cdp as CDPSession + this.cdp = cdp this.setupEventListeners() } diff --git a/playwriter/src/executor.ts b/playwriter/src/executor.ts index a998233..8bf724e 100644 --- a/playwriter/src/executor.ts +++ b/playwriter/src/executor.ts @@ -16,7 +16,7 @@ import * as acorn from 'acorn' import { createSmartDiff } from './diff-utils.js' import { getCdpUrl } from './utils.js' import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js' -import { getCDPSessionForPage, CDPSession, ICDPSession, getExistingCDPSessionForPage } from './cdp-session.js' +import { ICDPSession, getCDPSessionForPage } from './cdp-session.js' import { Debugger } from './debugger.js' import { Editor } from './editor.js' import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js' @@ -218,7 +218,7 @@ export class PlaywrightExecutor { private browserLogs: Map = new Map() private lastSnapshots: WeakMap = new WeakMap() private lastRefToLocator: WeakMap> = new WeakMap() - private cdpSessionCache: WeakMap = new WeakMap() + private scopedFs: ScopedFS private sandboxedRequire: NodeRequire @@ -555,7 +555,6 @@ export class PlaywrightExecutor { page: resolvedPage, frame, locator, - wsUrl: getCdpUrl(this.cdpConfig), interactiveOnly, }) const snapshotStr = rawSnapshot.toWellFormed?.() ?? rawSnapshot @@ -717,30 +716,7 @@ export class PlaywrightExecutor { if (options.page.isClosed()) { throw new Error('Cannot create CDP session for closed page') } - - const cached = this.cdpSessionCache.get(options.page) - if (cached) { - return cached - } - - // Reuse Playwright's internal CDP session over the same WebSocket instead of - // creating a new WS connection. This is critical for the relay where - // Target.attachToTarget is intercepted and can't create real new sessions. - const session = await getExistingCDPSessionForPage({ page: options.page }) - this.cdpSessionCache.set(options.page, session) - - options.page.on('close', () => { - const cachedSession = this.cdpSessionCache.get(options.page) - if (!cachedSession) { - return - } - this.cdpSessionCache.delete(options.page) - // Borrowed sessions: detach is a no-op since Playwright owns the underlying session. - // We just clean up the cache reference. - cachedSession.detach().catch(() => {}) - }) - - return session + return await getCDPSessionForPage({ page: options.page }) } const createDebugger = (options: { cdp: ICDPSession }) => new Debugger(options) @@ -761,7 +737,6 @@ export class PlaywrightExecutor { const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => { return screenshotWithAccessibilityLabels({ ...options, - wsUrl: getCdpUrl(this.cdpConfig), collector: screenshotCollector, logger: { info: (...args) => { diff --git a/playwriter/src/index.ts b/playwriter/src/index.ts index 208a70e..e2df340 100644 --- a/playwriter/src/index.ts +++ b/playwriter/src/index.ts @@ -1,6 +1,6 @@ export * from './cdp-relay.js' export * from './utils.js' -export { CDPSession, getCDPSessionForPage, getExistingCDPSessionForPage, PlaywrightCDPSessionAdapter } from './cdp-session.js' +export { getCDPSessionForPage, PlaywrightCDPSessionAdapter } from './cdp-session.js' export type { ICDPSession } from './cdp-session.js' export { Editor } from './editor.js' export type { ReadResult, SearchMatch, EditResult } from './editor.js' diff --git a/playwriter/src/react-source.ts b/playwriter/src/react-source.ts index 1bc7336..9306274 100644 --- a/playwriter/src/react-source.ts +++ b/playwriter/src/react-source.ts @@ -2,7 +2,7 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import type { Page, Locator, ElementHandle } from '@xmorse/playwright-core' -import type { ICDPSession, CDPSession } from './cdp-session.js' +import type { ICDPSession } from './cdp-session.js' export interface ReactSourceLocation { fileName: string | null @@ -30,8 +30,7 @@ export async function getReactSource({ locator: Locator | ElementHandle cdp: ICDPSession }): Promise { - // Cast to CDPSession for internal type safety - at runtime both are compatible - const cdp = cdpSession as CDPSession + const cdp = cdpSession const page: Page = 'page' in locator && typeof locator.page === 'function' ? locator.page() : (locator as any)._page if (!page) { diff --git a/playwriter/src/relay-core.test.ts b/playwriter/src/relay-core.test.ts index a8dd46e..ac49cab 100644 --- a/playwriter/src/relay-core.test.ts +++ b/playwriter/src/relay-core.test.ts @@ -55,7 +55,7 @@ describe('Relay Core Tests', () => { await new Promise((r) => { setTimeout(r, 100) }) const cdpSession = await withTimeout({ - promise: getCDPSessionForPage({ page, wsUrl: getCdpUrl({ port: TEST_PORT }) }), + promise: getCDPSessionForPage({ page }), timeoutMs: 10000, errorMessage: 'Timed out creating CDP session for page', }) @@ -82,7 +82,7 @@ describe('Relay Core Tests', () => { }) expect(hasGlobalAfter).toEqual({ foo: 'bar' }) - cdpSession.close() + await cdpSession.detach() await page.close() }, 60000) diff --git a/playwriter/src/relay-session.test.ts b/playwriter/src/relay-session.test.ts index b923b66..66073f9 100644 --- a/playwriter/src/relay-session.test.ts +++ b/playwriter/src/relay-session.test.ts @@ -53,7 +53,7 @@ describe('CDP Session Tests', () => { await new Promise(r => setTimeout(r, 100)) const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page }) const dbg = new Debugger({ cdp: cdpSession }) await dbg.enable() @@ -102,7 +102,7 @@ describe('CDP Session Tests', () => { expect(dbg.isPaused()).toBe(false) await evalPromise - cdpSession.close() + await cdpSession.detach() await page.close() }, 60000) @@ -161,7 +161,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const dbg = new Debugger({ cdp: cdpSession }) await dbg.enable() @@ -171,7 +171,7 @@ describe('CDP Session Tests', () => { expect(scripts[0]).toHaveProperty('scriptId') expect(scripts[0]).toHaveProperty('url') - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -197,7 +197,7 @@ describe('CDP Session Tests', () => { await new Promise(r => setTimeout(r, 100)) const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page }) const dbg = new Debugger({ cdp: cdpSession }) await dbg.enable() @@ -216,7 +216,7 @@ describe('CDP Session Tests', () => { await dbg.deleteBreakpoint({ breakpointId: bpId }) expect(dbg.listBreakpoints()).toHaveLength(0) - cdpSession.close() + await cdpSession.detach() await page.close() }, 60000) @@ -234,7 +234,7 @@ describe('CDP Session Tests', () => { await new Promise(r => setTimeout(r, 100)) const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page }) const dbg = new Debugger({ cdp: cdpSession }) await dbg.enable() @@ -285,7 +285,7 @@ describe('CDP Session Tests', () => { await dbg.resume() await evalPromise - cdpSession.close() + await cdpSession.detach() await page.close() }, 60000) @@ -304,7 +304,7 @@ describe('CDP Session Tests', () => { await new Promise(r => setTimeout(r, 100)) const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page }) await cdpSession.send('Profiler.enable') await cdpSession.send('Profiler.start') @@ -336,7 +336,7 @@ describe('CDP Session Tests', () => { expect(functionNames.every((name) => typeof name === 'string')).toBe(true) await cdpSession.send('Profiler.disable') - cdpSession.close() + await cdpSession.detach() await page.close() }, 60000) @@ -362,7 +362,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const initialTargets = await cdpSession.send('Target.getTargets') const initialPageTarget = initialTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('example.com')) @@ -383,7 +383,7 @@ describe('CDP Session Tests', () => { const exampleOrgTargets = allPageTargets.filter(t => t.url.includes('example.org')) expect(exampleOrgTargets).toHaveLength(1) - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -417,7 +417,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const { targetInfos } = await cdpSession.send('Target.getTargets') const allPageTargets = targetInfos.filter(t => t.type === 'page') @@ -442,7 +442,7 @@ describe('CDP Session Tests', () => { ] `) - cdpSession.close() + await cdpSession.detach() await browser.close() await page1.close() await page2.close() @@ -469,7 +469,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const evalResult = await cdpSession.send('Runtime.evaluate', { expression: 'document.title', @@ -477,7 +477,7 @@ describe('CDP Session Tests', () => { }) expect(evalResult.result.value).toContain('Example Domain') - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -501,7 +501,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const initialEvalResult = await cdpSession.send('Runtime.evaluate', { expression: 'document.title', @@ -530,7 +530,7 @@ describe('CDP Session Tests', () => { }) expect(locationResult.result.value).toBe(newUrl) - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -553,7 +553,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const dbg = new Debugger({ cdp: cdpSession }) await dbg.enable() @@ -588,7 +588,7 @@ describe('CDP Session Tests', () => { await dbg.setPauseOnExceptions({ state: 'none' }) - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -637,7 +637,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const dbg = new Debugger({ cdp: cdpSession }) await dbg.enable() @@ -676,7 +676,7 @@ describe('CDP Session Tests', () => { // Wait for evaluate to complete after resume await evalPromise - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -740,7 +740,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const editor = new Editor({ cdp: cdpSession }) await editor.enable() @@ -784,7 +784,7 @@ describe('CDP Session Tests', () => { expect(consoleLogs).toContain('Hello, World') expect(consoleLogs).toContain('EDITOR_TEST_MARKER') - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -807,7 +807,7 @@ describe('CDP Session Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const editor = new Editor({ cdp: cdpSession }) await editor.enable() @@ -859,7 +859,7 @@ describe('CDP Session Tests', () => { }) expect(colorAfter).toBe('rgb(0, 255, 0)') - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -908,7 +908,7 @@ describe('CDP Session Tests', () => { expect(hasBippyBefore).toBe(false) const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const { getReactSource } = await import('./react-source.js') const source = await getReactSource({ locator: btn, cdp: cdpSession }) @@ -1017,7 +1017,7 @@ describe('Service Worker Target Tests', () => { expect(cdpPage).toBeDefined() const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) await cdpSession.send('Network.disable') await cdpSession.send('Network.enable', { @@ -1036,7 +1036,7 @@ describe('Service Worker Target Tests', () => { expect(body).toContain('Example Domain') expect(body).toContain('') - cdpSession.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) diff --git a/playwriter/src/snapshot-tools.test.ts b/playwriter/src/snapshot-tools.test.ts index 2a63243..9016899 100644 --- a/playwriter/src/snapshot-tools.test.ts +++ b/playwriter/src/snapshot-tools.test.ts @@ -8,7 +8,7 @@ import fs from 'node:fs' import os from 'node:os' import { imageSize } from 'image-size' import { getCdpUrl } from './utils.js' -import { getCDPSessionForPage, getExistingCDPSessionForPage } from './cdp-session.js' +import { getCDPSessionForPage } from './cdp-session.js' import type { CDPCommand } from './cdp-types.js' import { screenshotWithAccessibilityLabels } from './aria-snapshot.js' import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, type TestContext, js } from './test-utils.js' @@ -483,8 +483,7 @@ describe('Snapshot & Screenshot Tests', () => { const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com')) expect(cdpPage).toBeDefined() - const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) + const cdpSession = await getCDPSessionForPage({ page: cdpPage! }) const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics') @@ -540,37 +539,7 @@ describe('Snapshot & Screenshot Tests', () => { console.log('window.devicePixelRatio:', windowDpr) expect(windowDpr).toBe(1) - cdpSession.close() - await browser.close() - await page.close() - }, 60000) - - it('should support getCDPSession through the relay', async () => { - const browserContext = getBrowserContext() - const serviceWorker = await getExtensionServiceWorker(browserContext) - - const page = await browserContext.newPage() - await page.goto('https://example.com/') - await page.bringToFront() - - await serviceWorker.evaluate(async () => { - await globalThis.toggleExtensionForActiveTab() - }) - - await new Promise(r => setTimeout(r, 100)) - - const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })) - const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com')) - expect(cdpPage).toBeDefined() - - const wsUrl = getCdpUrl({ port: TEST_PORT }) - const cdpClient = await getCDPSessionForPage({ page: cdpPage!, wsUrl }) - - const layoutMetrics = await cdpClient.send('Page.getLayoutMetrics') - expect(layoutMetrics.cssVisualViewport).toBeDefined() - expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0) - - cdpClient.close() + await cdpSession.detach() await browser.close() await page.close() }, 60000) @@ -593,8 +562,8 @@ describe('Snapshot & Screenshot Tests', () => { const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com')) expect(cdpPage).toBeDefined() - // Use the new getExistingCDPSessionForPage which reuses Playwright's internal WS - const cdpClient = await getExistingCDPSessionForPage({ page: cdpPage! }) + // Use the new getCDPSessionForPage which reuses Playwright's internal WS + const cdpClient = await getCDPSessionForPage({ page: cdpPage! }) // Should be able to send CDP commands just like the regular getCDPSessionForPage const layoutMetrics = await cdpClient.send('Page.getLayoutMetrics') @@ -649,7 +618,6 @@ describe('Snapshot & Screenshot Tests', () => { const ariaResult = await getAriaSnapshot({ page: cdpPage!, - wsUrl: getCdpUrl({ port: TEST_PORT }), }) expect(ariaResult.snapshot).toBeDefined() @@ -812,7 +780,7 @@ describe('Snapshot & Screenshot Tests', () => { const { labelCount } = await withTimeout( `showAriaRefLabels(${name})`, async () => { - return await showAriaRefLabels({ page: cdpPage, wsUrl }) + return await showAriaRefLabels({ page: cdpPage }) }, 60000 ) diff --git a/playwriter/src/styles.ts b/playwriter/src/styles.ts index 63a8b0a..46e99d8 100644 --- a/playwriter/src/styles.ts +++ b/playwriter/src/styles.ts @@ -1,4 +1,4 @@ -import type { ICDPSession, CDPSession } from './cdp-session.js' +import type { ICDPSession } from './cdp-session.js' import type { Locator } from '@xmorse/playwright-core' export interface StyleSource { @@ -73,8 +73,7 @@ export async function getStylesForLocator({ cdp: ICDPSession includeUserAgentStyles?: boolean }): Promise { - // Cast to CDPSession for internal type safety - at runtime both are compatible - const cdp = cdpSession as CDPSession + const cdp = cdpSession await cdp.send('DOM.enable') await cdp.send('CSS.enable') @@ -272,7 +271,7 @@ function formatElementDescription(node: any): string { return desc } -async function getStylesheetUrl(cdp: CDPSession, styleSheetId: string): Promise { +async function getStylesheetUrl(cdp: ICDPSession, styleSheetId: string): Promise { try { await cdp.send('CSS.getStyleSheetText', { styleSheetId }) return `stylesheet:${styleSheetId}`