From 4112695fabfcf73e9b14cfaf65a9268e201ce722 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 25 Sep 2025 18:16:50 -0700 Subject: [PATCH] feat: allow console / network on pause (#37593) --- .../agents/playwright-test-generator.md | 3 +- .../.claude/agents/playwright-test-healer.md | 4 +- .../.claude/agents/playwright-test-planner.md | 2 +- .../chatmodes/🎭 generator.chatmode.md | 1 + .../.github/chatmodes/🎭 healer.chatmode.md | 2 +- .../playwright-core/src/client/network.ts | 3 + .../playwright-core/src/protocol/validator.ts | 2 + .../server/dispatchers/networkDispatchers.ts | 6 +- .../playwright-core/src/server/network.ts | 5 ++ packages/playwright/src/agents/healer.md | 2 + packages/playwright/src/mcp/browser/tab.ts | 23 ++++---- .../src/mcp/browser/tools/console.ts | 3 +- .../src/mcp/browser/tools/network.ts | 16 +++-- packages/protocol/src/channels.d.ts | 4 ++ packages/protocol/src/protocol.yml | 3 + tests/mcp/test-debug.spec.ts | 58 +++++++++++++++++++ 16 files changed, 111 insertions(+), 26 deletions(-) diff --git a/examples/todomvc/.claude/agents/playwright-test-generator.md b/examples/todomvc/.claude/agents/playwright-test-generator.md index 7e94347e73..277ff679a4 100644 --- a/examples/todomvc/.claude/agents/playwright-test-generator.md +++ b/examples/todomvc/.claude/agents/playwright-test-generator.md @@ -1,6 +1,6 @@ --- name: playwright-test-generator -description: Use this agent when you need to create automated browser tests using Playwright. Examples: Context: User wants to test a login flow on their web application. user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then verifies the dashboard page loads' assistant: 'I'll use the playwright-test-generator agent to create and validate this login test for you' The user needs a specific browser automation test created, which is exactly what the playwright-test-generator agent is designed for. Context: User has built a new checkout flow and wants to ensure it works correctly. user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the order?' assistant: 'I'll use the playwright-test-generator agent to build a comprehensive checkout flow test' This is a complex user journey that needs to be automated and tested, perfect for the playwright-test-generator agent. +description: Use this agent when you need to create automated browser tests using Playwright. Examples: Context: User wants to test a login flow on their web application. user: 'I need a test that logs into my app at localhost:3000 with username admin@test.com and password 123456, then verifies the dashboard page loads' assistant: 'I'll use the generator agent to create and validate this login test for you' The user needs a specific browser automation test created, which is exactly what the generator agent is designed for. Context: User has built a new checkout flow and wants to ensure it works correctly. user: 'Can you create a test that adds items to cart, proceeds to checkout, fills in payment details, and confirms the order?' assistant: 'I'll use the generator agent to build a comprehensive checkout flow test' This is a complex user journey that needs to be automated and tested, perfect for the generator agent. tools: Glob, Grep, Read, Write, mcp__playwright-test__browser_click, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_verify_element_visible, mcp__playwright-test__browser_verify_list_visible, mcp__playwright-test__browser_verify_text_visible, mcp__playwright-test__browser_verify_value, mcp__playwright-test__browser_wait_for, mcp__playwright-test__test_setup_page model: sonnet color: blue @@ -30,6 +30,7 @@ Your process is methodical and thorough: @playwright/test source code that follows following convention: - One file per scenario, one test in a file + - Use seed test content (copyright, structure) to emit consistent tests. - File name must be fs-friendly scenario name - Test must be placed in a describe matching the top-level test plan item - Test title must match the scenario name diff --git a/examples/todomvc/.claude/agents/playwright-test-healer.md b/examples/todomvc/.claude/agents/playwright-test-healer.md index 952b083e8e..61efa5a1f5 100644 --- a/examples/todomvc/.claude/agents/playwright-test-healer.md +++ b/examples/todomvc/.claude/agents/playwright-test-healer.md @@ -1,7 +1,7 @@ --- name: playwright-test-healer -description: Use this agent when you need to debug and fix failing Playwright tests. Examples: Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' assistant: 'I'll use the playwright-test-healer agent to debug and fix the failing login test.' The user has identified a specific failing test that needs debugging and fixing, which is exactly what the playwright-test-healer agent is designed for. Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' assistant: 'Let me use the playwright-test-healer agent to investigate and fix the user-registration test.' A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. -tools: Glob, Grep, Read, Write, Edit, MultiEdit, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run +description: Use this agent when you need to debug and fix failing Playwright tests. Examples: Context: A developer has a failing Playwright test that needs to be debugged and fixed. user: 'The login test is failing, can you fix it?' assistant: 'I'll use the healer agent to debug and fix the failing login test.' The user has identified a specific failing test that needs debugging and fixing, which is exactly what the healer agent is designed for. Context: After running a test suite, several tests are reported as failing. user: 'Test user-registration.spec.ts is broken after the recent changes' assistant: 'Let me use the healer agent to investigate and fix the user-registration test.' A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. +tools: Glob, Grep, Read, Write, Edit, MultiEdit, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run model: sonnet color: red --- diff --git a/examples/todomvc/.claude/agents/playwright-test-planner.md b/examples/todomvc/.claude/agents/playwright-test-planner.md index 834421306f..e2b17b94c3 100644 --- a/examples/todomvc/.claude/agents/playwright-test-planner.md +++ b/examples/todomvc/.claude/agents/playwright-test-planner.md @@ -1,6 +1,6 @@ --- name: playwright-test-planner -description: Use this agent when you need to create comprehensive test plan for a web application or website. Examples: Context: User wants to test a new e-commerce checkout flow. user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout' assistant: 'I'll use the playwright-test-planner agent to navigate to your checkout page and create comprehensive test scenarios.' The user needs test planning for a specific web page, so use the playwright-test-planner agent to explore and create test scenarios. Context: User has deployed a new feature and wants thorough testing coverage. user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?' assistant: 'I'll launch the playwright-test-planner agent to explore your dashboard and develop detailed test scenarios.' This requires web exploration and test scenario creation, perfect for the playwright-test-planner agent. +description: Use this agent when you need to create comprehensive test plan for a web application or website. Examples: Context: User wants to test a new e-commerce checkout flow. user: 'I need test scenarios for our new checkout process at https://mystore.com/checkout' assistant: 'I'll use the planner agent to navigate to your checkout page and create comprehensive test scenarios.' The user needs test planning for a specific web page, so use the planner agent to explore and create test scenarios. Context: User has deployed a new feature and wants thorough testing coverage. user: 'Can you help me test our new user dashboard at https://app.example.com/dashboard?' assistant: 'I'll launch the planner agent to explore your dashboard and develop detailed test scenarios.' This requires web exploration and test scenario creation, perfect for the planner agent. tools: Glob, Grep, Read, Write, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__test_setup_page model: sonnet color: green diff --git a/examples/todomvc/.github/chatmodes/🎭 generator.chatmode.md b/examples/todomvc/.github/chatmodes/🎭 generator.chatmode.md index 79d660e831..7ec0fb8a00 100644 --- a/examples/todomvc/.github/chatmodes/🎭 generator.chatmode.md +++ b/examples/todomvc/.github/chatmodes/🎭 generator.chatmode.md @@ -27,6 +27,7 @@ Your process is methodical and thorough: @playwright/test source code that follows following convention: - One file per scenario, one test in a file + - Use seed test content (copyright, structure) to emit consistent tests. - File name must be fs-friendly scenario name - Test must be placed in a describe matching the top-level test plan item - Test title must match the scenario name diff --git a/examples/todomvc/.github/chatmodes/🎭 healer.chatmode.md b/examples/todomvc/.github/chatmodes/🎭 healer.chatmode.md index b2de43deb9..ea1dd53015 100644 --- a/examples/todomvc/.github/chatmodes/🎭 healer.chatmode.md +++ b/examples/todomvc/.github/chatmodes/🎭 healer.chatmode.md @@ -1,6 +1,6 @@ --- description: Use this agent when you need to debug and fix failing Playwright tests. -tools: ['createFile', 'createDirectory', 'editFiles', 'fileSearch', 'textSearch', 'listDirectory', 'readFile', 'test_browser_evaluate', 'test_browser_generate_locator', 'test_browser_snapshot', 'test_debug', 'test_list', 'test_run'] +tools: ['createFile', 'createDirectory', 'editFiles', 'fileSearch', 'textSearch', 'listDirectory', 'readFile', 'test_browser_console_messages', 'test_browser_evaluate', 'test_browser_generate_locator', 'test_browser_network_requests', 'test_browser_snapshot', 'test_debug', 'test_list', 'test_run'] --- You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 88c34c6785..727090961a 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -91,6 +91,7 @@ export class Request extends ChannelOwner implements ap private _actualHeadersPromise: Promise | undefined; _timing: ResourceTiming; private _fallbackOverrides: SerializedFallbackOverrides = {}; + _hasResponse = false; static from(request: channels.RequestChannel): Request { return (request as any)._object; @@ -117,6 +118,8 @@ export class Request extends ChannelOwner implements ap responseStart: -1, responseEnd: -1, }; + this._hasResponse = this._initializer.hasResponse; + this._channel.on('response', () => this._hasResponse = true); } url(): string { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 339a2d1dc7..595694d8f7 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2266,7 +2266,9 @@ scheme.RequestInitializer = tObject({ headers: tArray(tType('NameValue')), isNavigationRequest: tBoolean, redirectedFrom: tOptional(tChannel(['Request'])), + hasResponse: tBoolean, }); +scheme.RequestResponseEvent = tOptional(tObject({})); scheme.RequestResponseParams = tOptional(tObject({})); scheme.RequestResponseResult = tObject({ response: tOptional(tChannel(['Response'])), diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index 671f2996ed..ba34add8a1 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -19,16 +19,16 @@ import { Dispatcher } from './dispatcher'; import { FrameDispatcher } from './frameDispatcher'; import { WorkerDispatcher } from './pageDispatcher'; import { TracingDispatcher } from './tracingDispatcher'; +import { Request } from '../network'; import type { APIRequestContext } from '../fetch'; -import type { Request, Response, Route } from '../network'; +import type { Response, Route } from '../network'; import type { BrowserContextDispatcher } from './browserContextDispatcher'; import type { RootDispatcher } from './dispatcher'; import type { PageDispatcher } from './pageDispatcher'; import type * as channels from '@protocol/channels'; import type { Progress } from '@protocol/progress'; - export class RequestDispatcher extends Dispatcher implements channels.RequestChannel { _type_Request: boolean; private _browserContextDispatcher: BrowserContextDispatcher; @@ -60,9 +60,11 @@ export class RequestDispatcher extends Dispatcher this._dispatchEvent('response', {})); } async rawRequestHeaders(params: channels.RequestRawRequestHeadersParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index 21253fdac9..f9ad37aa6c 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -117,6 +117,10 @@ export class Request extends SdkObject { private _bodySize: number | undefined; _responseBodyOverride: { body: string; isBase64: boolean; } | undefined; + static Events = { + Response: 'response', + }; + constructor(context: contexts.BrowserContext, frame: frames.Frame | null, serviceWorker: pages.Worker | null, redirectedFrom: Request | null, documentId: string | undefined, url: string, resourceType: string, method: string, postData: Buffer | null, headers: HeadersArray) { super(frame || context, 'request'); @@ -202,6 +206,7 @@ export class Request extends SdkObject { _setResponse(response: Response) { this._response = response; this._waitForResponsePromise.resolve(response); + this.emit(Request.Events.Response, response); } _finalRequest(): Request { diff --git a/packages/playwright/src/agents/healer.md b/packages/playwright/src/agents/healer.md index 0f7e728958..2bd30a4072 100644 --- a/packages/playwright/src/agents/healer.md +++ b/packages/playwright/src/agents/healer.md @@ -9,8 +9,10 @@ tools: - read - write - edit + - playwright-test/browser_console_messages - playwright-test/browser_evaluate - playwright-test/browser_generate_locator + - playwright-test/browser_network_requests - playwright-test/browser_snapshot - playwright-test/test_debug - playwright-test/test_list diff --git a/packages/playwright/src/mcp/browser/tab.ts b/packages/playwright/src/mcp/browser/tab.ts index 7c58d1b9a7..77687432c4 100644 --- a/packages/playwright/src/mcp/browser/tab.ts +++ b/packages/playwright/src/mcp/browser/tab.ts @@ -53,10 +53,11 @@ export class Tab extends EventEmitter { private _lastTitle = 'about:blank'; private _consoleMessages: ConsoleMessage[] = []; private _recentConsoleMessages: ConsoleMessage[] = []; - private _requests: Map = new Map(); + private _requests: Set = new Set(); private _onPageClose: (tab: Tab) => void; private _modalStates: ModalState[] = []; private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = []; + private _initializedPromise: Promise; constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { super(); @@ -65,8 +66,7 @@ export class Tab extends EventEmitter { this._onPageClose = onPageClose; page.on('console', event => this._handleConsoleMessage(messageToConsoleMessage(event))); page.on('pageerror', error => this._handleConsoleMessage(pageErrorToConsoleMessage(error))); - page.on('request', request => this._requests.set(request, null)); - page.on('response', response => this._requests.set(response.request(), response)); + page.on('request', request => this._requests.add(request)); page.on('close', () => this._onClose()); page.on('filechooser', chooser => { this.setModalState({ @@ -83,7 +83,7 @@ export class Tab extends EventEmitter { page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation); page.setDefaultTimeout(this.context.config.timeouts.action); (page as any)[tabSymbol] = this; - void this._initialize(); + this._initializedPromise = this._initialize(); } static forPage(page: playwright.Page): Tab | undefined { @@ -98,13 +98,8 @@ export class Tab extends EventEmitter { for (const error of errors) this._handleConsoleMessage(pageErrorToConsoleMessage(error)); const requests = await this.page.requests().catch(() => []); - for (const request of requests) { - this._requests.set(request, null); - void request.response().catch(() => null).then(response => { - if (response) - this._requests.set(request, response); - }); - } + for (const request of requests) + this._requests.add(request); } modalStates(): ModalState[] { @@ -207,11 +202,13 @@ export class Tab extends EventEmitter { await this.waitForLoadState('load', { timeout: 5000 }); } - consoleMessages(): ConsoleMessage[] { + async consoleMessages(): Promise { + await this._initializedPromise; return this._consoleMessages; } - requests(): Map { + async requests(): Promise> { + await this._initializedPromise; return this._requests; } diff --git a/packages/playwright/src/mcp/browser/tools/console.ts b/packages/playwright/src/mcp/browser/tools/console.ts index 3ad0ba4db4..381cd41927 100644 --- a/packages/playwright/src/mcp/browser/tools/console.ts +++ b/packages/playwright/src/mcp/browser/tools/console.ts @@ -27,7 +27,8 @@ const console = defineTabTool({ type: 'readOnly', }, handle: async (tab, params, response) => { - tab.consoleMessages().map(message => response.addResult(message.toString())); + const messages = await tab.consoleMessages(); + messages.map(message => response.addResult(message.toString())); }, }); diff --git a/packages/playwright/src/mcp/browser/tools/network.ts b/packages/playwright/src/mcp/browser/tools/network.ts index 2ea081671c..ed72253787 100644 --- a/packages/playwright/src/mcp/browser/tools/network.ts +++ b/packages/playwright/src/mcp/browser/tools/network.ts @@ -18,6 +18,7 @@ import { z } from '../../sdk/bundle'; import { defineTabTool } from './tool'; import type * as playwright from 'playwright-core'; +import type { Request } from '../../../../../playwright-core/src/client/network'; const requests = defineTabTool({ capability: 'core', @@ -31,16 +32,21 @@ const requests = defineTabTool({ }, handle: async (tab, params, response) => { - const requests = tab.requests(); - [...requests.entries()].forEach(([req, res]) => response.addResult(renderRequest(req, res))); + const requests = await tab.requests(); + for (const request of requests) + response.addResult(await renderRequest(request)); }, }); -function renderRequest(request: playwright.Request, response: playwright.Response | null) { +async function renderRequest(request: playwright.Request) { const result: string[] = []; result.push(`[${request.method().toUpperCase()}] ${request.url()}`); - if (response) - result.push(`=> [${response.status()}] ${response.statusText()}`); + const hasResponse = (request as Request)._hasResponse; + if (hasResponse) { + const response = await request.response(); + if (response) + result.push(`=> [${response.status()}] ${response.statusText()}`); + } return result.join(' '); } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 0bc51cbf7f..a025e267d9 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3863,14 +3863,17 @@ export type RequestInitializer = { headers: NameValue[], isNavigationRequest: boolean, redirectedFrom?: RequestChannel, + hasResponse: boolean, }; export interface RequestEventTarget { + on(event: 'response', callback: (params: RequestResponseEvent) => void): this; } export interface RequestChannel extends RequestEventTarget, Channel { _type_Request: boolean; response(params?: RequestResponseParams, progress?: Progress): Promise; rawRequestHeaders(params?: RequestRawRequestHeadersParams, progress?: Progress): Promise; } +export type RequestResponseEvent = {}; export type RequestResponseParams = {}; export type RequestResponseOptions = {}; export type RequestResponseResult = { @@ -3883,6 +3886,7 @@ export type RequestRawRequestHeadersResult = { }; export interface RequestEvents { + 'response': RequestResponseEvent; } // ----------- Route ----------- diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 7e2173979b..1eee2e08f1 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3447,6 +3447,7 @@ Request: items: NameValue isNavigationRequest: boolean redirectedFrom: Request? + hasResponse: boolean commands: @@ -3462,6 +3463,8 @@ Request: type: array items: NameValue + events: + response: Route: type: interface diff --git a/tests/mcp/test-debug.spec.ts b/tests/mcp/test-debug.spec.ts index 772f6b325b..88ae92786f 100644 --- a/tests/mcp/test-debug.spec.ts +++ b/tests/mcp/test-debug.spec.ts @@ -290,3 +290,61 @@ Running 1 test using 1 worker ### Paused on error: Error: expect(locator).toBeVisible() failed`)); }); + +test('test_debug w/ console_messages', async ({ startClient }) => { + const { client, id } = await prepareDebugTest(startClient, ` + import { test, expect } from '@playwright/test'; + test('fail', async ({ page }) => { + await page.evaluate(() => { + console.log('console.log'); + console.error('console.error'); + }); + await expect(page.getByRole('button', { name: 'Missing' })).toBeVisible({ timeout: 1000 }); + }); + `); + + expect(await client.callTool({ + name: 'test_debug', + arguments: { + test: { id, title: 'fail' }, + }, + })).toHaveTextResponse(expect.stringContaining(` +Running 1 test using 1 worker +### Paused on error: +Error: expect(locator).toBeVisible() failed`)); + + expect(await client.callTool({ + name: 'browser_console_messages', + })).toHaveResponse({ + result: expect.stringMatching(/\[LOG] console.log.*\n\[ERROR\] console.error/), + }); +}); + +test('test_debug w/ network_requests', async ({ startClient, server }) => { + const { client, id } = await prepareDebugTest(startClient, ` + import { test, expect } from '@playwright/test'; + test('fail', async ({ page }) => { + await page.goto(${JSON.stringify(server.HELLO_WORLD)}); + await page.evaluate(async () => { + await fetch('missing'); + }); + await expect(page.getByRole('button', { name: 'Missing' })).toBeVisible({ timeout: 1000 }); + }); + `); + + expect(await client.callTool({ + name: 'test_debug', + arguments: { + test: { id, title: 'fail' }, + }, + })).toHaveTextResponse(expect.stringContaining(` +Running 1 test using 1 worker +### Paused on error: +Error: expect(locator).toBeVisible() failed`)); + + expect(await client.callTool({ + name: 'browser_network_requests', + })).toHaveResponse({ + result: `\[GET\] ${server.HELLO_WORLD} => [200] OK\n\[GET\] ${server.PREFIX}/missing => [404] Not Found`, + }); +});