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