Add Playwright CDPSession compatibility and export classes

- Add ICDPSession interface for Playwright CDPSession compatibility
- Add method aliases: addListener, removeListener, once, detach
- Make on/off return 'this' for chaining
- Export CDPSession, Editor, Debugger classes from index
- Classes accept ICDPSession but use typed CDPSession internally
This commit is contained in:
Tommy D. Rossi
2025-12-31 11:51:03 +01:00
parent a5f57ea5d6
commit 0f9591722d
7 changed files with 81 additions and 21 deletions
+50 -3
View File
@@ -3,12 +3,26 @@ import type { Page } from 'playwright-core'
import type { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'
import type { CDPResponseBase, CDPEventBase } from './cdp-types.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.
*/
export interface ICDPSession {
send(method: string, params?: object): Promise<unknown>
on(event: string, callback: (params: any) => void): unknown
off(event: string, callback: (params: any) => void): unknown
detach(): Promise<void>
}
interface PendingRequest {
resolve: (result: unknown) => void
reject: (error: Error) => void
}
export class CDPSession {
export class CDPSession implements ICDPSession {
private ws: WebSocket
private pendingRequests = new Map<number, PendingRequest>()
private eventListeners = new Map<string, Set<(params: unknown) => void>>()
@@ -90,15 +104,48 @@ export class CDPSession {
})
}
on<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void) {
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
}
off<K extends keyof ProtocolMapping.Events>(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void) {
/** 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() {
+6 -4
View File
@@ -1,4 +1,4 @@
import type { CDPSession } from './cdp-session.js'
import type { ICDPSession, CDPSession } from './cdp-session.js'
import type { Protocol } from 'devtools-protocol'
export interface BreakpointInfo {
@@ -61,7 +61,8 @@ export class Debugger {
* Creates a new Debugger instance.
*
* @param options - Configuration options
* @param options.cdp - A CDPSession instance for sending CDP commands
* @param options.cdp - A CDPSession instance for sending CDP commands (works with both
* our CDPSession and Playwright's CDPSession)
*
* @example
* ```ts
@@ -69,8 +70,9 @@ export class Debugger {
* const dbg = new Debugger({ cdp })
* ```
*/
constructor({ cdp }: { cdp: CDPSession }) {
this.cdp = cdp
constructor({ cdp }: { cdp: ICDPSession }) {
// Cast to CDPSession for internal type safety - at runtime both are compatible
this.cdp = cdp as CDPSession
this.setupEventListeners()
}
+4 -3
View File
@@ -1,4 +1,4 @@
import type { CDPSession } from './cdp-session.js'
import type { ICDPSession, CDPSession } from './cdp-session.js'
export interface ReadResult {
content: string
@@ -52,8 +52,9 @@ export class Editor {
private stylesheets = new Map<string, string>()
private sourceCache = new Map<string, string>()
constructor({ cdp }: { cdp: CDPSession }) {
this.cdp = cdp
constructor({ cdp }: { cdp: ICDPSession }) {
// Cast to CDPSession for internal type safety - at runtime both are compatible
this.cdp = cdp as CDPSession
this.setupEventListeners()
}
+6
View File
@@ -1,2 +1,8 @@
export * from './cdp-relay.js'
export * from './utils.js'
export { CDPSession, getCDPSessionForPage } from './cdp-session.js'
export type { ICDPSession } from './cdp-session.js'
export { Editor } from './editor.js'
export type { ReadResult, SearchMatch, EditResult } from './editor.js'
export { Debugger } from './debugger.js'
export type { BreakpointInfo, LocationInfo, EvaluateResult, ScriptInfo } from './debugger.js'
+5 -5
View File
@@ -15,7 +15,7 @@ import { createPatch } from 'diff'
import { getCdpUrl, LOG_FILE_PATH, VERSION, sleep } from './utils.js'
import { killPortProcess } from 'kill-port-process'
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
import { getCDPSessionForPage, CDPSession, ICDPSession } from './cdp-session.js'
import { Debugger } from './debugger.js'
import { Editor } from './editor.js'
import { getStylesForLocator, formatStylesAsText, type StylesResult } from './styles.js'
@@ -77,8 +77,8 @@ interface VMContext {
clearAllLogs: () => void
waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
getCDPSession: (options: { page: Page }) => Promise<CDPSession>
createDebugger: (options: { cdp: CDPSession }) => Debugger
createEditor: (options: { cdp: CDPSession }) => Editor
createDebugger: (options: { cdp: ICDPSession }) => Debugger
createEditor: (options: { cdp: ICDPSession }) => Editor
getStylesForLocator: (options: { locator: any }) => Promise<StylesResult>
formatStylesAsText: (styles: StylesResult) => string
getReactSource: (options: { locator: any }) => Promise<ReactSourceLocation | null>
@@ -675,11 +675,11 @@ server.tool(
return session
}
const createDebugger = (options: { cdp: CDPSession }) => {
const createDebugger = (options: { cdp: ICDPSession }) => {
return new Debugger(options)
}
const createEditor = (options: { cdp: CDPSession }) => {
const createEditor = (options: { cdp: ICDPSession }) => {
return new Editor(options)
}
+5 -3
View File
@@ -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 'playwright-core'
import type { CDPSession } from './cdp-session.js'
import type { ICDPSession, CDPSession } from './cdp-session.js'
export interface ReactSourceLocation {
fileName: string | null
@@ -25,11 +25,13 @@ function getBippyCode(): string {
export async function getReactSource({
locator,
cdp,
cdp: cdpSession,
}: {
locator: Locator | ElementHandle
cdp: CDPSession
cdp: ICDPSession
}): Promise<ReactSourceLocation | null> {
// Cast to CDPSession for internal type safety - at runtime both are compatible
const cdp = cdpSession as CDPSession
const page: Page = 'page' in locator && typeof locator.page === 'function' ? locator.page() : (locator as any)._page
if (!page) {
+5 -3
View File
@@ -1,4 +1,4 @@
import type { CDPSession } from './cdp-session.js'
import type { ICDPSession, CDPSession } from './cdp-session.js'
import type { Locator } from 'playwright-core'
export interface StyleSource {
@@ -66,13 +66,15 @@ interface CSSStyleSheetHeader {
export async function getStylesForLocator({
locator,
cdp,
cdp: cdpSession,
includeUserAgentStyles = false,
}: {
locator: Locator
cdp: CDPSession
cdp: ICDPSession
includeUserAgentStyles?: boolean
}): Promise<StylesResult> {
// Cast to CDPSession for internal type safety - at runtime both are compatible
const cdp = cdpSession as CDPSession
await cdp.send('DOM.enable')
await cdp.send('CSS.enable')