From efb1e1dd74f7596077c881d6348b5ab87940bb65 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 6 Mar 2026 22:10:09 -0800 Subject: [PATCH] feat(cdp): add 'event' and 'close' events to CDPSession - 'event' fires for every CDP event with { name, params? }, allowing subscriptions to all CDP events without knowing names ahead of time - 'close' fires when the session is detached or the target closes --- .claude/skills/playwright-dev/api.md | 4 +- docs/src/api/class-cdpsession.md | 24 +++ packages/playwright-client/types/types.d.ts | 160 +++++++++++++++++- .../playwright-core/src/client/cdpSession.ts | 5 + .../playwright-core/src/protocol/validator.ts | 1 + .../dispatchers/cdpSessionDispatcher.ts | 5 +- packages/playwright-core/types/types.d.ts | 160 +++++++++++++++++- packages/protocol/src/channels.d.ts | 3 + packages/protocol/src/protocol.yml | 2 + tests/library/chromium/session.spec.ts | 30 ++++ utils/generate_types/overrides.d.ts | 10 +- 11 files changed, 387 insertions(+), 17 deletions(-) diff --git a/.claude/skills/playwright-dev/api.md b/.claude/skills/playwright-dev/api.md index 06431bbe61c62..2ef9ecb7d43ab 100644 --- a/.claude/skills/playwright-dev/api.md +++ b/.claude/skills/playwright-dev/api.md @@ -30,7 +30,7 @@ Description of the option. ``` **Key syntax rules:** -- `* since: v1.XX` — version from package.json (without -next) +- `* since: v1.XX` — always take the version from package.json (without -next) - `* langs: js, python` — language filter (optional) - `* langs: alias-java: navigate` — language-specific method name - `* deprecated: v1.XX` — deprecation marker @@ -60,6 +60,8 @@ Description. Description. ``` +Keep methods, events and property definitions sorted alphabetically within the file. + Watch will kick in and auto-generate: - `packages/playwright-core/types/types.d.ts` — public API types - `packages/playwright/types/test.d.ts` — test API types diff --git a/docs/src/api/class-cdpsession.md b/docs/src/api/class-cdpsession.md index a14dd58fb4ffc..8efa8e1da71ad 100644 --- a/docs/src/api/class-cdpsession.md +++ b/docs/src/api/class-cdpsession.md @@ -67,6 +67,30 @@ params.addProperty("playbackRate", playbackRate / 2); client.send("Animation.setPlaybackRate", params); ``` +## event: CDPSession.close +* since: v1.59 +* langs: js + +Emitted when the session is closed, either because the target was closed or `session.detach()` was called. + +## event: CDPSession.event +* since: v1.59 +* langs: js +- argument: <[Object]> + - `name` <[string]> CDP event name. + - `params` ?<[Object]> CDP event parameters. + +Emitted for every CDP event received from the session. Allows subscribing to all CDP events at once without knowing +their names ahead of time. + +**Usage** + +```js +session.on('event', ({ name, params }) => { + console.log(`CDP event: ${name}`, params); +}); +``` + ## async method: CDPSession.detach * since: v1.8 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 815eb462afc7f..2048ef36657b7 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -16023,11 +16023,11 @@ export interface BrowserType { * */ export interface CDPSession { - on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + on(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + addListener(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + off(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + removeListener(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + once(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; /** * @param method Protocol method name. * @param params Optional method parameters. @@ -16036,6 +16036,156 @@ export interface CDPSession { method: T, params?: Protocol.CommandParameters[T] ): Promise; + /** + * Emitted when the session is closed, either because the target was closed or `session.detach()` was called. + */ + on(event: 'close', listener: () => any): this; + + /** + * Emitted for every CDP event received from the session. Allows subscribing to all CDP events at once without knowing + * their names ahead of time. + * + * **Usage** + * + * ```js + * session.on('event', ({ name, params }) => { + * console.log(`CDP event: ${name}`, params); + * }); + * ``` + * + */ + on(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. + */ + once(event: 'close', listener: () => any): this; + + /** + * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. + */ + once(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Emitted when the session is closed, either because the target was closed or `session.detach()` was called. + */ + addListener(event: 'close', listener: () => any): this; + + /** + * Emitted for every CDP event received from the session. Allows subscribing to all CDP events at once without knowing + * their names ahead of time. + * + * **Usage** + * + * ```js + * session.on('event', ({ name, params }) => { + * console.log(`CDP event: ${name}`, params); + * }); + * ``` + * + */ + addListener(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'close', listener: () => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'close', listener: () => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Emitted when the session is closed, either because the target was closed or `session.detach()` was called. + */ + prependListener(event: 'close', listener: () => any): this; + + /** + * Emitted for every CDP event received from the session. Allows subscribing to all CDP events at once without knowing + * their names ahead of time. + * + * **Usage** + * + * ```js + * session.on('event', ({ name, params }) => { + * console.log(`CDP event: ${name}`, params); + * }); + * ``` + * + */ + prependListener(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + /** * Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be * used to send messages. diff --git a/packages/playwright-core/src/client/cdpSession.ts b/packages/playwright-core/src/client/cdpSession.ts index 0a58f79d27bba..25f374af79209 100644 --- a/packages/playwright-core/src/client/cdpSession.ts +++ b/packages/playwright-core/src/client/cdpSession.ts @@ -30,6 +30,11 @@ export class CDPSession extends ChannelOwner impleme this._channel.on('event', ({ method, params }) => { this.emit(method, params); + this.emit('event', { name: method, params }); + }); + + this._channel.on('close', () => { + this.emit('close'); }); this.on = super.on; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 2a31b6c52183d..d8fc33c1bd78b 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2609,6 +2609,7 @@ scheme.CDPSessionEventEvent = tObject({ method: tString, params: tOptional(tAny), }); +scheme.CDPSessionCloseEvent = tOptional(tObject({})); scheme.CDPSessionSendParams = tObject({ method: tString, params: tOptional(tAny), diff --git a/packages/playwright-core/src/server/dispatchers/cdpSessionDispatcher.ts b/packages/playwright-core/src/server/dispatchers/cdpSessionDispatcher.ts index 58d785705ef3f..8e16795e24259 100644 --- a/packages/playwright-core/src/server/dispatchers/cdpSessionDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/cdpSessionDispatcher.ts @@ -28,7 +28,10 @@ export class CDPSessionDispatcher extends Dispatcher this._dispatchEvent('event', { method, params })); - this.addObjectListener(CDPSession.Events.Closed, () => this._dispose()); + this.addObjectListener(CDPSession.Events.Closed, () => { + this._dispatchEvent('close'); + this._dispose(); + }); } async send(params: channels.CDPSessionSendParams, progress: Progress): Promise { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 815eb462afc7f..2048ef36657b7 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16023,11 +16023,11 @@ export interface BrowserType { * */ export interface CDPSession { - on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + on(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + addListener(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + off(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + removeListener(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + once(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; /** * @param method Protocol method name. * @param params Optional method parameters. @@ -16036,6 +16036,156 @@ export interface CDPSession { method: T, params?: Protocol.CommandParameters[T] ): Promise; + /** + * Emitted when the session is closed, either because the target was closed or `session.detach()` was called. + */ + on(event: 'close', listener: () => any): this; + + /** + * Emitted for every CDP event received from the session. Allows subscribing to all CDP events at once without knowing + * their names ahead of time. + * + * **Usage** + * + * ```js + * session.on('event', ({ name, params }) => { + * console.log(`CDP event: ${name}`, params); + * }); + * ``` + * + */ + on(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. + */ + once(event: 'close', listener: () => any): this; + + /** + * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. + */ + once(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Emitted when the session is closed, either because the target was closed or `session.detach()` was called. + */ + addListener(event: 'close', listener: () => any): this; + + /** + * Emitted for every CDP event received from the session. Allows subscribing to all CDP events at once without knowing + * their names ahead of time. + * + * **Usage** + * + * ```js + * session.on('event', ({ name, params }) => { + * console.log(`CDP event: ${name}`, params); + * }); + * ``` + * + */ + addListener(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'close', listener: () => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'close', listener: () => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + + /** + * Emitted when the session is closed, either because the target was closed or `session.detach()` was called. + */ + prependListener(event: 'close', listener: () => any): this; + + /** + * Emitted for every CDP event received from the session. Allows subscribing to all CDP events at once without knowing + * their names ahead of time. + * + * **Usage** + * + * ```js + * session.on('event', ({ name, params }) => { + * console.log(`CDP event: ${name}`, params); + * }); + * ``` + * + */ + prependListener(event: 'event', listener: (data: { + /** + * CDP event name. + */ + name: string; + + /** + * CDP event parameters. + */ + params?: Object; + }) => any): this; + /** * Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be * used to send messages. diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index ed98799521805..eac927eee8d24 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -4550,6 +4550,7 @@ export interface WritableStreamEvents { export type CDPSessionInitializer = {}; export interface CDPSessionEventTarget { on(event: 'event', callback: (params: CDPSessionEventEvent) => void): this; + on(event: 'close', callback: (params: CDPSessionCloseEvent) => void): this; } export interface CDPSessionChannel extends CDPSessionEventTarget, Channel { _type_CDPSession: boolean; @@ -4560,6 +4561,7 @@ export type CDPSessionEventEvent = { method: string, params?: any, }; +export type CDPSessionCloseEvent = {}; export type CDPSessionSendParams = { method: string, params?: any, @@ -4576,6 +4578,7 @@ export type CDPSessionDetachResult = void; export interface CDPSessionEvents { 'event': CDPSessionEventEvent; + 'close': CDPSessionCloseEvent; } // ----------- Electron ----------- diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index a812775aee472..bb03e231aee16 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3984,6 +3984,8 @@ CDPSession: method: string params: json? + close: + Electron: type: interface diff --git a/tests/library/chromium/session.spec.ts b/tests/library/chromium/session.spec.ts index c2069a7a3ca11..5e7789b7bd7b9 100644 --- a/tests/library/chromium/session.spec.ts +++ b/tests/library/chromium/session.spec.ts @@ -146,6 +146,36 @@ browserTest('should reject protocol calls when page closes', async function({ br await context.close(); }); +it('should emit event for each CDP event', async function({ page, server }) { + const client = await page.context().newCDPSession(page); + await client.send('Network.enable'); + const events = []; + client.on('event', event => events.push(event)); + await page.goto(server.EMPTY_PAGE); + expect(events.length).toBeGreaterThan(0); + const requestEvent = events.find(e => e.name === 'Network.requestWillBeSent'); + expect(requestEvent).toBeTruthy(); + expect(requestEvent.params.request.url).toBe(server.EMPTY_PAGE); +}); + +it('should emit close event when session is detached', async function({ page }) { + const client = await page.context().newCDPSession(page); + let closeFired = false; + client.on('close', () => closeFired = true); + await client.detach(); + expect(closeFired).toBe(true); +}); + +browserTest('should emit close event when page closes', async function({ browser }) { + const context = await browser.newContext(); + const page = await context.newPage(); + const session = await context.newCDPSession(page); + const closePromise = new Promise(f => session.on('close', f)); + await page.close(); + await closePromise; + await context.close(); +}); + browserTest('should work with newBrowserCDPSession', async function({ browser }) { const session = await browser.newBrowserCDPSession(); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index d34403fb5d731..f5677d8f6150d 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -229,11 +229,11 @@ export interface BrowserType { } export interface CDPSession { - on: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - addListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - off: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - removeListener: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; - once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; + on(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + addListener(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + off(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + removeListener(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; + once(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void): this; send( method: T, params?: Protocol.CommandParameters[T]