chore: parse response in tests (#796)

This commit is contained in:
Pavel Feldman
2025-07-30 12:47:22 -07:00
committed by GitHub
parent 65d99fe595
commit 4df162aff5
33 changed files with 442 additions and 365 deletions

1
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"playwright": "1.55.0-alpha-1752701791000", "playwright": "1.55.0-alpha-1752701791000",
"playwright-core": "1.55.0-alpha-1752701791000", "playwright-core": "1.55.0-alpha-1752701791000",
"ws": "^8.18.1", "ws": "^8.18.1",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.4" "zod-to-json-schema": "^3.24.4"
}, },
"bin": { "bin": {

View File

@@ -45,6 +45,7 @@
"playwright": "1.55.0-alpha-1752701791000", "playwright": "1.55.0-alpha-1752701791000",
"playwright-core": "1.55.0-alpha-1752701791000", "playwright-core": "1.55.0-alpha-1752701791000",
"ws": "^8.18.1", "ws": "^8.18.1",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.4" "zod-to-json-schema": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -93,8 +93,8 @@ export class Context {
if (!this._tabs.length) { if (!this._tabs.length) {
return [ return [
'### No open tabs', '### Open tabs',
'Use the "browser_navigate" tool to navigate to a page first.', 'No open tabs. Use the "browser_navigate" tool to navigate to a page first.',
'', '',
]; ];
} }

View File

@@ -88,12 +88,12 @@ export function createServer(backend: ServerBackend, runHeartbeat: boolean): Ser
} }
const errorResult = (...messages: string[]) => ({ const errorResult = (...messages: string[]) => ({
content: [{ type: 'text', text: messages.join('\n') }], content: [{ type: 'text', text: '### Result\n' + messages.join('\n') }],
isError: true, isError: true,
}); });
const tool = tools.find(tool => tool.name === request.params.name) as ToolSchema<any>; const tool = tools.find(tool => tool.name === request.params.name) as ToolSchema<any>;
if (!tool) if (!tool)
return errorResult(`Tool "${request.params.name}" not found`); return errorResult(`Error: Tool "${request.params.name}" not found`);
try { try {
return await backend.callTool(tool, tool.inputSchema.parse(request.params.arguments || {})); return await backend.callTool(tool, tool.inputSchema.parse(request.params.arguments || {}));

View File

@@ -28,6 +28,7 @@ export class Response {
readonly toolName: string; readonly toolName: string;
readonly toolArgs: Record<string, any>; readonly toolArgs: Record<string, any>;
private _isError: boolean | undefined;
constructor(context: Context, toolName: string, toolArgs: Record<string, any>) { constructor(context: Context, toolName: string, toolArgs: Record<string, any>) {
this._context = context; this._context = context;
@@ -39,6 +40,11 @@ export class Response {
this._result.push(result); this._result.push(result);
} }
addError(error: string) {
this._result.push(`Error: ${error}`);
this._isError = true;
}
result() { result() {
return this._result.join('\n'); return this._result.join('\n');
} }
@@ -77,7 +83,7 @@ export class Response {
return this._snapshot; return this._snapshot;
} }
async serialize(): Promise<{ content: (TextContent | ImageContent)[] }> { async serialize(): Promise<{ content: (TextContent | ImageContent)[], isError?: boolean }> {
const response: string[] = []; const response: string[] = [];
// Start with command result. // Start with command result.
@@ -116,6 +122,6 @@ ${this._code.join('\n')}
content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType }); content.push({ type: 'image', data: image.data.toString('base64'), mimeType: image.contentType });
} }
return { content }; return { content, isError: this._isError };
} }
} }

View File

@@ -49,7 +49,6 @@ const resize = defineTabTool({
}, },
handle: async (tab, params, response) => { handle: async (tab, params, response) => {
response.addCode(`// Resize browser window to ${params.width}x${params.height}`);
response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`); response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
await tab.waitForCompletion(async () => { await tab.waitForCompletion(async () => {

View File

@@ -67,11 +67,9 @@ const type = defineTabTool({
await tab.waitForCompletion(async () => { await tab.waitForCompletion(async () => {
if (params.slowly) { if (params.slowly) {
response.setIncludeSnapshot(); response.setIncludeSnapshot();
response.addCode(`// Press "${params.text}" sequentially into "${params.element}"`);
response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`); response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
await locator.pressSequentially(params.text); await locator.pressSequentially(params.text);
} else { } else {
response.addCode(`// Fill "${params.text}" into "${params.element}"`);
response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`); response.addCode(`await page.${await generateLocator(locator)}.fill(${javascript.quote(params.text)});`);
await locator.fill(params.text); await locator.fill(params.text);
} }

View File

@@ -35,7 +35,6 @@ const navigate = defineTool({
await tab.navigate(params.url); await tab.navigate(params.url);
response.setIncludeSnapshot(); response.setIncludeSnapshot();
response.addCode(`// Navigate to ${params.url}`);
response.addCode(`await page.goto('${params.url}');`); response.addCode(`await page.goto('${params.url}');`);
}, },
}); });
@@ -53,7 +52,6 @@ const goBack = defineTabTool({
handle: async (tab, params, response) => { handle: async (tab, params, response) => {
await tab.page.goBack(); await tab.page.goBack();
response.setIncludeSnapshot(); response.setIncludeSnapshot();
response.addCode(`// Navigate back`);
response.addCode(`await page.goBack();`); response.addCode(`await page.goBack();`);
}, },
}); });
@@ -70,7 +68,6 @@ const goForward = defineTabTool({
handle: async (tab, params, response) => { handle: async (tab, params, response) => {
await tab.page.goForward(); await tab.page.goForward();
response.setIncludeSnapshot(); response.setIncludeSnapshot();
response.addCode(`// Navigate forward`);
response.addCode(`await page.goForward();`); response.addCode(`await page.goForward();`);
}, },
}); });

View File

@@ -37,7 +37,6 @@ const pdf = defineTabTool({
handle: async (tab, params, response) => { handle: async (tab, params, response) => {
const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`); const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
response.addCode(`// Save page as ${fileName}`);
response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`); response.addCode(`await page.pdf(${javascript.formatObject({ path: fileName })});`);
response.addResult(`Saved page as ${fileName}`); response.addResult(`Saved page as ${fileName}`);
await tab.page.pdf({ path: fileName }); await tab.page.pdf({ path: fileName });

View File

@@ -63,13 +63,11 @@ const click = defineTabTool({
const button = params.button; const button = params.button;
const buttonAttr = button ? `{ button: '${button}' }` : ''; const buttonAttr = button ? `{ button: '${button}' }` : '';
if (params.doubleClick) { if (params.doubleClick)
response.addCode(`// Double click ${params.element}`);
response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`); response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`);
} else { else
response.addCode(`// Click ${params.element}`);
response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`); response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`);
}
await tab.waitForCompletion(async () => { await tab.waitForCompletion(async () => {
if (params.doubleClick) if (params.doubleClick)
@@ -151,7 +149,6 @@ const selectOption = defineTabTool({
response.setIncludeSnapshot(); response.setIncludeSnapshot();
const locator = await tab.refLocator(params); const locator = await tab.refLocator(params);
response.addCode(`// Select options [${params.values.join(', ')}] in ${params.element}`);
response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`); response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`);
await tab.waitForCompletion(async () => { await tab.waitForCompletion(async () => {

View File

@@ -60,10 +60,11 @@ export function defineTabTool<Input extends z.Schema>(tool: TabTool<Input>): Too
const tab = context.currentTabOrDie(); const tab = context.currentTabOrDie();
const modalStates = tab.modalStates().map(state => state.type); const modalStates = tab.modalStates().map(state => state.type);
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState)) if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
throw new Error(`The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n')); response.addError(`The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
if (!tool.clearsModalState && modalStates.length) else if (!tool.clearsModalState && modalStates.length)
throw new Error(`Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n')); response.addError(`Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
return tool.handle(tab, params, response); else
return tool.handle(tab, params, response);
}, },
}; };
} }

View File

@@ -25,7 +25,9 @@ test('cdp server', async ({ cdpServer, startClient, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`); })).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
}); });
test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => { test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
@@ -41,18 +43,21 @@ test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
element: 'Hello, world!', element: 'Hello, world!',
ref: 'f0', ref: 'f0',
}, },
})).toHaveTextContent(`Error: No open pages available. Use the \"browser_navigate\" tool to navigate to a page first.`); })).toHaveResponse({
result: `Error: No open pages available. Use the "browser_navigate" tool to navigate to a page first.`,
isError: true,
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_snapshot', name: 'browser_snapshot',
})).toHaveTextContent(`### Page state })).toHaveResponse({
- Page URL: ${server.HELLO_WORLD} pageState: expect.stringContaining(`- Page URL: ${server.HELLO_WORLD}
- Page Title: Title - Page Title: Title
- Page Snapshot: - Page Snapshot:
\`\`\`yaml \`\`\`yaml
- generic [active] [ref=e1]: Hello, world! - generic [active] [ref=e1]: Hello, world!
\`\`\` \`\`\``),
`); });
}); });
test('should throw connection error and allow re-connecting', async ({ cdpServer, startClient, server }) => { test('should throw connection error and allow re-connecting', async ({ cdpServer, startClient, server }) => {
@@ -66,12 +71,17 @@ test('should throw connection error and allow re-connecting', async ({ cdpServer
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent(`Error: browserType.connectOverCDP: connect ECONNREFUSED`); })).toHaveResponse({
result: expect.stringContaining(`Error: browserType.connectOverCDP: connect ECONNREFUSED`),
isError: true,
});
await cdpServer.start(); await cdpServer.start();
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`); })).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
}); });
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename. // NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.

View File

@@ -33,21 +33,10 @@ test('browser_click', async ({ client, server, mcpBrowser }) => {
element: 'Submit button', element: 'Submit button',
ref: 'e2', ref: 'e2',
}, },
})).toHaveTextContent(` })).toHaveResponse({
### Ran Playwright code code: `await page.getByRole('button', { name: 'Submit' }).click();`,
\`\`\`js pageState: expect.stringContaining(`- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]`),
// Click Submit button });
await page.getByRole('button', { name: 'Submit' }).click();
\`\`\`
### Page state
- Page URL: ${server.PREFIX}
- Page Title: Title
- Page Snapshot:
\`\`\`yaml
- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]
\`\`\`
`);
}); });
test('browser_click (double)', async ({ client, server }) => { test('browser_click (double)', async ({ client, server }) => {
@@ -73,21 +62,10 @@ test('browser_click (double)', async ({ client, server }) => {
ref: 'e2', ref: 'e2',
doubleClick: true, doubleClick: true,
}, },
})).toHaveTextContent(` })).toHaveResponse({
### Ran Playwright code code: `await page.getByRole('heading', { name: 'Click me' }).dblclick();`,
\`\`\`js pageState: expect.stringContaining(`- heading "Double clicked" [level=1] [ref=e3]`),
// Double click Click me });
await page.getByRole('heading', { name: 'Click me' }).dblclick();
\`\`\`
### Page state
- Page URL: ${server.PREFIX}
- Page Title: Title
- Page Snapshot:
\`\`\`yaml
- heading "Double clicked" [level=1] [ref=e3]
\`\`\`
`);
}); });
test('browser_click (right)', async ({ client, server }) => { test('browser_click (right)', async ({ client, server }) => {
@@ -114,6 +92,8 @@ test('browser_click (right)', async ({ client, server }) => {
button: 'right', button: 'right',
}, },
}); });
expect(result).toContainTextContent(`await page.getByRole('button', { name: 'Menu' }).click({ button: 'right' });`); expect(result).toHaveResponse({
expect(result).toContainTextContent(`- button "Right clicked"`); code: `await page.getByRole('button', { name: 'Menu' }).click({ button: 'right' });`,
pageState: expect.stringContaining(`- button "Right clicked"`),
});
}); });

View File

@@ -37,7 +37,9 @@ test('config user data dir', async ({ startClient, server, mcpMode }, testInfo)
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent(`Hello, world!`); })).toHaveResponse({
pageState: expect.stringContaining(`Hello, world!`),
});
const files = await fs.promises.readdir(config.browser!.userDataDir!); const files = await fs.promises.readdir(config.browser!.userDataDir!);
expect(files.length).toBeGreaterThan(0); expect(files.length).toBeGreaterThan(0);
@@ -58,7 +60,9 @@ test.describe(() => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: 'data:text/html,<script>document.title = navigator.userAgent</script>' }, arguments: { url: 'data:text/html,<script>document.title = navigator.userAgent</script>' },
})).toContainTextContent(`Firefox`); })).toHaveResponse({
pageState: expect.stringContaining(`Firefox`),
});
}); });
}); });

View File

@@ -37,11 +37,10 @@ test('browser_console_messages', async ({ client, server }) => {
const resource = await client.callTool({ const resource = await client.callTool({
name: 'browser_console_messages', name: 'browser_console_messages',
}); });
expect(resource).toHaveTextContent([ expect(resource).toHaveResponse({
'### Result', result: `[LOG] Hello, world! @ ${server.PREFIX}:4
`[LOG] Hello, world! @ ${server.PREFIX}:4`, [ERROR] Error @ ${server.PREFIX}:5`,
`[ERROR] Error @ ${server.PREFIX}:5`, });
].join('\n'));
}); });
test('browser_console_messages (page error)', async ({ client, server }) => { test('browser_console_messages (page error)', async ({ client, server }) => {
@@ -64,8 +63,12 @@ test('browser_console_messages (page error)', async ({ client, server }) => {
const resource = await client.callTool({ const resource = await client.callTool({
name: 'browser_console_messages', name: 'browser_console_messages',
}); });
expect(resource).toHaveTextContent(/Error: Error in script/); expect(resource).toHaveResponse({
expect(resource).toHaveTextContent(new RegExp(server.PREFIX)); result: expect.stringContaining(`Error: Error in script`),
});
expect(resource).toHaveResponse({
result: expect.stringContaining(server.PREFIX),
});
}); });
test('recent console messages', async ({ client, server }) => { test('recent console messages', async ({ client, server }) => {
@@ -91,7 +94,7 @@ test('recent console messages', async ({ client, server }) => {
}, },
}); });
expect(response).toContainTextContent(` expect(response).toHaveResponse({
### New console messages consoleMessages: expect.stringContaining(`- [LOG] Hello, world! @`),
- [LOG] Hello, world! @`); });
}); });

View File

@@ -20,22 +20,15 @@ test('browser_navigate', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toHaveTextContent(` })).toHaveResponse({
### Ran Playwright code code: `await page.goto('${server.HELLO_WORLD}');`,
\`\`\`js pageState: `- Page URL: ${server.HELLO_WORLD}
// Navigate to ${server.HELLO_WORLD}
await page.goto('${server.HELLO_WORLD}');
\`\`\`
### Page state
- Page URL: ${server.HELLO_WORLD}
- Page Title: Title - Page Title: Title
- Page Snapshot: - Page Snapshot:
\`\`\`yaml \`\`\`yaml
- generic [active] [ref=e1]: Hello, world! - generic [active] [ref=e1]: Hello, world!
\`\`\` \`\`\``,
` });
);
}); });
test('browser_select_option', async ({ client, server }) => { test('browser_select_option', async ({ client, server }) => {
@@ -59,23 +52,17 @@ test('browser_select_option', async ({ client, server }) => {
ref: 'e2', ref: 'e2',
values: ['bar'], values: ['bar'],
}, },
})).toHaveTextContent(` })).toHaveResponse({
### Ran Playwright code code: `await page.getByRole('combobox').selectOption(['bar']);`,
\`\`\`js pageState: `- Page URL: ${server.PREFIX}
// Select options [bar] in Select
await page.getByRole('combobox').selectOption(['bar']);
\`\`\`
### Page state
- Page URL: ${server.PREFIX}
- Page Title: Title - Page Title: Title
- Page Snapshot: - Page Snapshot:
\`\`\`yaml \`\`\`yaml
- combobox [ref=e2]: - combobox [ref=e2]:
- option "Foo" - option "Foo"
- option "Bar" [selected] - option "Bar" [selected]
\`\`\` \`\`\``,
`); });
}); });
test('browser_select_option (multiple)', async ({ client, server }) => { test('browser_select_option (multiple)', async ({ client, server }) => {
@@ -100,24 +87,14 @@ test('browser_select_option (multiple)', async ({ client, server }) => {
ref: 'e2', ref: 'e2',
values: ['bar', 'baz'], values: ['bar', 'baz'],
}, },
})).toHaveTextContent(` })).toHaveResponse({
### Ran Playwright code code: `await page.getByRole('listbox').selectOption(['bar', 'baz']);`,
\`\`\`js pageState: expect.stringContaining(`
// Select options [bar, baz] in Select
await page.getByRole('listbox').selectOption(['bar', 'baz']);
\`\`\`
### Page state
- Page URL: ${server.PREFIX}
- Page Title: Title
- Page Snapshot:
\`\`\`yaml
- listbox [ref=e2]: - listbox [ref=e2]:
- option "Foo" [ref=e3] - option "Foo" [ref=e3]
- option "Bar" [selected] [ref=e4] - option "Bar" [selected] [ref=e4]
- option "Baz" [selected] [ref=e5] - option "Baz" [selected] [ref=e5]`),
\`\`\` });
`);
}); });
test('browser_resize', async ({ client, server }) => { test('browser_resize', async ({ client, server }) => {
@@ -141,12 +118,12 @@ test('browser_resize', async ({ client, server }) => {
height: 780, height: 780,
}, },
}); });
expect(response).toContainTextContent(`### Ran Playwright code expect(response).toHaveResponse({
\`\`\`js code: `await page.setViewportSize({ width: 390, height: 780 });`,
// Resize browser window to 390x780 });
await page.setViewportSize({ width: 390, height: 780 }); await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toHaveResponse({
\`\`\``); pageState: expect.stringContaining(`Window size: 390x780`),
await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent('Window size: 390x780'); });
}); });
test('old locator error message', async ({ client, server }) => { test('old locator error message', async ({ client, server }) => {
@@ -165,10 +142,11 @@ test('old locator error message', async ({ client, server }) => {
arguments: { arguments: {
url: server.PREFIX, url: server.PREFIX,
}, },
})).toContainTextContent(` })).toHaveResponse({
pageState: expect.stringContaining(`
- button "Button 1" [ref=e2] - button "Button 1" [ref=e2]
- button "Button 2" [ref=e3] - button "Button 2" [ref=e3]`),
`.trim()); });
await client.callTool({ await client.callTool({
name: 'browser_click', name: 'browser_click',
@@ -184,7 +162,10 @@ test('old locator error message', async ({ client, server }) => {
element: 'Button 2', element: 'Button 2',
ref: 'e3', ref: 'e3',
}, },
})).toContainTextContent('Ref e3 not found in the current page snapshot. Try capturing new snapshot.'); })).toHaveResponse({
result: expect.stringContaining(`Ref e3 not found in the current page snapshot. Try capturing new snapshot.`),
isError: true,
});
}); });
test('visibility: hidden > visible should be shown', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/535' } }, async ({ client, server }) => { test('visibility: hidden > visible should be shown', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/535' } }, async ({ client, server }) => {
@@ -203,5 +184,7 @@ test('visibility: hidden > visible should be shown', { annotation: { type: 'issu
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_snapshot' name: 'browser_snapshot'
})).toContainTextContent('- button "Button"'); })).toHaveResponse({
pageState: expect.stringContaining(`- button "Button"`),
});
}); });

View File

@@ -39,5 +39,7 @@ test('--device should work', async ({ startClient, server, mcpMode }) => {
arguments: { arguments: {
url: server.PREFIX, url: server.PREFIX,
}, },
})).toContainTextContent(`393x659`); })).toHaveResponse({
pageState: expect.stringContaining(`393x659`),
});
}); });

View File

@@ -21,7 +21,9 @@ test('alert dialog', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]'); })).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',
@@ -29,25 +31,31 @@ test('alert dialog', async ({ client, server }) => {
element: 'Button', element: 'Button',
ref: 'e2', ref: 'e2',
}, },
})).toHaveTextContent(`### Ran Playwright code })).toHaveResponse({
\`\`\`js code: `await page.getByRole('button', { name: 'Button' }).click();`,
// Click Button modalState: `- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`,
await page.getByRole('button', { name: 'Button' }).click(); });
\`\`\`
### Modal state expect(await client.callTool({
- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool name: 'browser_click',
`); arguments: {
element: 'Button',
ref: 'e2',
},
})).toHaveResponse({
code: undefined,
modalState: `- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`,
});
const result = await client.callTool({ expect(await client.callTool({
name: 'browser_handle_dialog', name: 'browser_handle_dialog',
arguments: { arguments: {
accept: true, accept: true,
}, },
})).toHaveResponse({
modalState: undefined,
pageState: expect.stringContaining(`- button "Button"`),
}); });
expect(result).not.toContainTextContent('### Modal state');
expect(result).toContainTextContent(`Page Snapshot:`);
}); });
test('two alert dialogs', async ({ client, server }) => { test('two alert dialogs', async ({ client, server }) => {
@@ -61,7 +69,9 @@ test('two alert dialogs', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]'); })).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',
@@ -69,15 +79,10 @@ test('two alert dialogs', async ({ client, server }) => {
element: 'Button', element: 'Button',
ref: 'e2', ref: 'e2',
}, },
})).toHaveTextContent(`### Ran Playwright code })).toHaveResponse({
\`\`\`js code: `await page.getByRole('button', { name: 'Button' }).click();`,
// Click Button modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 1"]: can be handled by the "browser_handle_dialog" tool`),
await page.getByRole('button', { name: 'Button' }).click(); });
\`\`\`
### Modal state
- ["alert" dialog with message "Alert 1"]: can be handled by the "browser_handle_dialog" tool
`);
const result = await client.callTool({ const result = await client.callTool({
name: 'browser_handle_dialog', name: 'browser_handle_dialog',
@@ -86,9 +91,9 @@ await page.getByRole('button', { name: 'Button' }).click();
}, },
}); });
expect(result).toContainTextContent(`### Modal state expect(result).toHaveResponse({
- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`),
`); });
const result2 = await client.callTool({ const result2 = await client.callTool({
name: 'browser_handle_dialog', name: 'browser_handle_dialog',
@@ -97,7 +102,9 @@ await page.getByRole('button', { name: 'Button' }).click();
}, },
}); });
expect(result2).not.toContainTextContent('### Modal state'); expect(result2).not.toHaveResponse({
modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`),
});
}); });
test('confirm dialog (true)', async ({ client, server }) => { test('confirm dialog (true)', async ({ client, server }) => {
@@ -111,7 +118,9 @@ test('confirm dialog (true)', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]'); })).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',
@@ -119,21 +128,19 @@ test('confirm dialog (true)', async ({ client, server }) => {
element: 'Button', element: 'Button',
ref: 'e2', ref: 'e2',
}, },
})).toContainTextContent(`### Modal state })).toHaveResponse({
- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`); modalState: expect.stringContaining(`- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`),
});
const result = await client.callTool({ expect(await client.callTool({
name: 'browser_handle_dialog', name: 'browser_handle_dialog',
arguments: { arguments: {
accept: true, accept: true,
}, },
})).toHaveResponse({
modalState: undefined,
pageState: expect.stringContaining(`- generic [active] [ref=e1]: "true"`),
}); });
expect(result).not.toContainTextContent('### Modal state');
expect(result).toContainTextContent(`- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: "true"
\`\`\``);
}); });
test('confirm dialog (false)', async ({ client, server }) => { test('confirm dialog (false)', async ({ client, server }) => {
@@ -147,7 +154,9 @@ test('confirm dialog (false)', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]'); })).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',
@@ -155,21 +164,19 @@ test('confirm dialog (false)', async ({ client, server }) => {
element: 'Button', element: 'Button',
ref: 'e2', ref: 'e2',
}, },
})).toContainTextContent(`### Modal state })).toHaveResponse({
- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool modalState: expect.stringContaining(`- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`),
`); });
const result = await client.callTool({ expect(await client.callTool({
name: 'browser_handle_dialog', name: 'browser_handle_dialog',
arguments: { arguments: {
accept: false, accept: false,
}, },
})).toHaveResponse({
modalState: undefined,
pageState: expect.stringContaining(`- generic [active] [ref=e1]: "false"`),
}); });
expect(result).toContainTextContent(`- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: "false"
\`\`\``);
}); });
test('prompt dialog', async ({ client, server }) => { test('prompt dialog', async ({ client, server }) => {
@@ -183,7 +190,9 @@ test('prompt dialog', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]'); })).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',
@@ -191,9 +200,9 @@ test('prompt dialog', async ({ client, server }) => {
element: 'Button', element: 'Button',
ref: 'e2', ref: 'e2',
}, },
})).toContainTextContent(`### Modal state })).toHaveResponse({
- ["prompt" dialog with message "Prompt"]: can be handled by the "browser_handle_dialog" tool modalState: expect.stringContaining(`- ["prompt" dialog with message "Prompt"]: can be handled by the "browser_handle_dialog" tool`),
`); });
const result = await client.callTool({ const result = await client.callTool({
name: 'browser_handle_dialog', name: 'browser_handle_dialog',
@@ -203,10 +212,9 @@ test('prompt dialog', async ({ client, server }) => {
}, },
}); });
expect(result).toContainTextContent(`- Page Snapshot: expect(result).toHaveResponse({
\`\`\`yaml pageState: expect.stringContaining(`- generic [active] [ref=e1]: Answer`),
- generic [active] [ref=e1]: Answer });
\`\`\``);
}); });
test('alert dialog w/ race', async ({ client, server }) => { test('alert dialog w/ race', async ({ client, server }) => {
@@ -214,7 +222,9 @@ test('alert dialog w/ race', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]'); })).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',
@@ -222,15 +232,10 @@ test('alert dialog w/ race', async ({ client, server }) => {
element: 'Button', element: 'Button',
ref: 'e2', ref: 'e2',
}, },
})).toHaveTextContent(`### Ran Playwright code })).toHaveResponse({
\`\`\`js code: `await page.getByRole('button', { name: 'Button' }).click();`,
// Click Button modalState: expect.stringContaining(`- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`),
await page.getByRole('button', { name: 'Button' }).click(); });
\`\`\`
### Modal state
- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool
`);
const result = await client.callTool({ const result = await client.callTool({
name: 'browser_handle_dialog', name: 'browser_handle_dialog',
@@ -239,11 +244,12 @@ await page.getByRole('button', { name: 'Button' }).click();
}, },
}); });
expect(result).not.toContainTextContent('### Modal state'); expect(result).toHaveResponse({
expect(result).toContainTextContent(`### Page state modalState: undefined,
- Page URL: ${server.PREFIX} pageState: expect.stringContaining(`- Page URL: ${server.PREFIX}
- Page Title: - Page Title:
- Page Snapshot: - Page Snapshot:
\`\`\`yaml \`\`\`yaml
- button "Button"`); - button "Button"`),
});
}); });

View File

@@ -20,15 +20,19 @@ test('browser_evaluate', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- Page Title: Title`); })).toHaveResponse({
pageState: expect.stringContaining(`- Page Title: Title`),
});
const result = await client.callTool({ expect(await client.callTool({
name: 'browser_evaluate', name: 'browser_evaluate',
arguments: { arguments: {
function: '() => document.title', function: '() => document.title',
}, },
})).toHaveResponse({
result: `"Title"`,
code: `await page.evaluate('() => document.title');`,
}); });
expect(result).toContainTextContent(`"Title"`);
}); });
test('browser_evaluate (element)', async ({ client, server }) => { test('browser_evaluate (element)', async ({ client, server }) => {
@@ -47,15 +51,19 @@ test('browser_evaluate (element)', async ({ client, server }) => {
element: 'body', element: 'body',
ref: 'e1', ref: 'e1',
}, },
})).toContainTextContent(`### Result })).toHaveResponse({
"red"`); result: `"red"`,
code: `await page.getByText('Hello, world!').evaluate('element => element.style.backgroundColor');`,
});
}); });
test('browser_evaluate (error)', async ({ client, server }) => { test('browser_evaluate (error)', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- Page Title: Title`); })).toHaveResponse({
pageState: expect.stringContaining(`- Page Title: Title`),
});
const result = await client.callTool({ const result = await client.callTool({
name: 'browser_evaluate', name: 'browser_evaluate',

View File

@@ -26,22 +26,21 @@ test('browser_file_upload', async ({ client, server }, testInfo) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent(` })).toHaveResponse({
\`\`\`yaml pageState: expect.stringContaining(`- generic [active] [ref=e1]:
- generic [active] [ref=e1]:
- button "Choose File" [ref=e2] - button "Choose File" [ref=e2]
- button "Button" [ref=e3] - button "Button" [ref=e3]`),
\`\`\``); });
{ {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_file_upload', name: 'browser_file_upload',
arguments: { paths: [] }, arguments: { paths: [] },
})).toHaveTextContent(` })).toHaveResponse({
Error: The tool "browser_file_upload" can only be used when there is related modal state present. isError: true,
### Modal state result: expect.stringContaining(`The tool "browser_file_upload" can only be used when there is related modal state present.`),
- There is no modal state present modalState: expect.stringContaining(`- There is no modal state present`),
`.trim()); });
} }
expect(await client.callTool({ expect(await client.callTool({
@@ -50,8 +49,9 @@ Error: The tool "browser_file_upload" can only be used when there is related mod
element: 'Textbox', element: 'Textbox',
ref: 'e2', ref: 'e2',
}, },
})).toContainTextContent(`### Modal state })).toHaveResponse({
- [File chooser]: can be handled by the "browser_file_upload" tool`); modalState: expect.stringContaining(`- [File chooser]: can be handled by the "browser_file_upload" tool`),
});
const filePath = testInfo.outputPath('test.txt'); const filePath = testInfo.outputPath('test.txt');
await fs.writeFile(filePath, 'Hello, world!'); await fs.writeFile(filePath, 'Hello, world!');
@@ -64,7 +64,10 @@ Error: The tool "browser_file_upload" can only be used when there is related mod
}, },
}); });
expect(response).not.toContainTextContent('### Modal state'); expect(response).toHaveResponse({
code: expect.stringContaining(`await fileChooser.setFiles(`),
modalState: undefined,
});
} }
{ {
@@ -76,7 +79,9 @@ Error: The tool "browser_file_upload" can only be used when there is related mod
}, },
}); });
expect(response).toContainTextContent('- [File chooser]: can be handled by the \"browser_file_upload\" tool'); expect(response).toHaveResponse({
modalState: `- [File chooser]: can be handled by the "browser_file_upload" tool`,
});
} }
{ {
@@ -88,9 +93,10 @@ Error: The tool "browser_file_upload" can only be used when there is related mod
}, },
}); });
expect(response).toContainTextContent(`Error: Tool "browser_click" does not handle the modal state. expect(response).toHaveResponse({
### Modal state result: `Error: Tool "browser_click" does not handle the modal state.`,
- [File chooser]: can be handled by the "browser_file_upload" tool`); modalState: expect.stringContaining(`- [File chooser]: can be handled by the "browser_file_upload" tool`),
});
} }
}); });
@@ -105,7 +111,9 @@ test('clicking on download link emits download', async ({ startClient, server, m
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent('- link "Download" [ref=e2]'); })).toHaveResponse({
pageState: expect.stringContaining(`- link "Download" [ref=e2]`),
});
await client.callTool({ await client.callTool({
name: 'browser_click', name: 'browser_click',
arguments: { arguments: {
@@ -113,8 +121,9 @@ test('clicking on download link emits download', async ({ startClient, server, m
ref: 'e2', ref: 'e2',
}, },
}); });
await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent(`### Downloads await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toHaveResponse({
- Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`); downloads: `- Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`,
});
}); });
test('navigating to download link emits download', async ({ startClient, server, mcpBrowser, mcpMode }, testInfo) => { test('navigating to download link emits download', async ({ startClient, server, mcpBrowser, mcpMode }, testInfo) => {
@@ -136,5 +145,7 @@ test('navigating to download link emits download', async ({ startClient, server,
arguments: { arguments: {
url: server.PREFIX + 'download', url: server.PREFIX + 'download',
}, },
})).toContainTextContent('### Downloads'); })).toHaveResponse({
downloads: expect.stringContaining(`- Downloaded file test.txt to`),
});
}); });

View File

@@ -199,41 +199,14 @@ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode']):
type Response = Awaited<ReturnType<Client['callTool']>>; type Response = Awaited<ReturnType<Client['callTool']>>;
export const expect = baseExpect.extend({ export const expect = baseExpect.extend({
toHaveTextContent(response: Response, content: string | RegExp) { toHaveResponse(response: Response, object: any) {
const parsed = parseResponse(response);
const isNot = this.isNot; const isNot = this.isNot;
try { try {
const text = (response.content as any)[0].text;
if (typeof content === 'string') {
if (isNot)
baseExpect(text.trim()).not.toBe(content.trim());
else
baseExpect(text.trim()).toBe(content.trim());
} else {
if (isNot)
baseExpect(text).not.toMatch(content);
else
baseExpect(text).toMatch(content);
}
} catch (e) {
return {
pass: isNot,
message: () => e.message,
};
}
return {
pass: !isNot,
message: () => ``,
};
},
toContainTextContent(response: Response, content: string) {
const isNot = this.isNot;
try {
const texts = (response.content as any).map(c => c.text).join('\n');
if (isNot) if (isNot)
expect(texts).not.toContain(content); expect(parsed).not.toEqual(expect.objectContaining(object));
else else
expect(texts).toContain(content); expect(parsed).toEqual(expect.objectContaining(object));
} catch (e) { } catch (e) {
return { return {
pass: isNot, pass: isNot,
@@ -250,3 +223,46 @@ export const expect = baseExpect.extend({
export function formatOutput(output: string): string[] { export function formatOutput(output: string): string[] {
return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean); return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean);
} }
function parseResponse(response: any) {
const text = (response as any).content[0].text;
const sections = parseSections(text);
const result = sections.get('Result');
const code = sections.get('Ran Playwright code');
const tabs = sections.get('Open tabs');
const pageState = sections.get('Page state');
const consoleMessages = sections.get('New console messages');
const modalState = sections.get('Modal state');
const downloads = sections.get('Downloads');
const codeNoFrame = code?.replace(/^```js\n/, '').replace(/\n```$/, '');
const isError = response.isError;
return {
result,
code: codeNoFrame,
tabs,
pageState,
consoleMessages,
modalState,
downloads,
isError,
};
}
function parseSections(text: string): Map<string, string> {
const sections = new Map<string, string>();
const sectionHeaders = text.split(/^### /m).slice(1); // Remove empty first element
for (const section of sectionHeaders) {
const firstNewlineIndex = section.indexOf('\n');
if (firstNewlineIndex === -1)
continue;
const sectionName = section.substring(0, firstNewlineIndex);
const sectionContent = section.substring(firstNewlineIndex + 1).trim();
sections.set(sectionName, sectionContent);
}
return sections;
}

View File

@@ -21,6 +21,7 @@ for (const mcpHeadless of [false, true]) {
test.use({ mcpHeadless }); test.use({ mcpHeadless });
test.skip(process.platform === 'linux', 'Auto-detection wont let this test run on linux'); test.skip(process.platform === 'linux', 'Auto-detection wont let this test run on linux');
test.skip(({ mcpMode, mcpHeadless }) => mcpMode === 'docker' && !mcpHeadless, 'Headed mode is not supported in docker'); test.skip(({ mcpMode, mcpHeadless }) => mcpMode === 'docker' && !mcpHeadless, 'Headed mode is not supported in docker');
test('browser', async ({ client, server, mcpBrowser }) => { test('browser', async ({ client, server, mcpBrowser }) => {
test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser ?? ''), 'Only chrome is supported for this test'); test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser ?? ''), 'Only chrome is supported for this test');
server.route('/', (req, res) => { server.route('/', (req, res) => {
@@ -40,11 +41,9 @@ for (const mcpHeadless of [false, true]) {
}, },
}); });
expect(response).toContainTextContent(`Mozilla/5.0`); expect(response).toHaveResponse({
if (mcpHeadless) pageState: (mcpHeadless ? expect : expect.not).stringContaining(`HeadlessChrome`),
expect(response).toContainTextContent(`HeadlessChrome`); });
else
expect(response).not.toContainTextContent(`HeadlessChrome`);
}); });
}); });
} }

View File

@@ -22,9 +22,8 @@ test('stitched aria frames', async ({ client }) => {
arguments: { arguments: {
url: `data:text/html,<h1>Hello</h1><iframe src="data:text/html,<button>World</button><main><iframe src='data:text/html,<p>Nested</p>'></iframe></main>"></iframe><iframe src="data:text/html,<h1>Should be invisible</h1>" style="display: none;"></iframe>`, url: `data:text/html,<h1>Hello</h1><iframe src="data:text/html,<button>World</button><main><iframe src='data:text/html,<p>Nested</p>'></iframe></main>"></iframe><iframe src="data:text/html,<h1>Should be invisible</h1>" style="display: none;"></iframe>`,
}, },
})).toContainTextContent(` })).toHaveResponse({
\`\`\`yaml pageState: expect.stringContaining(`- generic [active] [ref=e1]:
- generic [active] [ref=e1]:
- heading "Hello" [level=1] [ref=e2] - heading "Hello" [level=1] [ref=e2]
- iframe [ref=e3]: - iframe [ref=e3]:
- generic [active] [ref=f1e1]: - generic [active] [ref=f1e1]:
@@ -32,7 +31,8 @@ test('stitched aria frames', async ({ client }) => {
- main [ref=f1e3]: - main [ref=f1e3]:
- iframe [ref=f1e4]: - iframe [ref=f1e4]:
- paragraph [ref=f2e2]: Nested - paragraph [ref=f2e2]: Nested
\`\`\``); \`\`\``),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',
@@ -40,5 +40,7 @@ test('stitched aria frames', async ({ client }) => {
element: 'World', element: 'World',
ref: 'f1e2', ref: 'f1e2',
}, },
})).toContainTextContent(`// Click World`); })).toHaveResponse({
code: `await page.locator('iframe').first().contentFrame().getByRole('button', { name: 'World' }).click();`,
});
}); });

View File

@@ -20,5 +20,7 @@ test('browser_install', async ({ client, mcpBrowser }) => {
test.skip(mcpBrowser !== 'chromium', 'Test only chromium'); test.skip(mcpBrowser !== 'chromium', 'Test only chromium');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_install', name: 'browser_install',
})).toContainTextContent(`### No open tabs`); })).toHaveResponse({
tabs: expect.stringContaining(`No open tabs`),
});
}); });

View File

@@ -27,18 +27,17 @@ test('test reopen browser', async ({ startClient, server, mcpMode }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_close', name: 'browser_close',
})).toContainTextContent(`### Ran Playwright code })).toHaveResponse({
\`\`\`js code: `await page.close()`,
await page.close() tabs: `No open tabs. Use the "browser_navigate" tool to navigate to a page first.`,
\`\`\` });
### No open tabs
Use the "browser_navigate" tool to navigate to a page first.`);
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`); })).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
await client.close(); await client.close();
@@ -68,7 +67,10 @@ test('executable path', async ({ startClient, server }) => {
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
}); });
expect(response).toContainTextContent(`executable doesn't exist`); expect(response).toHaveResponse({
result: expect.stringContaining(`executable doesn't exist`),
isError: true,
});
}); });
test('persistent context', async ({ startClient, server }) => { test('persistent context', async ({ startClient, server }) => {
@@ -82,11 +84,12 @@ test('persistent context', async ({ startClient, server }) => {
`, 'text/html'); `, 'text/html');
const { client } = await startClient(); const { client } = await startClient();
const response = await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: NO`),
}); });
expect(response).toContainTextContent(`Storage: NO`);
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise(resolve => setTimeout(resolve, 3000));
@@ -95,12 +98,12 @@ test('persistent context', async ({ startClient, server }) => {
}); });
const { client: client2 } = await startClient(); const { client: client2 } = await startClient();
const response2 = await client2.callTool({ expect(await client2.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: YES`),
}); });
expect(response2).toContainTextContent(`Storage: YES`);
}); });
test('isolated context', async ({ startClient, server }) => { test('isolated context', async ({ startClient, server }) => {
@@ -114,22 +117,24 @@ test('isolated context', async ({ startClient, server }) => {
`, 'text/html'); `, 'text/html');
const { client: client1 } = await startClient({ args: [`--isolated`] }); const { client: client1 } = await startClient({ args: [`--isolated`] });
const response = await client1.callTool({ expect(await client1.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: NO`),
}); });
expect(response).toContainTextContent(`Storage: NO`);
await client1.callTool({ await client1.callTool({
name: 'browser_close', name: 'browser_close',
}); });
const { client: client2 } = await startClient({ args: [`--isolated`] }); const { client: client2 } = await startClient({ args: [`--isolated`] });
const response2 = await client2.callTool({ expect(await client2.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: NO`),
}); });
expect(response2).toContainTextContent(`Storage: NO`);
}); });
test('isolated context with storage state', async ({ startClient, server }, testInfo) => { test('isolated context with storage state', async ({ startClient, server }, testInfo) => {
@@ -155,9 +160,10 @@ test('isolated context with storage state', async ({ startClient, server }, test
`--isolated`, `--isolated`,
`--storage-state=${storageStatePath}`, `--storage-state=${storageStatePath}`,
] }); ] });
const response = await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: session-value`),
}); });
expect(response).toContainTextContent(`Storage: session-value`);
}); });

View File

@@ -40,7 +40,8 @@ test('browser_network_requests', async ({ client, server }) => {
await expect.poll(() => client.callTool({ await expect.poll(() => client.callTool({
name: 'browser_network_requests', name: 'browser_network_requests',
})).toHaveTextContent(`### Result })).toHaveResponse({
[GET] ${`${server.PREFIX}`} => [200] OK result: expect.stringContaining(`[GET] ${`${server.PREFIX}`} => [200] OK
[GET] ${`${server.PREFIX}json`} => [200] OK`); [GET] ${`${server.PREFIX}json`} => [200] OK`),
});
}); });

View File

@@ -27,7 +27,10 @@ test('save as pdf unavailable', async ({ startClient, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_pdf_save', name: 'browser_pdf_save',
})).toHaveTextContent(/Tool \"browser_pdf_save\" not found/); })).toHaveResponse({
result: 'Error: Tool "browser_pdf_save" not found',
isError: true,
});
}); });
test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => { test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => {
@@ -40,12 +43,16 @@ test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`); })).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
const response = await client.callTool({ });
name: 'browser_pdf_save',
expect(await client.callTool({
name: 'browser_pdf_save',
})).toHaveResponse({
code: expect.stringContaining(`await page.pdf(`),
result: expect.stringMatching(/Saved page as.*page-[^:]+.pdf/),
}); });
expect(response).toHaveTextContent(/Save page as.*page-[^:]+.pdf/);
}); });
test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, server }, testInfo) => { test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, server }, testInfo) => {
@@ -58,14 +65,19 @@ test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, ser
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`); })).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_pdf_save', name: 'browser_pdf_save',
arguments: { arguments: {
filename: 'output.pdf', filename: 'output.pdf',
}, },
})).toContainTextContent(`output.pdf`); })).toHaveResponse({
result: expect.stringContaining(`output.pdf`),
code: expect.stringContaining(`await page.pdf(`),
});
const files = [...fs.readdirSync(outputDir)]; const files = [...fs.readdirSync(outputDir)];

View File

@@ -25,7 +25,9 @@ test('browser_take_screenshot (viewport)', async ({ startClient, server }, testI
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`); })).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@@ -51,7 +53,9 @@ test('browser_take_screenshot (element)', async ({ startClient, server }, testIn
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`[ref=e1]`); })).toHaveResponse({
pageState: expect.stringContaining(`[ref=e1]`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@@ -82,7 +86,9 @@ test('--output-dir should work', async ({ startClient, server }, testInfo) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`); })).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
await client.callTool({ await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@@ -104,7 +110,9 @@ for (const raw of [undefined, true]) {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent(`Navigate to http://localhost`); })).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@@ -144,7 +152,9 @@ test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient,
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`); })).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@@ -184,7 +194,9 @@ test('browser_take_screenshot (imageResponses=omit)', async ({ startClient, serv
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`); })).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
await client.callTool({ await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@@ -209,7 +221,9 @@ test('browser_take_screenshot (fullPage: true)', async ({ startClient, server },
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`); })).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@@ -236,7 +250,9 @@ test('browser_take_screenshot (fullPage with element should error)', async ({ st
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`[ref=e1]`); })).toHaveResponse({
pageState: expect.stringContaining(`[ref=e1]`),
});
const result = await client.callTool({ const result = await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@@ -259,7 +275,9 @@ test('browser_take_screenshot (viewport without snapshot)', async ({ startClient
// Ensure we have a tab but don't navigate anywhere (no snapshot captured) // Ensure we have a tab but don't navigate anywhere (no snapshot captured)
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_tab_list', name: 'browser_tab_list',
})).toContainTextContent('about:blank'); })).toHaveResponse({
tabs: `- 0: (current) [] (about:blank)`,
});
// This should work without requiring a snapshot since it's a viewport screenshot // This should work without requiring a snapshot since it's a viewport screenshot
expect(await client.callTool({ expect(await client.callTool({

View File

@@ -30,99 +30,87 @@ async function createTab(client: Client, title: string, body: string) {
test('list initial tabs', async ({ client }) => { test('list initial tabs', async ({ client }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_tab_list', name: 'browser_tab_list',
})).toHaveTextContent(`### Open tabs })).toHaveResponse({
- 0: (current) [] (about:blank)`); tabs: `- 0: (current) [] (about:blank)`,
});
}); });
test('list first tab', async ({ client }) => { test('list first tab', async ({ client }) => {
await createTab(client, 'Tab one', 'Body one'); await createTab(client, 'Tab one', 'Body one');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_tab_list', name: 'browser_tab_list',
})).toHaveTextContent(`### Open tabs })).toHaveResponse({
- 0: [] (about:blank) tabs: `- 0: [] (about:blank)
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`); - 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`,
});
}); });
test('create new tab', async ({ client }) => { test('create new tab', async ({ client }) => {
const result = await createTab(client, 'Tab one', 'Body one'); expect(await createTab(client, 'Tab one', 'Body one')).toHaveResponse({
expect(result).toContainTextContent(`### Open tabs tabs: `- 0: [] (about:blank)
- 0: [] (about:blank) - 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`,
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>) pageState: expect.stringContaining(`- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
`);
expect(result).toContainTextContent(`
### Page state
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one - Page Title: Tab one
- Page Snapshot: - Page Snapshot:
\`\`\`yaml \`\`\`yaml
- generic [active] [ref=e1]: Body one - generic [active] [ref=e1]: Body one
\`\`\``); \`\`\``),
});
const result2 = await createTab(client, 'Tab two', 'Body two'); expect(await createTab(client, 'Tab two', 'Body two')).toHaveResponse({
expect(result2).toContainTextContent(`### Open tabs tabs: `- 0: [] (about:blank)
- 0: [] (about:blank)
- 1: [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>) - 1: [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
- 2: (current) [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>) - 2: (current) [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)`,
`); pageState: expect.stringContaining(`- Page URL: data:text/html,<title>Tab two</title><body>Body two</body>
expect(result2).toContainTextContent(`
### Page state
- Page URL: data:text/html,<title>Tab two</title><body>Body two</body>
- Page Title: Tab two - Page Title: Tab two
- Page Snapshot: - Page Snapshot:
\`\`\`yaml \`\`\`yaml
- generic [active] [ref=e1]: Body two - generic [active] [ref=e1]: Body two
\`\`\``); \`\`\``),
});
}); });
test('select tab', async ({ client }) => { test('select tab', async ({ client }) => {
await createTab(client, 'Tab one', 'Body one'); await createTab(client, 'Tab one', 'Body one');
await createTab(client, 'Tab two', 'Body two'); await createTab(client, 'Tab two', 'Body two');
const result = await client.callTool({ expect(await client.callTool({
name: 'browser_tab_select', name: 'browser_tab_select',
arguments: { arguments: {
index: 1, index: 1,
}, },
}); })).toHaveResponse({
expect(result).toContainTextContent(`### Open tabs tabs: `- 0: [] (about:blank)
- 0: [] (about:blank)
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>) - 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
- 2: [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)`); - 2: [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)`,
pageState: expect.stringContaining(`- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
expect(result).toContainTextContent(`
### Page state
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one - Page Title: Tab one
- Page Snapshot: - Page Snapshot:
\`\`\`yaml \`\`\`yaml
- generic [active] [ref=e1]: Body one - generic [active] [ref=e1]: Body one
\`\`\``); \`\`\``),
});
}); });
test('close tab', async ({ client }) => { test('close tab', async ({ client }) => {
await createTab(client, 'Tab one', 'Body one'); await createTab(client, 'Tab one', 'Body one');
await createTab(client, 'Tab two', 'Body two'); await createTab(client, 'Tab two', 'Body two');
const result = await client.callTool({ expect(await client.callTool({
name: 'browser_tab_close', name: 'browser_tab_close',
arguments: { arguments: {
index: 2, index: 2,
}, },
}); })).toHaveResponse({
expect(result).toContainTextContent(`### Open tabs tabs: `- 0: [] (about:blank)
- 0: [] (about:blank) - 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`,
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`); pageState: expect.stringContaining(`- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
expect(result).toContainTextContent(`
### Page state
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one - Page Title: Tab one
- Page Snapshot: - Page Snapshot:
\`\`\`yaml \`\`\`yaml
- generic [active] [ref=e1]: Body one - generic [active] [ref=e1]: Body one
\`\`\``); \`\`\``),
});
}); });
test('reuse first tab when navigating', async ({ startClient, cdpServer, server }) => { test('reuse first tab when navigating', async ({ startClient, cdpServer, server }) => {

View File

@@ -29,7 +29,9 @@ test('check that trace is saved', async ({ startClient, server, mcpMode }, testI
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`); })).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(fs.existsSync(path.join(outputDir, 'traces', 'trace.trace'))).toBeTruthy(); expect(fs.existsSync(path.join(outputDir, 'traces', 'trace.trace'))).toBeTruthy();
}); });

View File

@@ -41,13 +41,18 @@ test('browser_type', async ({ client, server }) => {
submit: true, submit: true,
}, },
}); });
expect(response).toContainTextContent(`fill('Hi!');`); expect(response).toHaveResponse({
expect(response).toContainTextContent(`- textbox`); code: `await page.getByRole('textbox').fill('Hi!');
await page.getByRole('textbox').press('Enter');`,
pageState: expect.stringContaining(`- textbox`),
});
} }
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_console_messages', name: 'browser_console_messages',
})).toHaveTextContent(/\[LOG\] Key pressed: Enter , Text: Hi!/); })).toHaveResponse({
result: expect.stringContaining(`[LOG] Key pressed: Enter , Text: Hi!`),
});
}); });
test('browser_type (slowly)', async ({ client, server }) => { test('browser_type (slowly)', async ({ client, server }) => {
@@ -72,15 +77,23 @@ test('browser_type (slowly)', async ({ client, server }) => {
}, },
}); });
expect(response).toContainTextContent(`pressSequentially('Hi!');`); expect(response).toHaveResponse({
expect(response).toContainTextContent(`- textbox`); code: `await page.getByRole('textbox').pressSequentially('Hi!');`,
pageState: expect.stringContaining(`- textbox`),
});
} }
const response = await client.callTool({ const response = await client.callTool({
name: 'browser_console_messages', name: 'browser_console_messages',
}); });
expect(response).toHaveTextContent(/\[LOG\] Key pressed: H Text: /); expect(response).toHaveResponse({
expect(response).toHaveTextContent(/\[LOG\] Key pressed: i Text: H/); result: expect.stringContaining(`[LOG] Key pressed: H Text: `),
expect(response).toHaveTextContent(/\[LOG\] Key pressed: ! Text: Hi/); });
expect(response).toHaveResponse({
result: expect.stringContaining(`[LOG] Key pressed: i Text: H`),
});
expect(response).toHaveResponse({
result: expect.stringContaining(`[LOG] Key pressed: ! Text: Hi`),
});
}); });
test('browser_type (no submit)', async ({ client, server }) => { test('browser_type (no submit)', async ({ client, server }) => {
@@ -95,7 +108,9 @@ test('browser_type (no submit)', async ({ client, server }) => {
url: server.PREFIX, url: server.PREFIX,
}, },
}); });
expect(response).toContainTextContent(`- textbox`); expect(response).toHaveResponse({
pageState: expect.stringContaining(`- textbox`),
});
} }
{ {
const response = await client.callTool({ const response = await client.callTool({
@@ -106,14 +121,18 @@ test('browser_type (no submit)', async ({ client, server }) => {
text: 'Hi!', text: 'Hi!',
}, },
}); });
expect(response).toContainTextContent(`fill('Hi!');`); expect(response).toHaveResponse({
// Should yield no snapshot. code: expect.stringContaining(`fill('Hi!')`),
expect(response).not.toContainTextContent(`- textbox`); // Should yield no snapshot.
pageState: expect.not.stringContaining(`- textbox`),
});
} }
{ {
const response = await client.callTool({ const response = await client.callTool({
name: 'browser_console_messages', name: 'browser_console_messages',
}); });
expect(response).toHaveTextContent(/\[LOG\] New value: Hi!/); expect(response).toHaveResponse({
result: expect.stringContaining(`[LOG] New value: Hi!`),
});
} }
}); });

View File

@@ -47,7 +47,9 @@ test('browser_wait_for(text)', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_wait_for', name: 'browser_wait_for',
arguments: { text: 'Text to appear' }, arguments: { text: 'Text to appear' },
})).toContainTextContent(`- generic [ref=e3]: Text to appear`); })).toHaveResponse({
pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`),
});
}); });
test('browser_wait_for(textGone)', async ({ client, server }) => { test('browser_wait_for(textGone)', async ({ client, server }) => {
@@ -81,5 +83,7 @@ test('browser_wait_for(textGone)', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_wait_for', name: 'browser_wait_for',
arguments: { textGone: 'Text to disappear' }, arguments: { textGone: 'Text to disappear' },
})).toContainTextContent(`- generic [ref=e3]: Text to appear`); })).toHaveResponse({
pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`),
});
}); });

View File

@@ -34,5 +34,7 @@ test('do not falsely advertise user agent as a test driver', async ({ client, se
arguments: { arguments: {
url: server.PREFIX, url: server.PREFIX,
}, },
})).toContainTextContent('webdriver: false'); })).toHaveResponse({
pageState: expect.stringContaining(`webdriver: false`),
});
}); });