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-core": "1.55.0-alpha-1752701791000",
"ws": "^8.18.1",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.4"
},
"bin": {

View File

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

View File

@@ -93,8 +93,8 @@ export class Context {
if (!this._tabs.length) {
return [
'### No open tabs',
'Use the "browser_navigate" tool to navigate to a page first.',
'### Open tabs',
'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[]) => ({
content: [{ type: 'text', text: messages.join('\n') }],
content: [{ type: 'text', text: '### Result\n' + messages.join('\n') }],
isError: true,
});
const tool = tools.find(tool => tool.name === request.params.name) as ToolSchema<any>;
if (!tool)
return errorResult(`Tool "${request.params.name}" not found`);
return errorResult(`Error: Tool "${request.params.name}" not found`);
try {
return await backend.callTool(tool, tool.inputSchema.parse(request.params.arguments || {}));

View File

@@ -28,6 +28,7 @@ export class Response {
readonly toolName: string;
readonly toolArgs: Record<string, any>;
private _isError: boolean | undefined;
constructor(context: Context, toolName: string, toolArgs: Record<string, any>) {
this._context = context;
@@ -39,6 +40,11 @@ export class Response {
this._result.push(result);
}
addError(error: string) {
this._result.push(`Error: ${error}`);
this._isError = true;
}
result() {
return this._result.join('\n');
}
@@ -77,7 +83,7 @@ export class Response {
return this._snapshot;
}
async serialize(): Promise<{ content: (TextContent | ImageContent)[] }> {
async serialize(): Promise<{ content: (TextContent | ImageContent)[], isError?: boolean }> {
const response: string[] = [];
// Start with command result.
@@ -116,6 +122,6 @@ ${this._code.join('\n')}
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) => {
response.addCode(`// Resize browser window to ${params.width}x${params.height}`);
response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
await tab.waitForCompletion(async () => {

View File

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

View File

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

View File

@@ -37,7 +37,6 @@ const pdf = defineTabTool({
handle: async (tab, params, response) => {
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.addResult(`Saved page as ${fileName}`);
await tab.page.pdf({ path: fileName });

View File

@@ -63,13 +63,11 @@ const click = defineTabTool({
const button = params.button;
const buttonAttr = button ? `{ button: '${button}' }` : '';
if (params.doubleClick) {
response.addCode(`// Double click ${params.element}`);
if (params.doubleClick)
response.addCode(`await page.${await generateLocator(locator)}.dblclick(${buttonAttr});`);
} else {
response.addCode(`// Click ${params.element}`);
else
response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`);
}
await tab.waitForCompletion(async () => {
if (params.doubleClick)
@@ -151,7 +149,6 @@ const selectOption = defineTabTool({
response.setIncludeSnapshot();
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)});`);
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 modalStates = tab.modalStates().map(state => state.type);
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'));
if (!tool.clearsModalState && modalStates.length)
throw new Error(`Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
return tool.handle(tab, params, response);
response.addError(`The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
else if (!tool.clearsModalState && modalStates.length)
response.addError(`Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
else
return tool.handle(tab, params, response);
},
};
}

View File

@@ -25,7 +25,9 @@ test('cdp server', async ({ cdpServer, startClient, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
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 }) => {
@@ -41,18 +43,21 @@ test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
element: 'Hello, world!',
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({
name: 'browser_snapshot',
})).toHaveTextContent(`### Page state
- Page URL: ${server.HELLO_WORLD}
})).toHaveResponse({
pageState: expect.stringContaining(`- Page URL: ${server.HELLO_WORLD}
- Page Title: Title
- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Hello, world!
\`\`\`
`);
\`\`\``),
});
});
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent(`Error: browserType.connectOverCDP: connect ECONNREFUSED`);
})).toHaveResponse({
result: expect.stringContaining(`Error: browserType.connectOverCDP: connect ECONNREFUSED`),
isError: true,
});
await cdpServer.start();
expect(await client.callTool({
name: 'browser_navigate',
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.

View File

@@ -33,21 +33,10 @@ test('browser_click', async ({ client, server, mcpBrowser }) => {
element: 'Submit button',
ref: 'e2',
},
})).toHaveTextContent(`
### Ran Playwright code
\`\`\`js
// 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]
\`\`\`
`);
})).toHaveResponse({
code: `await page.getByRole('button', { name: 'Submit' }).click();`,
pageState: expect.stringContaining(`- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]`),
});
});
test('browser_click (double)', async ({ client, server }) => {
@@ -73,21 +62,10 @@ test('browser_click (double)', async ({ client, server }) => {
ref: 'e2',
doubleClick: true,
},
})).toHaveTextContent(`
### Ran Playwright code
\`\`\`js
// 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]
\`\`\`
`);
})).toHaveResponse({
code: `await page.getByRole('heading', { name: 'Click me' }).dblclick();`,
pageState: expect.stringContaining(`- heading "Double clicked" [level=1] [ref=e3]`),
});
});
test('browser_click (right)', async ({ client, server }) => {
@@ -114,6 +92,8 @@ test('browser_click (right)', async ({ client, server }) => {
button: 'right',
},
});
expect(result).toContainTextContent(`await page.getByRole('button', { name: 'Menu' }).click({ button: 'right' });`);
expect(result).toContainTextContent(`- button "Right clicked"`);
expect(result).toHaveResponse({
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent(`Hello, world!`);
})).toHaveResponse({
pageState: expect.stringContaining(`Hello, world!`),
});
const files = await fs.promises.readdir(config.browser!.userDataDir!);
expect(files.length).toBeGreaterThan(0);
@@ -58,7 +60,9 @@ test.describe(() => {
expect(await client.callTool({
name: 'browser_navigate',
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({
name: 'browser_console_messages',
});
expect(resource).toHaveTextContent([
'### Result',
`[LOG] Hello, world! @ ${server.PREFIX}:4`,
`[ERROR] Error @ ${server.PREFIX}:5`,
].join('\n'));
expect(resource).toHaveResponse({
result: `[LOG] Hello, world! @ ${server.PREFIX}:4
[ERROR] Error @ ${server.PREFIX}:5`,
});
});
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({
name: 'browser_console_messages',
});
expect(resource).toHaveTextContent(/Error: Error in script/);
expect(resource).toHaveTextContent(new RegExp(server.PREFIX));
expect(resource).toHaveResponse({
result: expect.stringContaining(`Error: Error in script`),
});
expect(resource).toHaveResponse({
result: expect.stringContaining(server.PREFIX),
});
});
test('recent console messages', async ({ client, server }) => {
@@ -91,7 +94,7 @@ test('recent console messages', async ({ client, server }) => {
},
});
expect(response).toContainTextContent(`
### New console messages
- [LOG] Hello, world! @`);
expect(response).toHaveResponse({
consoleMessages: expect.stringContaining(`- [LOG] Hello, world! @`),
});
});

View File

@@ -20,22 +20,15 @@ test('browser_navigate', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveTextContent(`
### Ran Playwright code
\`\`\`js
// Navigate to ${server.HELLO_WORLD}
await page.goto('${server.HELLO_WORLD}');
\`\`\`
### Page state
- Page URL: ${server.HELLO_WORLD}
})).toHaveResponse({
code: `await page.goto('${server.HELLO_WORLD}');`,
pageState: `- Page URL: ${server.HELLO_WORLD}
- Page Title: Title
- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Hello, world!
\`\`\`
`
);
\`\`\``,
});
});
test('browser_select_option', async ({ client, server }) => {
@@ -59,23 +52,17 @@ test('browser_select_option', async ({ client, server }) => {
ref: 'e2',
values: ['bar'],
},
})).toHaveTextContent(`
### Ran Playwright code
\`\`\`js
// Select options [bar] in Select
await page.getByRole('combobox').selectOption(['bar']);
\`\`\`
### Page state
- Page URL: ${server.PREFIX}
})).toHaveResponse({
code: `await page.getByRole('combobox').selectOption(['bar']);`,
pageState: `- Page URL: ${server.PREFIX}
- Page Title: Title
- Page Snapshot:
\`\`\`yaml
- combobox [ref=e2]:
- option "Foo"
- option "Bar" [selected]
\`\`\`
`);
\`\`\``,
});
});
test('browser_select_option (multiple)', async ({ client, server }) => {
@@ -100,24 +87,14 @@ test('browser_select_option (multiple)', async ({ client, server }) => {
ref: 'e2',
values: ['bar', 'baz'],
},
})).toHaveTextContent(`
### Ran Playwright code
\`\`\`js
// 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
})).toHaveResponse({
code: `await page.getByRole('listbox').selectOption(['bar', 'baz']);`,
pageState: expect.stringContaining(`
- listbox [ref=e2]:
- option "Foo" [ref=e3]
- option "Bar" [selected] [ref=e4]
- option "Baz" [selected] [ref=e5]
\`\`\`
`);
- option "Baz" [selected] [ref=e5]`),
});
});
test('browser_resize', async ({ client, server }) => {
@@ -141,12 +118,12 @@ test('browser_resize', async ({ client, server }) => {
height: 780,
},
});
expect(response).toContainTextContent(`### Ran Playwright code
\`\`\`js
// Resize browser window to 390x780
await page.setViewportSize({ width: 390, height: 780 });
\`\`\``);
await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent('Window size: 390x780');
expect(response).toHaveResponse({
code: `await page.setViewportSize({ width: 390, height: 780 });`,
});
await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toHaveResponse({
pageState: expect.stringContaining(`Window size: 390x780`),
});
});
test('old locator error message', async ({ client, server }) => {
@@ -165,10 +142,11 @@ test('old locator error message', async ({ client, server }) => {
arguments: {
url: server.PREFIX,
},
})).toContainTextContent(`
})).toHaveResponse({
pageState: expect.stringContaining(`
- button "Button 1" [ref=e2]
- button "Button 2" [ref=e3]
`.trim());
- button "Button 2" [ref=e3]`),
});
await client.callTool({
name: 'browser_click',
@@ -184,7 +162,10 @@ test('old locator error message', async ({ client, server }) => {
element: 'Button 2',
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 }) => {
@@ -203,5 +184,7 @@ test('visibility: hidden > visible should be shown', { annotation: { type: 'issu
expect(await client.callTool({
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: {
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]');
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
@@ -29,25 +31,31 @@ test('alert dialog', async ({ client, server }) => {
element: 'Button',
ref: 'e2',
},
})).toHaveTextContent(`### Ran Playwright code
\`\`\`js
// Click Button
await page.getByRole('button', { name: 'Button' }).click();
\`\`\`
})).toHaveResponse({
code: `await page.getByRole('button', { name: 'Button' }).click();`,
modalState: `- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`,
});
### Modal state
- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool
`);
expect(await client.callTool({
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',
arguments: {
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 }) => {
@@ -61,7 +69,9 @@ test('two alert dialogs', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]');
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
@@ -69,15 +79,10 @@ test('two alert dialogs', async ({ client, server }) => {
element: 'Button',
ref: 'e2',
},
})).toHaveTextContent(`### Ran Playwright code
\`\`\`js
// Click Button
await page.getByRole('button', { name: 'Button' }).click();
\`\`\`
### Modal state
- ["alert" dialog with message "Alert 1"]: can be handled by the "browser_handle_dialog" tool
`);
})).toHaveResponse({
code: `await page.getByRole('button', { name: 'Button' }).click();`,
modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 1"]: can be handled by the "browser_handle_dialog" tool`),
});
const result = await client.callTool({
name: 'browser_handle_dialog',
@@ -86,9 +91,9 @@ await page.getByRole('button', { name: 'Button' }).click();
},
});
expect(result).toContainTextContent(`### Modal state
- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool
`);
expect(result).toHaveResponse({
modalState: expect.stringContaining(`- ["alert" dialog with message "Alert 2"]: can be handled by the "browser_handle_dialog" tool`),
});
const result2 = await client.callTool({
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 }) => {
@@ -111,7 +118,9 @@ test('confirm dialog (true)', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]');
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
@@ -119,21 +128,19 @@ test('confirm dialog (true)', async ({ client, server }) => {
element: 'Button',
ref: 'e2',
},
})).toContainTextContent(`### Modal state
- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`);
})).toHaveResponse({
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',
arguments: {
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 }) => {
@@ -147,7 +154,9 @@ test('confirm dialog (false)', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]');
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
@@ -155,21 +164,19 @@ test('confirm dialog (false)', async ({ client, server }) => {
element: 'Button',
ref: 'e2',
},
})).toContainTextContent(`### Modal state
- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool
`);
})).toHaveResponse({
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',
arguments: {
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 }) => {
@@ -183,7 +190,9 @@ test('prompt dialog', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]');
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
@@ -191,9 +200,9 @@ test('prompt dialog', async ({ client, server }) => {
element: 'Button',
ref: 'e2',
},
})).toContainTextContent(`### Modal state
- ["prompt" dialog with message "Prompt"]: can be handled by the "browser_handle_dialog" tool
`);
})).toHaveResponse({
modalState: expect.stringContaining(`- ["prompt" dialog with message "Prompt"]: can be handled by the "browser_handle_dialog" tool`),
});
const result = await client.callTool({
name: 'browser_handle_dialog',
@@ -203,10 +212,9 @@ test('prompt dialog', async ({ client, server }) => {
},
});
expect(result).toContainTextContent(`- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Answer
\`\`\``);
expect(result).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Answer`),
});
});
test('alert dialog w/ race', async ({ client, server }) => {
@@ -214,7 +222,9 @@ test('alert dialog w/ race', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent('- button "Button" [ref=e2]');
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
@@ -222,15 +232,10 @@ test('alert dialog w/ race', async ({ client, server }) => {
element: 'Button',
ref: 'e2',
},
})).toHaveTextContent(`### Ran Playwright code
\`\`\`js
// Click Button
await page.getByRole('button', { name: 'Button' }).click();
\`\`\`
### Modal state
- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool
`);
})).toHaveResponse({
code: `await page.getByRole('button', { name: 'Button' }).click();`,
modalState: expect.stringContaining(`- ["alert" dialog with message "Alert"]: can be handled by the "browser_handle_dialog" tool`),
});
const result = await client.callTool({
name: 'browser_handle_dialog',
@@ -239,11 +244,12 @@ await page.getByRole('button', { name: 'Button' }).click();
},
});
expect(result).not.toContainTextContent('### Modal state');
expect(result).toContainTextContent(`### Page state
- Page URL: ${server.PREFIX}
expect(result).toHaveResponse({
modalState: undefined,
pageState: expect.stringContaining(`- Page URL: ${server.PREFIX}
- Page Title:
- Page Snapshot:
\`\`\`yaml
- button "Button"`);
- button "Button"`),
});
});

View File

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

View File

@@ -26,22 +26,21 @@ test('browser_file_upload', async ({ client, server }, testInfo) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent(`
\`\`\`yaml
- generic [active] [ref=e1]:
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]:
- button "Choose File" [ref=e2]
- button "Button" [ref=e3]
\`\`\``);
- button "Button" [ref=e3]`),
});
{
expect(await client.callTool({
name: 'browser_file_upload',
arguments: { paths: [] },
})).toHaveTextContent(`
Error: The tool "browser_file_upload" can only be used when there is related modal state present.
### Modal state
- There is no modal state present
`.trim());
})).toHaveResponse({
isError: true,
result: expect.stringContaining(`The tool "browser_file_upload" can only be used when there is related modal state present.`),
modalState: expect.stringContaining(`- There is no modal state present`),
});
}
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',
ref: 'e2',
},
})).toContainTextContent(`### Modal state
- [File chooser]: can be handled by the "browser_file_upload" tool`);
})).toHaveResponse({
modalState: expect.stringContaining(`- [File chooser]: can be handled by the "browser_file_upload" tool`),
});
const filePath = testInfo.outputPath('test.txt');
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.
### Modal state
- [File chooser]: can be handled by the "browser_file_upload" tool`);
expect(response).toHaveResponse({
result: `Error: Tool "browser_click" does not handle the modal state.`,
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent('- link "Download" [ref=e2]');
})).toHaveResponse({
pageState: expect.stringContaining(`- link "Download" [ref=e2]`),
});
await client.callTool({
name: 'browser_click',
arguments: {
@@ -113,8 +121,9 @@ test('clicking on download link emits download', async ({ startClient, server, m
ref: 'e2',
},
});
await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent(`### Downloads
- Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`);
await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toHaveResponse({
downloads: `- Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`,
});
});
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: {
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']>>;
export const expect = baseExpect.extend({
toHaveTextContent(response: Response, content: string | RegExp) {
toHaveResponse(response: Response, object: any) {
const parsed = parseResponse(response);
const isNot = this.isNot;
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)
expect(texts).not.toContain(content);
expect(parsed).not.toEqual(expect.objectContaining(object));
else
expect(texts).toContain(content);
expect(parsed).toEqual(expect.objectContaining(object));
} catch (e) {
return {
pass: isNot,
@@ -250,3 +223,46 @@ export const expect = baseExpect.extend({
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);
}
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.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('browser', async ({ client, server, mcpBrowser }) => {
test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser ?? ''), 'Only chrome is supported for this test');
server.route('/', (req, res) => {
@@ -40,11 +41,9 @@ for (const mcpHeadless of [false, true]) {
},
});
expect(response).toContainTextContent(`Mozilla/5.0`);
if (mcpHeadless)
expect(response).toContainTextContent(`HeadlessChrome`);
else
expect(response).not.toContainTextContent(`HeadlessChrome`);
expect(response).toHaveResponse({
pageState: (mcpHeadless ? expect : expect.not).stringContaining(`HeadlessChrome`),
});
});
});
}

View File

@@ -22,9 +22,8 @@ test('stitched aria frames', async ({ client }) => {
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>`,
},
})).toContainTextContent(`
\`\`\`yaml
- generic [active] [ref=e1]:
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]:
- heading "Hello" [level=1] [ref=e2]
- iframe [ref=e3]:
- generic [active] [ref=f1e1]:
@@ -32,7 +31,8 @@ test('stitched aria frames', async ({ client }) => {
- main [ref=f1e3]:
- iframe [ref=f1e4]:
- paragraph [ref=f2e2]: Nested
\`\`\``);
\`\`\``),
});
expect(await client.callTool({
name: 'browser_click',
@@ -40,5 +40,7 @@ test('stitched aria frames', async ({ client }) => {
element: 'World',
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');
expect(await client.callTool({
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({
name: 'browser_close',
})).toContainTextContent(`### Ran Playwright code
\`\`\`js
await page.close()
\`\`\`
### No open tabs
Use the "browser_navigate" tool to navigate to a page first.`);
})).toHaveResponse({
code: `await page.close()`,
tabs: `No open tabs. Use the "browser_navigate" tool to navigate to a page first.`,
});
expect(await client.callTool({
name: 'browser_navigate',
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();
@@ -68,7 +67,10 @@ test('executable path', async ({ startClient, server }) => {
name: 'browser_navigate',
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 }) => {
@@ -82,11 +84,12 @@ test('persistent context', async ({ startClient, server }) => {
`, 'text/html');
const { client } = await startClient();
const response = await client.callTool({
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: NO`),
});
expect(response).toContainTextContent(`Storage: NO`);
await new Promise(resolve => setTimeout(resolve, 3000));
@@ -95,12 +98,12 @@ test('persistent context', async ({ startClient, server }) => {
});
const { client: client2 } = await startClient();
const response2 = await client2.callTool({
expect(await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: YES`),
});
expect(response2).toContainTextContent(`Storage: YES`);
});
test('isolated context', async ({ startClient, server }) => {
@@ -114,22 +117,24 @@ test('isolated context', async ({ startClient, server }) => {
`, 'text/html');
const { client: client1 } = await startClient({ args: [`--isolated`] });
const response = await client1.callTool({
expect(await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: NO`),
});
expect(response).toContainTextContent(`Storage: NO`);
await client1.callTool({
name: 'browser_close',
});
const { client: client2 } = await startClient({ args: [`--isolated`] });
const response2 = await client2.callTool({
expect(await client2.callTool({
name: 'browser_navigate',
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) => {
@@ -155,9 +160,10 @@ test('isolated context with storage state', async ({ startClient, server }, test
`--isolated`,
`--storage-state=${storageStatePath}`,
] });
const response = await client.callTool({
expect(await client.callTool({
name: 'browser_navigate',
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({
name: 'browser_network_requests',
})).toHaveTextContent(`### Result
[GET] ${`${server.PREFIX}`} => [200] OK
[GET] ${`${server.PREFIX}json`} => [200] OK`);
})).toHaveResponse({
result: expect.stringContaining(`[GET] ${`${server.PREFIX}`} => [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({
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) => {
@@ -40,12 +43,16 @@ test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [active] [ref=e1]: Hello, world!`);
const response = await client.callTool({
name: 'browser_pdf_save',
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
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) => {
@@ -58,14 +65,19 @@ test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, ser
expect(await client.callTool({
name: 'browser_navigate',
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({
name: 'browser_pdf_save',
arguments: {
filename: 'output.pdf',
},
})).toContainTextContent(`output.pdf`);
})).toHaveResponse({
result: expect.stringContaining(`output.pdf`),
code: expect.stringContaining(`await page.pdf(`),
});
const files = [...fs.readdirSync(outputDir)];

View File

@@ -25,7 +25,9 @@ test('browser_take_screenshot (viewport)', async ({ startClient, server }, testI
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`);
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
@@ -51,7 +53,9 @@ test('browser_take_screenshot (element)', async ({ startClient, server }, testIn
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`[ref=e1]`);
})).toHaveResponse({
pageState: expect.stringContaining(`[ref=e1]`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
@@ -82,7 +86,9 @@ test('--output-dir should work', async ({ startClient, server }, testInfo) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`);
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
await client.callTool({
name: 'browser_take_screenshot',
@@ -104,7 +110,9 @@ for (const raw of [undefined, true]) {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toContainTextContent(`Navigate to http://localhost`);
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
@@ -144,7 +152,9 @@ test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient,
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`);
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
@@ -184,7 +194,9 @@ test('browser_take_screenshot (imageResponses=omit)', async ({ startClient, serv
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`);
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
await client.callTool({
name: 'browser_take_screenshot',
@@ -209,7 +221,9 @@ test('browser_take_screenshot (fullPage: true)', async ({ startClient, server },
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`Navigate to http://localhost`);
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
@@ -236,7 +250,9 @@ test('browser_take_screenshot (fullPage with element should error)', async ({ st
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`[ref=e1]`);
})).toHaveResponse({
pageState: expect.stringContaining(`[ref=e1]`),
});
const result = await client.callTool({
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)
expect(await client.callTool({
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
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 }) => {
expect(await client.callTool({
name: 'browser_tab_list',
})).toHaveTextContent(`### Open tabs
- 0: (current) [] (about:blank)`);
})).toHaveResponse({
tabs: `- 0: (current) [] (about:blank)`,
});
});
test('list first tab', async ({ client }) => {
await createTab(client, 'Tab one', 'Body one');
expect(await client.callTool({
name: 'browser_tab_list',
})).toHaveTextContent(`### Open tabs
- 0: [] (about:blank)
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`);
})).toHaveResponse({
tabs: `- 0: [] (about:blank)
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`,
});
});
test('create new tab', async ({ client }) => {
const result = await createTab(client, 'Tab one', 'Body one');
expect(result).toContainTextContent(`### Open tabs
- 0: [] (about:blank)
- 1: (current) [Tab one] (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>
expect(await createTab(client, 'Tab one', 'Body one')).toHaveResponse({
tabs: `- 0: [] (about:blank)
- 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>
- Page Title: Tab one
- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Body one
\`\`\``);
\`\`\``),
});
const result2 = await createTab(client, 'Tab two', 'Body two');
expect(result2).toContainTextContent(`### Open tabs
- 0: [] (about:blank)
expect(await createTab(client, 'Tab two', 'Body two')).toHaveResponse({
tabs: `- 0: [] (about:blank)
- 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>)
`);
expect(result2).toContainTextContent(`
### Page state
- Page URL: 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>
- Page Title: Tab two
- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Body two
\`\`\``);
\`\`\``),
});
});
test('select tab', async ({ client }) => {
await createTab(client, 'Tab one', 'Body one');
await createTab(client, 'Tab two', 'Body two');
const result = await client.callTool({
expect(await client.callTool({
name: 'browser_tab_select',
arguments: {
index: 1,
},
});
expect(result).toContainTextContent(`### Open tabs
- 0: [] (about:blank)
})).toHaveResponse({
tabs: `- 0: [] (about:blank)
- 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>)`);
expect(result).toContainTextContent(`
### Page state
- Page URL: 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>)`,
pageState: expect.stringContaining(`- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one
- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Body one
\`\`\``);
\`\`\``),
});
});
test('close tab', async ({ client }) => {
await createTab(client, 'Tab one', 'Body one');
await createTab(client, 'Tab two', 'Body two');
const result = await client.callTool({
expect(await client.callTool({
name: 'browser_tab_close',
arguments: {
index: 2,
},
});
expect(result).toContainTextContent(`### Open tabs
- 0: [] (about:blank)
- 1: (current) [Tab one] (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>
})).toHaveResponse({
tabs: `- 0: [] (about:blank)
- 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>
- Page Title: Tab one
- Page Snapshot:
\`\`\`yaml
- generic [active] [ref=e1]: Body one
\`\`\``);
\`\`\``),
});
});
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({
name: 'browser_navigate',
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();
});

View File

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