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 { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'
import type { CDPResponseBase, CDPEventBase } from './cdp-types.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 { interface PendingRequest {
resolve: (result: unknown) => void resolve: (result: unknown) => void
reject: (error: Error) => void reject: (error: Error) => void
} }
export class CDPSession { export class CDPSession implements ICDPSession {
private ws: WebSocket private ws: WebSocket
private pendingRequests = new Map<number, PendingRequest>() private pendingRequests = new Map<number, PendingRequest>()
private eventListeners = new Map<string, Set<(params: unknown) => void>>() 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)) { if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set()) this.eventListeners.set(event, new Set())
} }
this.eventListeners.get(event)!.add(callback as (params: unknown) => void) 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) 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() { 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' import type { Protocol } from 'devtools-protocol'
export interface BreakpointInfo { export interface BreakpointInfo {
@@ -61,7 +61,8 @@ export class Debugger {
* Creates a new Debugger instance. * Creates a new Debugger instance.
* *
* @param options - Configuration options * @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 * @example
* ```ts * ```ts
@@ -69,8 +70,9 @@ export class Debugger {
* const dbg = new Debugger({ cdp }) * const dbg = new Debugger({ cdp })
* ``` * ```
*/ */
constructor({ cdp }: { cdp: CDPSession }) { constructor({ cdp }: { cdp: ICDPSession }) {
this.cdp = cdp // Cast to CDPSession for internal type safety - at runtime both are compatible
this.cdp = cdp as CDPSession
this.setupEventListeners() 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 { export interface ReadResult {
content: string content: string
@@ -52,8 +52,9 @@ export class Editor {
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: CDPSession }) { constructor({ cdp }: { cdp: ICDPSession }) {
this.cdp = cdp // Cast to CDPSession for internal type safety - at runtime both are compatible
this.cdp = cdp as CDPSession
this.setupEventListeners() this.setupEventListeners()
} }
+6
View File
@@ -1,2 +1,8 @@
export * from './cdp-relay.js' export * from './cdp-relay.js'
export * from './utils.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 { getCdpUrl, LOG_FILE_PATH, VERSION, sleep } from './utils.js'
import { killPortProcess } from 'kill-port-process' import { killPortProcess } from 'kill-port-process'
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js' 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 { 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'
@@ -77,8 +77,8 @@ interface VMContext {
clearAllLogs: () => void clearAllLogs: () => void
waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult> waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
getCDPSession: (options: { page: Page }) => Promise<CDPSession> getCDPSession: (options: { page: Page }) => Promise<CDPSession>
createDebugger: (options: { cdp: CDPSession }) => Debugger createDebugger: (options: { cdp: ICDPSession }) => Debugger
createEditor: (options: { cdp: CDPSession }) => Editor createEditor: (options: { cdp: ICDPSession }) => Editor
getStylesForLocator: (options: { locator: any }) => Promise<StylesResult> getStylesForLocator: (options: { locator: any }) => Promise<StylesResult>
formatStylesAsText: (styles: StylesResult) => string formatStylesAsText: (styles: StylesResult) => string
getReactSource: (options: { locator: any }) => Promise<ReactSourceLocation | null> getReactSource: (options: { locator: any }) => Promise<ReactSourceLocation | null>
@@ -675,11 +675,11 @@ server.tool(
return session return session
} }
const createDebugger = (options: { cdp: CDPSession }) => { const createDebugger = (options: { cdp: ICDPSession }) => {
return new Debugger(options) return new Debugger(options)
} }
const createEditor = (options: { cdp: CDPSession }) => { const createEditor = (options: { cdp: ICDPSession }) => {
return new Editor(options) return new Editor(options)
} }
+5 -3
View File
@@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import type { Page, Locator, ElementHandle } from 'playwright-core' 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 { export interface ReactSourceLocation {
fileName: string | null fileName: string | null
@@ -25,11 +25,13 @@ function getBippyCode(): string {
export async function getReactSource({ export async function getReactSource({
locator, locator,
cdp, cdp: cdpSession,
}: { }: {
locator: Locator | ElementHandle locator: Locator | ElementHandle
cdp: CDPSession cdp: ICDPSession
}): Promise<ReactSourceLocation | null> { }): 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 const page: Page = 'page' in locator && typeof locator.page === 'function' ? locator.page() : (locator as any)._page
if (!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' import type { Locator } from 'playwright-core'
export interface StyleSource { export interface StyleSource {
@@ -66,13 +66,15 @@ interface CSSStyleSheetHeader {
export async function getStylesForLocator({ export async function getStylesForLocator({
locator, locator,
cdp, cdp: cdpSession,
includeUserAgentStyles = false, includeUserAgentStyles = false,
}: { }: {
locator: Locator locator: Locator
cdp: CDPSession 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 as CDPSession
await cdp.send('DOM.enable') await cdp.send('DOM.enable')
await cdp.send('CSS.enable') await cdp.send('CSS.enable')