feat: allow console / network on pause (#37593)
This commit is contained in:
parent
888da0f89d
commit
4112695fab
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: playwright-test-generator
|
||||
description: Use this agent when you need to create automated browser tests using Playwright. Examples: <example>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' <commentary> The user needs a specific browser automation test created, which is exactly what the playwright-test-generator agent is designed for. </commentary></example><example>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' <commentary> This is a complex user journey that needs to be automated and tested, perfect for the playwright-test-generator agent. </commentary></example>
|
||||
description: Use this agent when you need to create automated browser tests using Playwright. Examples: <example>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' <commentary> The user needs a specific browser automation test created, which is exactly what the generator agent is designed for. </commentary></example><example>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' <commentary> This is a complex user journey that needs to be automated and tested, perfect for the generator agent. </commentary></example>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: playwright-test-healer
|
||||
description: Use this agent when you need to debug and fix failing Playwright tests. Examples: <example>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.' <commentary> 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. </commentary></example><example>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.' <commentary> A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. </commentary></example>
|
||||
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: <example>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.' <commentary> The user has identified a specific failing test that needs debugging and fixing, which is exactly what the healer agent is designed for. </commentary></example><example>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.' <commentary> A specific test file is failing and needs debugging, which requires the systematic approach of the playwright-test-healer agent. </commentary></example>
|
||||
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
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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: <example>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.' <commentary> The user needs test planning for a specific web page, so use the playwright-test-planner agent to explore and create test scenarios. </commentary></example><example>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.' <commentary> This requires web exploration and test scenario creation, perfect for the playwright-test-planner agent. </commentary></example>
|
||||
description: Use this agent when you need to create comprehensive test plan for a web application or website. Examples: <example>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.' <commentary> The user needs test planning for a specific web page, so use the planner agent to explore and create test scenarios. </commentary></example><example>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.' <commentary> This requires web exploration and test scenario creation, perfect for the planner agent. </commentary></example>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
private _actualHeadersPromise: Promise<RawHeaders> | 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<channels.RequestChannel> implements ap
|
|||
responseStart: -1,
|
||||
responseEnd: -1,
|
||||
};
|
||||
this._hasResponse = this._initializer.hasResponse;
|
||||
this._channel.on('response', () => this._hasResponse = true);
|
||||
}
|
||||
|
||||
url(): string {
|
||||
|
|
|
|||
|
|
@ -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'])),
|
||||
|
|
|
|||
|
|
@ -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<Request, channels.RequestChannel, BrowserContextDispatcher | PageDispatcher | FrameDispatcher> implements channels.RequestChannel {
|
||||
_type_Request: boolean;
|
||||
private _browserContextDispatcher: BrowserContextDispatcher;
|
||||
|
|
@ -60,9 +60,11 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
|
|||
headers: request.headers(),
|
||||
isNavigationRequest: request.isNavigationRequest(),
|
||||
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
|
||||
hasResponse: !!request._existingResponse(),
|
||||
});
|
||||
this._type_Request = true;
|
||||
this._browserContextDispatcher = scope;
|
||||
this.addObjectListener(Request.Events.Response, () => this._dispatchEvent('response', {}));
|
||||
}
|
||||
|
||||
async rawRequestHeaders(params: channels.RequestRawRequestHeadersParams, progress: Progress): Promise<channels.RequestRawRequestHeadersResult> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -53,10 +53,11 @@ export class Tab extends EventEmitter<TabEventsInterface> {
|
|||
private _lastTitle = 'about:blank';
|
||||
private _consoleMessages: ConsoleMessage[] = [];
|
||||
private _recentConsoleMessages: ConsoleMessage[] = [];
|
||||
private _requests: Map<playwright.Request, playwright.Response | null> = new Map();
|
||||
private _requests: Set<playwright.Request> = new Set();
|
||||
private _onPageClose: (tab: Tab) => void;
|
||||
private _modalStates: ModalState[] = [];
|
||||
private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = [];
|
||||
private _initializedPromise: Promise<void>;
|
||||
|
||||
constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) {
|
||||
super();
|
||||
|
|
@ -65,8 +66,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
|
|||
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<TabEventsInterface> {
|
|||
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<TabEventsInterface> {
|
|||
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<TabEventsInterface> {
|
|||
await this.waitForLoadState('load', { timeout: 5000 });
|
||||
}
|
||||
|
||||
consoleMessages(): ConsoleMessage[] {
|
||||
async consoleMessages(): Promise<ConsoleMessage[]> {
|
||||
await this._initializedPromise;
|
||||
return this._consoleMessages;
|
||||
}
|
||||
|
||||
requests(): Map<playwright.Request, playwright.Response | null> {
|
||||
async requests(): Promise<Set<playwright.Request>> {
|
||||
await this._initializedPromise;
|
||||
return this._requests;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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(' ');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<RequestResponseResult>;
|
||||
rawRequestHeaders(params?: RequestRawRequestHeadersParams, progress?: Progress): Promise<RequestRawRequestHeadersResult>;
|
||||
}
|
||||
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 -----------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue