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:
@@ -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')
|
||||
|
||||
@@ -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<AriaSnapshotResult> {
|
||||
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<T>(value: T): value is NonNullable<T> {
|
||||
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<AriaLabel[]> {
|
||||
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<void>
|
||||
* 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<void> {
|
||||
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`)
|
||||
}
|
||||
|
||||
+18
-232
@@ -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<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>(
|
||||
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<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
|
||||
}
|
||||
): Promise<ProtocolMapping.Commands[K]['returnType']>
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(id)
|
||||
reject(new Error(`CDP command timeout: ${method}`))
|
||||
}, 30000)
|
||||
on<K extends keyof ProtocolMapping.Events>(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<K extends keyof ProtocolMapping.Events>(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<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
|
||||
detach(): Promise<void>
|
||||
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<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)
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
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<PlaywrightCDPSessionAdapter> {
|
||||
export async function getCDPSessionForPage({ page }: { page: Page }): Promise<PlaywrightCDPSessionAdapter> {
|
||||
const context = page.context()
|
||||
const playwrightSession = await context.getExistingCDPSession(page)
|
||||
return new PlaywrightCDPSessionAdapter(playwrightSession)
|
||||
|
||||
@@ -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<CDPSession>
|
||||
export declare const createDebugger: (options: { cdp: CDPSession }) => Debugger
|
||||
export declare const createEditor: (options: { cdp: CDPSession }) => Editor
|
||||
export declare const getCDPSession: (options: { page: Page }) => Promise<ICDPSession>
|
||||
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<StylesResult>
|
||||
export declare const formatStylesAsText: (styles: StylesResult) => string
|
||||
export declare const console: { log: (...args: unknown[]) => void }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, string>()
|
||||
private stylesheets = new Map<string, string>()
|
||||
private sourceCache = new Map<string, string>()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, string[]> = new Map()
|
||||
private lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
||||
private lastRefToLocator: WeakMap<Page, Map<string, string>> = new WeakMap()
|
||||
private cdpSessionCache: WeakMap<Page, ICDPSession> = 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) => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<ReactSourceLocation | null> {
|
||||
// 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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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('</html>')
|
||||
|
||||
cdpSession.close()
|
||||
await cdpSession.detach()
|
||||
await browser.close()
|
||||
await page.close()
|
||||
}, 60000)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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<StylesResult> {
|
||||
// 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<string> {
|
||||
async function getStylesheetUrl(cdp: ICDPSession, styleSheetId: string): Promise<string> {
|
||||
try {
|
||||
await cdp.send('CSS.getStyleSheetText', { styleSheetId })
|
||||
return `stylesheet:${styleSheetId}`
|
||||
|
||||
Reference in New Issue
Block a user