From e284dedf0c3b0d051026595f017f715bdd192fb6 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 18 Mar 2026 04:42:34 +0000 Subject: [PATCH] chore: update debugger api --- docs/src/api/class-debugger.md | 27 ++++++----- packages/playwright-client/types/types.d.ts | 48 +++++++++++-------- .../playwright-core/src/client/debugger.ts | 12 ++++- .../playwright-core/src/protocol/validator.ts | 17 ++++--- .../playwright-core/src/server/debugger.ts | 2 +- .../server/dispatchers/debuggerDispatcher.ts | 24 +++++++++- .../src/tools/backend/devtools.ts | 10 ++-- .../src/utils/isomorphic/protocolMetainfo.ts | 4 +- packages/playwright-core/types/types.d.ts | 48 +++++++++++-------- .../playwright/src/mcp/test/browserBackend.ts | 2 +- packages/protocol/src/channels.d.ts | 32 +++++++------ packages/protocol/src/protocol.yml | 23 +++++---- tests/library/debugger.spec.ts | 14 ++++-- tests/playwright-test/playwright.spec.ts | 2 +- 14 files changed, 166 insertions(+), 99 deletions(-) diff --git a/docs/src/api/class-debugger.md b/docs/src/api/class-debugger.md index 4f3068f7cce68..044d1acfa9fd8 100644 --- a/docs/src/api/class-debugger.md +++ b/docs/src/api/class-debugger.md @@ -5,8 +5,6 @@ API for controlling the Playwright debugger. The debugger allows pausing script execution and inspecting the page. Obtain the debugger instance via [`property: BrowserContext.debugger`]. -See also [`method: Page.pause`] for a simple way to pause script execution. - ## event: Debugger.pausedStateChanged * since: v1.59 @@ -23,28 +21,35 @@ Emitted when the debugger pauses or resumes. Returns details about the currently paused calls. Returns an empty array if the debugger is not paused. +## async method: Debugger.pause +* since: v1.59 + +Configures the debugger to pause before the next action is executed. + +Throws if the debugger is already paused. Use [`method: Debugger.next`] or [`method: Debugger.runTo`] to step while paused. + +Note that [`method: Page.pause`] is equivalent to a "debugger" statement — it pauses execution at the call site immediately. On the contrary, [`method: Debugger.pause`] is equivalent to "pause on next statement" — it configures the debugger to pause before the next action is executed. + ## async method: Debugger.resume * since: v1.59 -Resumes script execution if the debugger is paused. +Resumes script execution. Throws if the debugger is not paused. -## async method: Debugger.setPauseAt +## async method: Debugger.next * since: v1.59 -Configures the debugger to pause at the next action or at a specific source location. -Call without arguments to reset the pausing behavior. +Resumes script execution and pauses again before the next action. Throws if the debugger is not paused. -### option: Debugger.setPauseAt.next +## async method: Debugger.runTo * since: v1.59 -- `next` <[boolean]> -When `true`, the debugger will pause before the next action. +Resumes script execution and pauses when an action originates from the given source location. Throws if the debugger is not paused. -### option: Debugger.setPauseAt.location +### param: Debugger.runTo.location * since: v1.59 - `location` <[Object]> - `file` <[string]> - `line` ?<[int]> - `column` ?<[int]> -When specified, the debugger will pause when the action originates from the given source location. +The source location to pause at. diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index a8796454b58f2..c176d05118dc8 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -19418,9 +19418,6 @@ export interface Coverage { * API for controlling the Playwright debugger. The debugger allows pausing script execution and inspecting the page. * Obtain the debugger instance via * [browserContext.debugger](https://playwright.dev/docs/api/class-browsercontext#browser-context-debugger). - * - * See also [page.pause()](https://playwright.dev/docs/api/class-page#page-pause) for a simple way to pause script - * execution. */ export interface Debugger { /** @@ -19453,6 +19450,25 @@ export interface Debugger { */ prependListener(event: 'pausedstatechanged', listener: () => any): this; + /** + * Resumes script execution and pauses again before the next action. Throws if the debugger is not paused. + */ + next(): Promise; + + /** + * Configures the debugger to pause before the next action is executed. + * + * Throws if the debugger is already paused. Use + * [debugger.next()](https://playwright.dev/docs/api/class-debugger#debugger-next) or + * [debugger.runTo(location)](https://playwright.dev/docs/api/class-debugger#debugger-run-to) to step while paused. + * + * Note that [page.pause()](https://playwright.dev/docs/api/class-page#page-pause) is equivalent to a "debugger" + * statement — it pauses execution at the call site immediately. On the contrary, + * [debugger.pause()](https://playwright.dev/docs/api/class-debugger#debugger-pause) is equivalent to "pause on next + * statement" — it configures the debugger to pause before the next action is executed. + */ + pause(): Promise; + /** * Returns details about the currently paused calls. Returns an empty array if the debugger is not paused. */ @@ -19469,31 +19485,21 @@ export interface Debugger { }>; /** - * Resumes script execution if the debugger is paused. + * Resumes script execution. Throws if the debugger is not paused. */ resume(): Promise; /** - * Configures the debugger to pause at the next action or at a specific source location. Call without arguments to - * reset the pausing behavior. - * @param options + * Resumes script execution and pauses when an action originates from the given source location. Throws if the + * debugger is not paused. + * @param location The source location to pause at. */ - setPauseAt(options?: { - /** - * When specified, the debugger will pause when the action originates from the given source location. - */ - location?: { - file: string; - - line?: number; + runTo(location: { + file: string; - column?: number; - }; + line?: number; - /** - * When `true`, the debugger will pause before the next action. - */ - next?: boolean; + column?: number; }): Promise; } diff --git a/packages/playwright-core/src/client/debugger.ts b/packages/playwright-core/src/client/debugger.ts index 013e3c41735b8..bc2a018018e64 100644 --- a/packages/playwright-core/src/client/debugger.ts +++ b/packages/playwright-core/src/client/debugger.ts @@ -37,14 +37,22 @@ export class Debugger extends ChannelOwner implements }); } - async setPauseAt(options: { next?: boolean, location?: { file: string, line?: number, column?: number } } = {}) { - await this._channel.setPauseAt(options); + async pause(): Promise { + await this._channel.pause(); } async resume(): Promise { await this._channel.resume(); } + async next(): Promise { + await this._channel.next(); + } + + async runTo(location: { file: string, line?: number, column?: number }): Promise { + await this._channel.runTo({ location }); + } + pausedDetails(): PausedDetail[] { return this._pausedDetails; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index a22619d8dc5a6..0fcda0fed6b24 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2508,17 +2508,20 @@ scheme.DebuggerPausedStateChangedEvent = tObject({ title: tString, })), }); -scheme.DebuggerSetPauseAtParams = tObject({ - next: tOptional(tBoolean), - location: tOptional(tObject({ +scheme.DebuggerPauseParams = tOptional(tObject({})); +scheme.DebuggerPauseResult = tOptional(tObject({})); +scheme.DebuggerResumeParams = tOptional(tObject({})); +scheme.DebuggerResumeResult = tOptional(tObject({})); +scheme.DebuggerNextParams = tOptional(tObject({})); +scheme.DebuggerNextResult = tOptional(tObject({})); +scheme.DebuggerRunToParams = tObject({ + location: tObject({ file: tString, line: tOptional(tInt), column: tOptional(tInt), - })), + }), }); -scheme.DebuggerSetPauseAtResult = tOptional(tObject({})); -scheme.DebuggerResumeParams = tOptional(tObject({})); -scheme.DebuggerResumeResult = tOptional(tObject({})); +scheme.DebuggerRunToResult = tOptional(tObject({})); scheme.DialogInitializer = tObject({ page: tOptional(tChannel(['Page'])), type: tString, diff --git a/packages/playwright-core/src/server/debugger.ts b/packages/playwright-core/src/server/debugger.ts index a28d45e351bad..541f877717354 100644 --- a/packages/playwright-core/src/server/debugger.ts +++ b/packages/playwright-core/src/server/debugger.ts @@ -54,7 +54,7 @@ export class Debugger extends SdkObject implements InstrumentationListener { async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { if (this._muted) return; - const pauseOnPauseCall = this._enabled && metadata.method === 'pause'; + const pauseOnPauseCall = this._enabled && metadata.type === 'BrowserContext' && metadata.method === 'pause'; const pauseOnNextStep = !!this._pauseAt.next && shouldPauseBeforeStep(metadata, this._pauseBeforeInputActions); const pauseOnLocation = !!this._pauseAt.location && matchesLocation(metadata, this._pauseAt.location); if (pauseOnPauseCall || pauseOnNextStep || pauseOnLocation) diff --git a/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts index 2cfc43fe8fd21..43f69eb9154c4 100644 --- a/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/debuggerDispatcher.ts @@ -50,12 +50,32 @@ export class DebuggerDispatcher extends Dispatcher { + async pause(params: channels.DebuggerPauseParams, progress: Progress): Promise { + if (this._object.isPaused()) + throw new Error('Debugger is already paused'); this._object.setPauseBeforeInputActions(); - this._object.setPauseAt(params); + this._object.setPauseAt({ next: true }); } async resume(params: channels.DebuggerResumeParams, progress: Progress): Promise { + if (!this._object.isPaused()) + throw new Error('Debugger is not paused'); + this._object.resume(); + } + + async next(params: channels.DebuggerNextParams, progress: Progress): Promise { + if (!this._object.isPaused()) + throw new Error('Debugger is not paused'); + this._object.setPauseBeforeInputActions(); + this._object.setPauseAt({ next: true }); + this._object.resume(); + } + + async runTo(params: channels.DebuggerRunToParams, progress: Progress): Promise { + if (!this._object.isPaused()) + throw new Error('Debugger is not paused'); + this._object.setPauseBeforeInputActions(); + this._object.setPauseAt({ location: params.location }); this._object.resume(); } } diff --git a/packages/playwright-core/src/tools/backend/devtools.ts b/packages/playwright-core/src/tools/backend/devtools.ts index 7b61b0d021f1f..6803c27c17032 100644 --- a/packages/playwright-core/src/tools/backend/devtools.ts +++ b/packages/playwright-core/src/tools/backend/devtools.ts @@ -43,9 +43,9 @@ const resume = defineTool({ browserContext.debugger.on('pausedstatechanged', listener); }); - let location; if (params.location) { const [file, lineStr] = params.location.split(':'); + let location; if (lineStr) { const line = Number(lineStr); if (isNaN(line)) @@ -54,10 +54,12 @@ const resume = defineTool({ } else { location = { file: params.location }; } + await browserContext.debugger.runTo(location); + } else if (params.step) { + await browserContext.debugger.next(); + } else { + await browserContext.debugger.resume(); } - - await browserContext.debugger.setPauseAt({ next: !!params.step, location }); - await browserContext.debugger.resume(); await pausedPromise; }, }); diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index a8d7ee1f9d56f..951b109102ba8 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -272,8 +272,10 @@ export const methodMetainfo = new Map any): this; + /** + * Resumes script execution and pauses again before the next action. Throws if the debugger is not paused. + */ + next(): Promise; + + /** + * Configures the debugger to pause before the next action is executed. + * + * Throws if the debugger is already paused. Use + * [debugger.next()](https://playwright.dev/docs/api/class-debugger#debugger-next) or + * [debugger.runTo(location)](https://playwright.dev/docs/api/class-debugger#debugger-run-to) to step while paused. + * + * Note that [page.pause()](https://playwright.dev/docs/api/class-page#page-pause) is equivalent to a "debugger" + * statement — it pauses execution at the call site immediately. On the contrary, + * [debugger.pause()](https://playwright.dev/docs/api/class-debugger#debugger-pause) is equivalent to "pause on next + * statement" — it configures the debugger to pause before the next action is executed. + */ + pause(): Promise; + /** * Returns details about the currently paused calls. Returns an empty array if the debugger is not paused. */ @@ -19469,31 +19485,21 @@ export interface Debugger { }>; /** - * Resumes script execution if the debugger is paused. + * Resumes script execution. Throws if the debugger is not paused. */ resume(): Promise; /** - * Configures the debugger to pause at the next action or at a specific source location. Call without arguments to - * reset the pausing behavior. - * @param options + * Resumes script execution and pauses when an action originates from the given source location. Throws if the + * debugger is not paused. + * @param location The source location to pause at. */ - setPauseAt(options?: { - /** - * When specified, the debugger will pause when the action originates from the given source location. - */ - location?: { - file: string; - - line?: number; + runTo(location: { + file: string; - column?: number; - }; + line?: number; - /** - * When `true`, the debugger will pause before the next action. - */ - next?: boolean; + column?: number; }): Promise; } diff --git a/packages/playwright/src/mcp/test/browserBackend.ts b/packages/playwright/src/mcp/test/browserBackend.ts index 36b5cbeba4461..0b3ac3718e31e 100644 --- a/packages/playwright/src/mcp/test/browserBackend.ts +++ b/packages/playwright/src/mcp/test/browserBackend.ts @@ -125,6 +125,6 @@ export async function runDaemonForContext(testInfo: TestInfoImpl, context: playw `- Run "playwright-cli attach ${sessionName}" to attach to this test`, ].join('\n')); - await context.debugger.setPauseAt({ next: true }); + await context.debugger.pause(); return true; } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index f9d1d4613fd42..a65fe15f2e9ce 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -4320,8 +4320,10 @@ export interface DebuggerEventTarget { } export interface DebuggerChannel extends DebuggerEventTarget, EventTargetChannel { _type_Debugger: boolean; - setPauseAt(params: DebuggerSetPauseAtParams, progress?: Progress): Promise; + pause(params?: DebuggerPauseParams, progress?: Progress): Promise; resume(params?: DebuggerResumeParams, progress?: Progress): Promise; + next(params?: DebuggerNextParams, progress?: Progress): Promise; + runTo(params: DebuggerRunToParams, progress?: Progress): Promise; } export type DebuggerPausedStateChangedEvent = { pausedDetails: { @@ -4333,26 +4335,26 @@ export type DebuggerPausedStateChangedEvent = { title: string, }[], }; -export type DebuggerSetPauseAtParams = { - next?: boolean, - location?: { +export type DebuggerPauseParams = {}; +export type DebuggerPauseOptions = {}; +export type DebuggerPauseResult = void; +export type DebuggerResumeParams = {}; +export type DebuggerResumeOptions = {}; +export type DebuggerResumeResult = void; +export type DebuggerNextParams = {}; +export type DebuggerNextOptions = {}; +export type DebuggerNextResult = void; +export type DebuggerRunToParams = { + location: { file: string, line?: number, column?: number, }, }; -export type DebuggerSetPauseAtOptions = { - next?: boolean, - location?: { - file: string, - line?: number, - column?: number, - }, +export type DebuggerRunToOptions = { + }; -export type DebuggerSetPauseAtResult = void; -export type DebuggerResumeParams = {}; -export type DebuggerResumeOptions = {}; -export type DebuggerResumeResult = void; +export type DebuggerRunToResult = void; export interface DebuggerEvents { 'pausedStateChanged': DebuggerPausedStateChangedEvent; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 5cb737443885b..71920a484b638 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3803,22 +3803,29 @@ Debugger: commands: - setPauseAt: - title: Configure pause behavior + pause: + title: Pause on next call + group: configuration + + resume: + title: Resume + group: configuration + + next: + title: Step to next call + group: configuration + + runTo: + title: Run to location group: configuration parameters: - next: boolean? location: - type: object? + type: object properties: file: string line: int? column: int? - resume: - title: Resume - group: configuration - events: pausedStateChanged: diff --git a/tests/library/debugger.spec.ts b/tests/library/debugger.spec.ts index 65d460cc50a83..bf04c2365a2c8 100644 --- a/tests/library/debugger.spec.ts +++ b/tests/library/debugger.spec.ts @@ -22,7 +22,7 @@ it('should pause at next and resume', async ({ context, server }) => { const dbg = context.debugger; expect(dbg.pausedDetails()).toEqual([]); - await dbg.setPauseAt({ next: true }); + await dbg.pause(); const clickPromise = page.click('div'); await new Promise(resolve => dbg.once('pausedstatechanged', resolve)); @@ -51,7 +51,7 @@ it('should pause at pause call', async ({ context, server }) => { const dbg = context.debugger; expect(dbg.pausedDetails()).toEqual([]); - await dbg.setPauseAt(); + await dbg.pause(); const pausePromise = page.pause(); await new Promise(resolve => dbg.once('pausedstatechanged', resolve)); @@ -65,15 +65,21 @@ it('should pause at pause call', async ({ context, server }) => { await pausePromise; }); -it('should pause at location', async ({ context, server }) => { +it('should run to location', async ({ context, server }) => { const page = await context.newPage(); await page.setContent('
click me
'); const dbg = context.debugger; expect(dbg.pausedDetails()).toEqual([]); + // First, pause on next action. + await dbg.pause(); + page.click('div').catch(() => {}); + await new Promise(resolve => dbg.once('pausedstatechanged', resolve)); + + // Now run to a specific location. const line = +(() => { return new Error('').stack.match(/debugger.spec.ts:(\d+)/)[1]; })(); // Note: careful with the line offset below. - await dbg.setPauseAt({ location: { file: 'debugger.spec', line: line + 4 } }); + await dbg.runTo({ file: 'debugger.spec', line: line + 4 }); await page.content(); // should not pause here const clickPromise = page.click('div'); // should pause here await new Promise(resolve => dbg.once('pausedstatechanged', resolve)); diff --git a/tests/playwright-test/playwright.spec.ts b/tests/playwright-test/playwright.spec.ts index 1d2fd3b665c3f..668b5a15675a4 100644 --- a/tests/playwright-test/playwright.spec.ts +++ b/tests/playwright-test/playwright.spec.ts @@ -970,7 +970,7 @@ test('should pause test timeout while on pause', async ({ runInlineTest }) => { import { test, expect } from '@playwright/test'; test('test', async ({ page, context }) => { - await context.debugger.setPauseAt({ next: true }); + await context.debugger.pause(); const paused = new Promise(f => context.debugger.once('pausedstatechanged', f)); const contentPromise = page.setContent('
hello
'); await paused;