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)
|
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')
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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,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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
Reference in New Issue
Block a user