diff --git a/playwriter/src/cdp-session.ts b/playwriter/src/cdp-session.ts index 9f5014e..14e9abd 100644 --- a/playwriter/src/cdp-session.ts +++ b/playwriter/src/cdp-session.ts @@ -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 + on(event: string, callback: (params: any) => void): unknown + off(event: string, callback: (params: any) => void): unknown + detach(): Promise +} + 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() private eventListeners = new Map void>>() @@ -90,15 +104,48 @@ export class CDPSession { }) } - on(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void) { + on(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(event: K, callback: (params: ProtocolMapping.Events[K][0]) => void) { + /** Alias for `on` - matches Playwright's CDPSession interface */ + addListener( + event: K, + callback: (params: ProtocolMapping.Events[K][0]) => void, + ): this { + return this.on(event, callback) + } + + off(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( + 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(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 { + this.close() + return Promise.resolve() } close() { diff --git a/playwriter/src/debugger.ts b/playwriter/src/debugger.ts index 06ac4bf..c442065 100644 --- a/playwriter/src/debugger.ts +++ b/playwriter/src/debugger.ts @@ -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() } diff --git a/playwriter/src/editor.ts b/playwriter/src/editor.ts index e67abaf..18355c8 100644 --- a/playwriter/src/editor.ts +++ b/playwriter/src/editor.ts @@ -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() private sourceCache = new Map() - 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() } diff --git a/playwriter/src/index.ts b/playwriter/src/index.ts index ef27357..32d8288 100644 --- a/playwriter/src/index.ts +++ b/playwriter/src/index.ts @@ -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' diff --git a/playwriter/src/mcp.ts b/playwriter/src/mcp.ts index ad8e3c3..8e75d7e 100644 --- a/playwriter/src/mcp.ts +++ b/playwriter/src/mcp.ts @@ -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 getCDPSession: (options: { page: Page }) => Promise - 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 formatStylesAsText: (styles: StylesResult) => string getReactSource: (options: { locator: any }) => Promise @@ -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) } diff --git a/playwriter/src/react-source.ts b/playwriter/src/react-source.ts index 910d1f8..b810694 100644 --- a/playwriter/src/react-source.ts +++ b/playwriter/src/react-source.ts @@ -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 { + // 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) { diff --git a/playwriter/src/styles.ts b/playwriter/src/styles.ts index b055bf6..3a104d3 100644 --- a/playwriter/src/styles.ts +++ b/playwriter/src/styles.ts @@ -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 { + // 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')