refactor: replace CDPSession class with type-safe ICDPSession backed by Playwright's internal CDP session

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
This commit is contained in:
Tommy D. Rossi
2026-02-06 18:38:26 +01:00
parent 682bde5c14
commit e851adfcb6
13 changed files with 85 additions and 367 deletions
+2 -4
View File
@@ -103,7 +103,7 @@ describe('aria-snapshot', () => {
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
if (SHOULD_DUMP_AX) { if (SHOULD_DUMP_AX) {
const cdp = await getCDPSessionForPage({ page, wsUrl: getCdpUrl({ port: TEST_PORT }) }) const cdp = await getCDPSessionForPage({ page })
try { try {
await cdp.send('DOM.enable') await cdp.send('DOM.enable')
await cdp.send('Accessibility.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) expect(snapshot.length).toBeGreaterThan(0)
// Check for locator format: attribute selector or role selector // Check for locator format: attribute selector or role selector
expect(snapshot).toMatch(/(?:\[id="|\[data-[\w-]+="|role=)/) expect(snapshot).toMatch(/(?:\[id="|\[data-[\w-]+="|role=)/)
@@ -130,7 +130,6 @@ describe('aria-snapshot', () => {
const { snapshot: interactiveSnapshot } = await getAriaSnapshot({ const { snapshot: interactiveSnapshot } = await getAriaSnapshot({
page, page,
wsUrl: getCdpUrl({ port: TEST_PORT }),
interactiveOnly: true, interactiveOnly: true,
}) })
expect(interactiveSnapshot).not.toMatch(/^-\s+heading\b/m) expect(interactiveSnapshot).not.toMatch(/^-\s+heading\b/m)
@@ -185,7 +184,6 @@ describe('aria-snapshot', () => {
const { snapshot } = await getAriaSnapshot({ const { snapshot } = await getAriaSnapshot({
page, page,
frame: iframeFrame!, frame: iframeFrame!,
wsUrl: getCdpUrl({ port: TEST_PORT }),
}) })
expect(snapshot).toContain('Iframe Heading') expect(snapshot).toContain('Iframe Heading')
+9 -14
View File
@@ -784,16 +784,15 @@ async function resolveFrame({ frame, page }: { frame?: Frame | FrameLocator; pag
* await page.locator(selector).click() * 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 page: Page
frame?: Frame | FrameLocator frame?: Frame | FrameLocator
locator?: Locator locator?: Locator
refFilter?: (info: { role: string; name: string }) => boolean refFilter?: (info: { role: string; name: string }) => boolean
wsUrl?: string
interactiveOnly?: boolean interactiveOnly?: boolean
cdp?: ICDPSession cdp?: ICDPSession
}): Promise<AriaSnapshotResult> { }): Promise<AriaSnapshotResult> {
const session = cdp || await getCDPSessionForPage({ page, wsUrl }) const session = cdp || await getCDPSessionForPage({ page })
// Resolve FrameLocator to an actual Frame. FrameLocator (from locator.contentFrame()) // 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() // is a scoping helper without CDP access. We need the real Frame from page.frames()
@@ -1128,20 +1127,18 @@ function isTruthy<T>(value: T): value is NonNullable<T> {
async function getLabelBoxesForRefs({ async function getLabelBoxesForRefs({
page, page,
refs, refs,
wsUrl,
maxConcurrency = MAX_LABEL_POSITION_CONCURRENCY, maxConcurrency = MAX_LABEL_POSITION_CONCURRENCY,
logger, logger,
cdp, cdp,
}: { }: {
page: Page page: Page
refs: AriaRef[] refs: AriaRef[]
wsUrl?: string
maxConcurrency?: number maxConcurrency?: number
logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void }
cdp?: ICDPSession cdp?: ICDPSession
}): Promise<AriaLabel[]> { }): Promise<AriaLabel[]> {
const log = logger?.info ?? logger?.error ?? console.error 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 sema = new Sema(maxConcurrency)
const labelRefs = refs.filter((ref) => { const labelRefs = refs.filter((ref) => {
return Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role) return Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role)
@@ -1218,11 +1215,10 @@ async function getLabelBoxesForRefs({
* await page.locator('[data-testid="submit-btn"]').click() * 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 page: Page
locator?: Locator locator?: Locator
interactiveOnly?: boolean interactiveOnly?: boolean
wsUrl?: string
logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void }
}): Promise<{ }): Promise<{
snapshot: string snapshot: string
@@ -1236,12 +1232,12 @@ export async function showAriaRefLabels({ page, locator, interactiveOnly = true,
log(`[showAriaRefLabels] ensureA11yClient: ${Date.now() - startTime}ms`) log(`[showAriaRefLabels] ensureA11yClient: ${Date.now() - startTime}ms`)
const cdpStart = Date.now() const cdpStart = Date.now()
const cdp = await getCDPSessionForPage({ page, wsUrl }) const cdp = await getCDPSessionForPage({ page })
log(`[showAriaRefLabels] getCDPSessionForPage: ${Date.now() - cdpStart}ms`) log(`[showAriaRefLabels] getCDPSessionForPage: ${Date.now() - cdpStart}ms`)
try { try {
const snapshotStart = Date.now() 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) => { const shortRefMap = new Map(refs.map((entry) => {
return [entry.ref, entry.shortRef] 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 rootHandle = locator ? await locator.elementHandle() : null
const labelsStart = Date.now() 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) => { const shortLabels = labels.map((label) => {
return { return {
...label, ...label,
@@ -1326,17 +1322,16 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void>
* await page.locator('[data-testid="submit-btn"]').click() * 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 page: Page
locator?: Locator locator?: Locator
interactiveOnly?: boolean interactiveOnly?: boolean
wsUrl?: string
collector: ScreenshotResult[] collector: ScreenshotResult[]
logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void }
}): Promise<void> { }): Promise<void> {
const log = logger?.info ?? logger?.error const log = logger?.info ?? logger?.error
const showLabelsStart = Date.now() 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) { if (log) {
log(`showAriaRefLabels: ${Date.now() - showLabelsStart}ms`) log(`showAriaRefLabels: ${Date.now() - showLabelsStart}ms`)
} }
+18 -232
View File
@@ -1,247 +1,31 @@
import WebSocket from 'ws'
import type { Page, CDPSession as PlaywrightCDPSession } from '@xmorse/playwright-core' import type { Page, CDPSession as PlaywrightCDPSession } from '@xmorse/playwright-core'
import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js' 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 * Type-safe CDP session interface using devtools-protocol ProtocolMapping.
* and Playwright's CDPSession. Use this type when you want to accept either. * Provides autocomplete and type checking for CDP commands and events.
* * Return types are inferred from the command string (e.g. 'Page.getLayoutMetrics'
* Uses loose types so Playwright's CDPSession (which uses Protocol.Events) * returns Protocol.Page.GetLayoutMetricsResponse).
* is assignable to this interface.
*/ */
export interface ICDPSession { export interface ICDPSession {
send(method: string, params?: object, sessionId?: string | null): Promise<unknown>
on(event: string, callback: (params: any) => void): unknown
off(event: string, callback: (params: any) => void): unknown
detach(): Promise<void>
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<number, PendingRequest>()
private eventListeners = new Map<string, Set<(params: unknown) => 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<K extends keyof ProtocolMapping.Commands>( send<K extends keyof ProtocolMapping.Commands>(
method: K, method: K,
params?: ProtocolMapping.Commands[K]['paramsType'][0], 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, sessionId?: string | null,
): Promise<ProtocolMapping.Commands[K]['returnType']> { ): Promise<ProtocolMapping.Commands[K]['returnType']>
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
}
return new Promise((resolve, reject) => { on<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): unknown
const timeout = setTimeout(() => {
this.pendingRequests.delete(id)
reject(new Error(`CDP command timeout: ${method}`))
}, 30000)
this.pendingRequests.set(id, { off<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): unknown
resolve: (result) => {
clearTimeout(timeout)
resolve(result as ProtocolMapping.Commands[K]['returnType'])
},
reject: (error) => {
clearTimeout(timeout)
reject(error)
},
})
try { detach(): Promise<void>
this.ws.send(JSON.stringify(message)) getSessionId?(): string | null
} catch (error) {
clearTimeout(timeout)
this.pendingRequests.delete(id)
reject(error instanceof Error ? error : new Error(String(error)))
}
})
}
on<K extends keyof ProtocolMapping.Events>(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<K extends keyof ProtocolMapping.Events>(
event: K,
callback: (params: ProtocolMapping.Events[K][0]) => void,
): this {
return this.on(event, callback)
}
off<K extends keyof ProtocolMapping.Events>(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<K extends keyof ProtocolMapping.Events>(
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<K extends keyof ProtocolMapping.Events>(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<void> {
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<CDPSession> {
const resolvedWsUrl = wsUrl || getCdpUrl()
const ws = new WebSocket(resolvedWsUrl)
await new Promise<void>((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
} }
/** /**
* Wraps Playwright's CDPSession (from context.getExistingCDPSession) into an ICDPSession. * Wraps Playwright's CDPSession (from context.getExistingCDPSession) into an ICDPSession.
* This reuses Playwright's internal CDP WebSocket instead of creating a new one, * 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. * 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 { export class PlaywrightCDPSessionAdapter implements ICDPSession {
private _playwrightSession: PlaywrightCDPSession private _playwrightSession: PlaywrightCDPSession
@@ -250,16 +34,19 @@ export class PlaywrightCDPSessionAdapter implements ICDPSession {
this._playwrightSession = playwrightSession this._playwrightSession = playwrightSession
} }
async send(method: string, params?: object): Promise<unknown> { async send<K extends keyof ProtocolMapping.Commands>(
method: K,
params?: ProtocolMapping.Commands[K]['paramsType'][0],
): Promise<ProtocolMapping.Commands[K]['returnType']> {
return await this._playwrightSession.send(method as never, params as never) return await this._playwrightSession.send(method as never, params as never)
} }
on(event: string, callback: (params: unknown) => void): this { on<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): this {
this._playwrightSession.on(event as never, callback as never) this._playwrightSession.on(event as never, callback as never)
return this return this
} }
off(event: string, callback: (params: unknown) => void): this { off<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void): this {
this._playwrightSession.off(event as never, callback as never) this._playwrightSession.off(event as never, callback as never)
return this 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. * 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 * This uses the same WebSocket Playwright already has, avoiding new connections.
* Playwright already has. Works through the relay because it doesn't call * Works through the relay because it doesn't call Target.attachToTarget.
* Target.attachToTarget.
*/ */
export async function getExistingCDPSessionForPage({ page }: { page: Page }): Promise<PlaywrightCDPSessionAdapter> { export async function getCDPSessionForPage({ page }: { page: Page }): Promise<PlaywrightCDPSessionAdapter> {
const context = page.context() const context = page.context()
const playwrightSession = await context.getExistingCDPSession(page) const playwrightSession = await context.getExistingCDPSession(page)
return new PlaywrightCDPSessionAdapter(playwrightSession) return new PlaywrightCDPSessionAdapter(playwrightSession)
+4 -4
View File
@@ -1,13 +1,13 @@
import type { Page, Locator } from '@xmorse/playwright-core' 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 { Debugger } from './debugger.js'
import type { Editor } from './editor.js' import type { Editor } from './editor.js'
import type { StylesResult } from './styles.js' import type { StylesResult } from './styles.js'
export declare const page: Page export declare const page: Page
export declare const getCDPSession: (options: { page: Page }) => Promise<CDPSession> export declare const getCDPSession: (options: { page: Page }) => Promise<ICDPSession>
export declare const createDebugger: (options: { cdp: CDPSession }) => Debugger export declare const createDebugger: (options: { cdp: ICDPSession }) => Debugger
export declare const createEditor: (options: { cdp: CDPSession }) => Editor export declare const createEditor: (options: { cdp: ICDPSession }) => Editor
export declare const getStylesForLocator: (options: { locator: Locator; includeUserAgentStyles?: boolean }) => Promise<StylesResult> export declare const getStylesForLocator: (options: { locator: Locator; includeUserAgentStyles?: boolean }) => Promise<StylesResult>
export declare const formatStylesAsText: (styles: StylesResult) => string export declare const formatStylesAsText: (styles: StylesResult) => string
export declare const console: { log: (...args: unknown[]) => void } export declare const console: { log: (...args: unknown[]) => void }
+3 -4
View File
@@ -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' import type { Protocol } from 'devtools-protocol'
export interface BreakpointInfo { export interface BreakpointInfo {
@@ -48,7 +48,7 @@ export interface ScriptInfo {
* ``` * ```
*/ */
export class Debugger { export class Debugger {
private cdp: CDPSession private cdp: ICDPSession
private debuggerEnabled = false private debuggerEnabled = false
private paused = false private paused = false
private currentCallFrames: Protocol.Debugger.CallFrame[] = [] private currentCallFrames: Protocol.Debugger.CallFrame[] = []
@@ -71,8 +71,7 @@ export class Debugger {
* ``` * ```
*/ */
constructor({ cdp }: { cdp: ICDPSession }) { constructor({ cdp }: { cdp: ICDPSession }) {
// Cast to CDPSession for internal type safety - at runtime both are compatible this.cdp = cdp
this.cdp = cdp as CDPSession
this.setupEventListeners() this.setupEventListeners()
} }
+3 -4
View File
@@ -1,4 +1,4 @@
import type { ICDPSession, CDPSession } from './cdp-session.js' import type { ICDPSession } from './cdp-session.js'
export interface ReadResult { export interface ReadResult {
content: string content: string
@@ -46,15 +46,14 @@ export interface EditResult {
* ``` * ```
*/ */
export class Editor { export class Editor {
private cdp: CDPSession private cdp: ICDPSession
private enabled = false private enabled = false
private scripts = new Map<string, string>() private scripts = new Map<string, string>()
private stylesheets = new Map<string, string>() private stylesheets = new Map<string, string>()
private sourceCache = new Map<string, string>() private sourceCache = new Map<string, string>()
constructor({ cdp }: { cdp: ICDPSession }) { constructor({ cdp }: { cdp: ICDPSession }) {
// Cast to CDPSession for internal type safety - at runtime both are compatible this.cdp = cdp
this.cdp = cdp as CDPSession
this.setupEventListeners() this.setupEventListeners()
} }
+3 -28
View File
@@ -16,7 +16,7 @@ import * as acorn from 'acorn'
import { createSmartDiff } from './diff-utils.js' import { createSmartDiff } from './diff-utils.js'
import { getCdpUrl } from './utils.js' import { getCdpUrl } from './utils.js'
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.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 { Debugger } from './debugger.js'
import { Editor } from './editor.js' import { Editor } from './editor.js'
import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js' import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
@@ -218,7 +218,7 @@ export class PlaywrightExecutor {
private browserLogs: Map<string, string[]> = new Map() private browserLogs: Map<string, string[]> = new Map()
private lastSnapshots: WeakMap<Page, string> = new WeakMap() private lastSnapshots: WeakMap<Page, string> = new WeakMap()
private lastRefToLocator: WeakMap<Page, Map<string, string>> = new WeakMap() private lastRefToLocator: WeakMap<Page, Map<string, string>> = new WeakMap()
private cdpSessionCache: WeakMap<Page, ICDPSession> = new WeakMap()
private scopedFs: ScopedFS private scopedFs: ScopedFS
private sandboxedRequire: NodeRequire private sandboxedRequire: NodeRequire
@@ -555,7 +555,6 @@ export class PlaywrightExecutor {
page: resolvedPage, page: resolvedPage,
frame, frame,
locator, locator,
wsUrl: getCdpUrl(this.cdpConfig),
interactiveOnly, interactiveOnly,
}) })
const snapshotStr = rawSnapshot.toWellFormed?.() ?? rawSnapshot const snapshotStr = rawSnapshot.toWellFormed?.() ?? rawSnapshot
@@ -717,30 +716,7 @@ export class PlaywrightExecutor {
if (options.page.isClosed()) { if (options.page.isClosed()) {
throw new Error('Cannot create CDP session for closed page') throw new Error('Cannot create CDP session for closed page')
} }
return await getCDPSessionForPage({ page: options.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
} }
const createDebugger = (options: { cdp: ICDPSession }) => new Debugger(options) const createDebugger = (options: { cdp: ICDPSession }) => new Debugger(options)
@@ -761,7 +737,6 @@ export class PlaywrightExecutor {
const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => { const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => {
return screenshotWithAccessibilityLabels({ return screenshotWithAccessibilityLabels({
...options, ...options,
wsUrl: getCdpUrl(this.cdpConfig),
collector: screenshotCollector, collector: screenshotCollector,
logger: { logger: {
info: (...args) => { info: (...args) => {
+1 -1
View File
@@ -1,6 +1,6 @@
export * from './cdp-relay.js' export * from './cdp-relay.js'
export * from './utils.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 type { ICDPSession } from './cdp-session.js'
export { Editor } from './editor.js' export { Editor } from './editor.js'
export type { ReadResult, SearchMatch, EditResult } from './editor.js' export type { ReadResult, SearchMatch, EditResult } from './editor.js'
+2 -3
View File
@@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import type { Page, Locator, ElementHandle } from '@xmorse/playwright-core' 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 { export interface ReactSourceLocation {
fileName: string | null fileName: string | null
@@ -30,8 +30,7 @@ export async function getReactSource({
locator: Locator | ElementHandle locator: Locator | ElementHandle
cdp: ICDPSession cdp: ICDPSession
}): Promise<ReactSourceLocation | null> { }): Promise<ReactSourceLocation | null> {
// Cast to CDPSession for internal type safety - at runtime both are compatible const cdp = cdpSession
const cdp = cdpSession as CDPSession
const page: Page = 'page' in locator && typeof locator.page === 'function' ? locator.page() : (locator as any)._page const page: Page = 'page' in locator && typeof locator.page === 'function' ? locator.page() : (locator as any)._page
if (!page) { if (!page) {
+2 -2
View File
@@ -55,7 +55,7 @@ describe('Relay Core Tests', () => {
await new Promise((r) => { setTimeout(r, 100) }) await new Promise((r) => { setTimeout(r, 100) })
const cdpSession = await withTimeout({ const cdpSession = await withTimeout({
promise: getCDPSessionForPage({ page, wsUrl: getCdpUrl({ port: TEST_PORT }) }), promise: getCDPSessionForPage({ page }),
timeoutMs: 10000, timeoutMs: 10000,
errorMessage: 'Timed out creating CDP session for page', errorMessage: 'Timed out creating CDP session for page',
}) })
@@ -82,7 +82,7 @@ describe('Relay Core Tests', () => {
}) })
expect(hasGlobalAfter).toEqual({ foo: 'bar' }) expect(hasGlobalAfter).toEqual({ foo: 'bar' })
cdpSession.close() await cdpSession.detach()
await page.close() await page.close()
}, 60000) }, 60000)
+29 -29
View File
@@ -53,7 +53,7 @@ describe('CDP Session Tests', () => {
await new Promise(r => setTimeout(r, 100)) await new Promise(r => setTimeout(r, 100))
const wsUrl = getCdpUrl({ port: TEST_PORT }) const wsUrl = getCdpUrl({ port: TEST_PORT })
const cdpSession = await getCDPSessionForPage({ page, wsUrl }) const cdpSession = await getCDPSessionForPage({ page })
const dbg = new Debugger({ cdp: cdpSession }) const dbg = new Debugger({ cdp: cdpSession })
await dbg.enable() await dbg.enable()
@@ -102,7 +102,7 @@ describe('CDP Session Tests', () => {
expect(dbg.isPaused()).toBe(false) expect(dbg.isPaused()).toBe(false)
await evalPromise await evalPromise
cdpSession.close() await cdpSession.detach()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -161,7 +161,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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 }) const dbg = new Debugger({ cdp: cdpSession })
await dbg.enable() await dbg.enable()
@@ -171,7 +171,7 @@ describe('CDP Session Tests', () => {
expect(scripts[0]).toHaveProperty('scriptId') expect(scripts[0]).toHaveProperty('scriptId')
expect(scripts[0]).toHaveProperty('url') expect(scripts[0]).toHaveProperty('url')
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -197,7 +197,7 @@ describe('CDP Session Tests', () => {
await new Promise(r => setTimeout(r, 100)) await new Promise(r => setTimeout(r, 100))
const wsUrl = getCdpUrl({ port: TEST_PORT }) const wsUrl = getCdpUrl({ port: TEST_PORT })
const cdpSession = await getCDPSessionForPage({ page, wsUrl }) const cdpSession = await getCDPSessionForPage({ page })
const dbg = new Debugger({ cdp: cdpSession }) const dbg = new Debugger({ cdp: cdpSession })
await dbg.enable() await dbg.enable()
@@ -216,7 +216,7 @@ describe('CDP Session Tests', () => {
await dbg.deleteBreakpoint({ breakpointId: bpId }) await dbg.deleteBreakpoint({ breakpointId: bpId })
expect(dbg.listBreakpoints()).toHaveLength(0) expect(dbg.listBreakpoints()).toHaveLength(0)
cdpSession.close() await cdpSession.detach()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -234,7 +234,7 @@ describe('CDP Session Tests', () => {
await new Promise(r => setTimeout(r, 100)) await new Promise(r => setTimeout(r, 100))
const wsUrl = getCdpUrl({ port: TEST_PORT }) const wsUrl = getCdpUrl({ port: TEST_PORT })
const cdpSession = await getCDPSessionForPage({ page, wsUrl }) const cdpSession = await getCDPSessionForPage({ page })
const dbg = new Debugger({ cdp: cdpSession }) const dbg = new Debugger({ cdp: cdpSession })
await dbg.enable() await dbg.enable()
@@ -285,7 +285,7 @@ describe('CDP Session Tests', () => {
await dbg.resume() await dbg.resume()
await evalPromise await evalPromise
cdpSession.close() await cdpSession.detach()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -304,7 +304,7 @@ describe('CDP Session Tests', () => {
await new Promise(r => setTimeout(r, 100)) await new Promise(r => setTimeout(r, 100))
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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.enable')
await cdpSession.send('Profiler.start') await cdpSession.send('Profiler.start')
@@ -336,7 +336,7 @@ describe('CDP Session Tests', () => {
expect(functionNames.every((name) => typeof name === 'string')).toBe(true) expect(functionNames.every((name) => typeof name === 'string')).toBe(true)
await cdpSession.send('Profiler.disable') await cdpSession.send('Profiler.disable')
cdpSession.close() await cdpSession.detach()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -362,7 +362,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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 initialTargets = await cdpSession.send('Target.getTargets')
const initialPageTarget = initialTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('example.com')) 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')) const exampleOrgTargets = allPageTargets.filter(t => t.url.includes('example.org'))
expect(exampleOrgTargets).toHaveLength(1) expect(exampleOrgTargets).toHaveLength(1)
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -417,7 +417,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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 { targetInfos } = await cdpSession.send('Target.getTargets')
const allPageTargets = targetInfos.filter(t => t.type === 'page') 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 browser.close()
await page1.close() await page1.close()
await page2.close() await page2.close()
@@ -469,7 +469,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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', { const evalResult = await cdpSession.send('Runtime.evaluate', {
expression: 'document.title', expression: 'document.title',
@@ -477,7 +477,7 @@ describe('CDP Session Tests', () => {
}) })
expect(evalResult.result.value).toContain('Example Domain') expect(evalResult.result.value).toContain('Example Domain')
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -501,7 +501,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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', { const initialEvalResult = await cdpSession.send('Runtime.evaluate', {
expression: 'document.title', expression: 'document.title',
@@ -530,7 +530,7 @@ describe('CDP Session Tests', () => {
}) })
expect(locationResult.result.value).toBe(newUrl) expect(locationResult.result.value).toBe(newUrl)
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -553,7 +553,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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 }) const dbg = new Debugger({ cdp: cdpSession })
await dbg.enable() await dbg.enable()
@@ -588,7 +588,7 @@ describe('CDP Session Tests', () => {
await dbg.setPauseOnExceptions({ state: 'none' }) await dbg.setPauseOnExceptions({ state: 'none' })
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -637,7 +637,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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 }) const dbg = new Debugger({ cdp: cdpSession })
await dbg.enable() await dbg.enable()
@@ -676,7 +676,7 @@ describe('CDP Session Tests', () => {
// Wait for evaluate to complete after resume // Wait for evaluate to complete after resume
await evalPromise await evalPromise
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -740,7 +740,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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 }) const editor = new Editor({ cdp: cdpSession })
await editor.enable() await editor.enable()
@@ -784,7 +784,7 @@ describe('CDP Session Tests', () => {
expect(consoleLogs).toContain('Hello, World') expect(consoleLogs).toContain('Hello, World')
expect(consoleLogs).toContain('EDITOR_TEST_MARKER') expect(consoleLogs).toContain('EDITOR_TEST_MARKER')
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -807,7 +807,7 @@ describe('CDP Session Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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 }) const editor = new Editor({ cdp: cdpSession })
await editor.enable() await editor.enable()
@@ -859,7 +859,7 @@ describe('CDP Session Tests', () => {
}) })
expect(colorAfter).toBe('rgb(0, 255, 0)') expect(colorAfter).toBe('rgb(0, 255, 0)')
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -908,7 +908,7 @@ describe('CDP Session Tests', () => {
expect(hasBippyBefore).toBe(false) expect(hasBippyBefore).toBe(false)
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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 { getReactSource } = await import('./react-source.js')
const source = await getReactSource({ locator: btn, cdp: cdpSession }) const source = await getReactSource({ locator: btn, cdp: cdpSession })
@@ -1017,7 +1017,7 @@ describe('Service Worker Target Tests', () => {
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) 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.disable')
await cdpSession.send('Network.enable', { await cdpSession.send('Network.enable', {
@@ -1036,7 +1036,7 @@ describe('Service Worker Target Tests', () => {
expect(body).toContain('Example Domain') expect(body).toContain('Example Domain')
expect(body).toContain('</html>') expect(body).toContain('</html>')
cdpSession.close() await cdpSession.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
+6 -38
View File
@@ -8,7 +8,7 @@ import fs from 'node:fs'
import os from 'node:os' import os from 'node:os'
import { imageSize } from 'image-size' import { imageSize } from 'image-size'
import { getCdpUrl } from './utils.js' 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 type { CDPCommand } from './cdp-types.js'
import { screenshotWithAccessibilityLabels } from './aria-snapshot.js' import { screenshotWithAccessibilityLabels } from './aria-snapshot.js'
import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, type TestContext, js } from './test-utils.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')) const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl({ port: TEST_PORT }) const cdpSession = await getCDPSessionForPage({ page: cdpPage! })
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics') const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
@@ -540,37 +539,7 @@ describe('Snapshot & Screenshot Tests', () => {
console.log('window.devicePixelRatio:', windowDpr) console.log('window.devicePixelRatio:', windowDpr)
expect(windowDpr).toBe(1) expect(windowDpr).toBe(1)
cdpSession.close() await cdpSession.detach()
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 browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -593,8 +562,8 @@ describe('Snapshot & Screenshot Tests', () => {
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com')) const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
expect(cdpPage).toBeDefined() expect(cdpPage).toBeDefined()
// Use the new getExistingCDPSessionForPage which reuses Playwright's internal WS // Use the new getCDPSessionForPage which reuses Playwright's internal WS
const cdpClient = await getExistingCDPSessionForPage({ page: cdpPage! }) const cdpClient = await getCDPSessionForPage({ page: cdpPage! })
// Should be able to send CDP commands just like the regular getCDPSessionForPage // Should be able to send CDP commands just like the regular getCDPSessionForPage
const layoutMetrics = await cdpClient.send('Page.getLayoutMetrics') const layoutMetrics = await cdpClient.send('Page.getLayoutMetrics')
@@ -649,7 +618,6 @@ describe('Snapshot & Screenshot Tests', () => {
const ariaResult = await getAriaSnapshot({ const ariaResult = await getAriaSnapshot({
page: cdpPage!, page: cdpPage!,
wsUrl: getCdpUrl({ port: TEST_PORT }),
}) })
expect(ariaResult.snapshot).toBeDefined() expect(ariaResult.snapshot).toBeDefined()
@@ -812,7 +780,7 @@ describe('Snapshot & Screenshot Tests', () => {
const { labelCount } = await withTimeout( const { labelCount } = await withTimeout(
`showAriaRefLabels(${name})`, `showAriaRefLabels(${name})`,
async () => { async () => {
return await showAriaRefLabels({ page: cdpPage, wsUrl }) return await showAriaRefLabels({ page: cdpPage })
}, },
60000 60000
) )
+3 -4
View File
@@ -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' import type { Locator } from '@xmorse/playwright-core'
export interface StyleSource { export interface StyleSource {
@@ -73,8 +73,7 @@ export async function getStylesForLocator({
cdp: ICDPSession cdp: ICDPSession
includeUserAgentStyles?: boolean includeUserAgentStyles?: boolean
}): Promise<StylesResult> { }): Promise<StylesResult> {
// Cast to CDPSession for internal type safety - at runtime both are compatible const cdp = cdpSession
const cdp = cdpSession as CDPSession
await cdp.send('DOM.enable') await cdp.send('DOM.enable')
await cdp.send('CSS.enable') await cdp.send('CSS.enable')
@@ -272,7 +271,7 @@ function formatElementDescription(node: any): string {
return desc return desc
} }
async function getStylesheetUrl(cdp: CDPSession, styleSheetId: string): Promise<string> { async function getStylesheetUrl(cdp: ICDPSession, styleSheetId: string): Promise<string> {
try { try {
await cdp.send('CSS.getStyleSheetText', { styleSheetId }) await cdp.send('CSS.getStyleSheetText', { styleSheetId })
return `stylesheet:${styleSheetId}` return `stylesheet:${styleSheetId}`