chore: only reset network log upon explicit navigation (#377)

Fixes https://github.com/microsoft/playwright-mcp/issues/376
This commit is contained in:
Pavel Feldman 2025-05-08 17:02:09 -07:00 committed by GitHub
parent 85c85bd2fb
commit 57b3c14276
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 228 additions and 179 deletions

View File

@ -35,10 +35,6 @@ export class Tab {
page.on('console', event => this._consoleMessages.push(event)); page.on('console', event => this._consoleMessages.push(event));
page.on('request', request => this._requests.set(request, null)); page.on('request', request => this._requests.set(request, null));
page.on('response', response => this._requests.set(response.request(), response)); page.on('response', response => this._requests.set(response.request(), response));
page.on('framenavigated', frame => {
if (!frame.parentFrame())
this._clearCollectedArtifacts();
});
page.on('close', () => this._onClose()); page.on('close', () => this._onClose());
page.on('filechooser', chooser => { page.on('filechooser', chooser => {
this.context.setModalState({ this.context.setModalState({
@ -66,6 +62,8 @@ export class Tab {
} }
async navigate(url: string) { async navigate(url: string) {
this._clearCollectedArtifacts();
const downloadEvent = this.page.waitForEvent('download').catch(() => {}); const downloadEvent = this.page.waitForEvent('download').catch(() => {});
try { try {
await this.page.goto(url, { waitUntil: 'domcontentloaded' }); await this.page.goto(url, { waitUntil: 'domcontentloaded' });

View File

@ -16,13 +16,11 @@
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('cdp server', async ({ cdpEndpoint, startClient }) => { test('cdp server', async ({ cdpEndpoint, startClient, server }) => {
const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] }); const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] });
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`); })).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`);
}); });
@ -55,20 +53,22 @@ test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => {
`); `);
}); });
test('should throw connection error and allow re-connecting', async ({ cdpEndpoint, startClient }) => { test('should throw connection error and allow re-connecting', async ({ cdpEndpoint, startClient, server }) => {
const port = 3200 + test.info().parallelIndex; const port = 3200 + test.info().parallelIndex;
const client = await startClient({ args: [`--cdp-endpoint=http://localhost:${port}`] }); const client = await startClient({ args: [`--cdp-endpoint=http://localhost:${port}`] });
server.setContent('/', `
<title>Title</title>
<body>Hello, world!</body>
`, 'text/html');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toContainTextContent(`Error: browserType.connectOverCDP: connect ECONNREFUSED`); })).toContainTextContent(`Error: browserType.connectOverCDP: connect ECONNREFUSED`);
await cdpEndpoint(port); await cdpEndpoint(port);
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`); })).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`);
}); });

View File

@ -19,7 +19,12 @@ import fs from 'node:fs';
import { Config } from '../config.js'; import { Config } from '../config.js';
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('config user data dir', async ({ startClient, localOutputPath }) => { test('config user data dir', async ({ startClient, localOutputPath, server }) => {
server.setContent('/', `
<title>Title</title>
<body>Hello, world!</body>
`, 'text/html');
const config: Config = { const config: Config = {
browser: { browser: {
userDataDir: localOutputPath('user-data-dir'), userDataDir: localOutputPath('user-data-dir'),
@ -31,9 +36,7 @@ test('config user data dir', async ({ startClient, localOutputPath }) => {
const client = await startClient({ args: ['--config', configPath] }); const client = await startClient({ args: ['--config', configPath] });
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><body>Hello, world!</body></html>',
},
})).toContainTextContent(`Hello, world!`); })).toContainTextContent(`Hello, world!`);
const files = await fs.promises.readdir(config.browser!.userDataDir!); const files = await fs.promises.readdir(config.browser!.userDataDir!);

View File

@ -16,11 +16,21 @@
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('browser_console_messages', async ({ client }) => { test('browser_console_messages', async ({ client, server }) => {
server.setContent('/', `
<!DOCTYPE html>
<html>
<script>
console.log("Hello, world!");
console.error("Error");
</script>
</html>
`, 'text/html');
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: {
url: 'data:text/html,<html><script>console.log("Hello, world!");console.error("Error"); </script></html>', url: server.PREFIX,
}, },
}); });

View File

@ -16,20 +16,18 @@
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('browser_navigate', async ({ client }) => { test('browser_navigate', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toHaveTextContent(` })).toHaveTextContent(`
- Ran Playwright code: - Ran Playwright code:
\`\`\`js \`\`\`js
// Navigate to data:text/html,<html><title>Title</title><body>Hello, world!</body></html> // Navigate to ${server.HELLO_WORLD}
await page.goto('data:text/html,<html><title>Title</title><body>Hello, world!</body></html>'); await page.goto('${server.HELLO_WORLD}');
\`\`\` \`\`\`
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html> - Page URL: ${server.HELLO_WORLD}
- Page Title: Title - Page Title: Title
- Page Snapshot - Page Snapshot
\`\`\`yaml \`\`\`yaml
@ -39,12 +37,15 @@ await page.goto('data:text/html,<html><title>Title</title><body>Hello, world!</b
); );
}); });
test('browser_click', async ({ client }) => { test('browser_click', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<button>Submit</button>
`, 'text/html');
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><button>Submit</button></html>',
},
}); });
expect(await client.callTool({ expect(await client.callTool({
@ -60,7 +61,7 @@ test('browser_click', async ({ client }) => {
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
\`\`\` \`\`\`
- Page URL: data:text/html,<html><title>Title</title><button>Submit</button></html> - Page URL: ${server.PREFIX}
- Page Title: Title - Page Title: Title
- Page Snapshot - Page Snapshot
\`\`\`yaml \`\`\`yaml
@ -69,12 +70,18 @@ await page.getByRole('button', { name: 'Submit' }).click();
`); `);
}); });
test('browser_select_option', async ({ client }) => { test('browser_select_option', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<select>
<option value="foo">Foo</option>
<option value="bar">Bar</option>
</select>
`, 'text/html');
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><select><option value="foo">Foo</option><option value="bar">Bar</option></select></html>',
},
}); });
expect(await client.callTool({ expect(await client.callTool({
@ -91,7 +98,7 @@ test('browser_select_option', async ({ client }) => {
await page.getByRole('combobox').selectOption(['bar']); await page.getByRole('combobox').selectOption(['bar']);
\`\`\` \`\`\`
- Page URL: data:text/html,<html><title>Title</title><select><option value="foo">Foo</option><option value="bar">Bar</option></select></html> - Page URL: ${server.PREFIX}
- Page Title: Title - Page Title: Title
- Page Snapshot - Page Snapshot
\`\`\`yaml \`\`\`yaml
@ -102,12 +109,19 @@ await page.getByRole('combobox').selectOption(['bar']);
`); `);
}); });
test('browser_select_option (multiple)', async ({ client }) => { test('browser_select_option (multiple)', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<select multiple>
<option value="foo">Foo</option>
<option value="bar">Bar</option>
<option value="baz">Baz</option>
</select>
`, 'text/html');
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><select multiple><option value="foo">Foo</option><option value="bar">Bar</option><option value="baz">Baz</option></select></html>',
},
}); });
expect(await client.callTool({ expect(await client.callTool({
@ -124,7 +138,7 @@ test('browser_select_option (multiple)', async ({ client }) => {
await page.getByRole('listbox').selectOption(['bar', 'baz']); await page.getByRole('listbox').selectOption(['bar', 'baz']);
\`\`\` \`\`\`
- Page URL: data:text/html,<html><title>Title</title><select multiple><option value="foo">Foo</option><option value="bar">Bar</option><option value="baz">Baz</option></select></html> - Page URL: ${server.PREFIX}
- Page Title: Title - Page Title: Title
- Page Snapshot - Page Snapshot
\`\`\`yaml \`\`\`yaml
@ -136,11 +150,18 @@ await page.getByRole('listbox').selectOption(['bar', 'baz']);
`); `);
}); });
test('browser_type', async ({ client }) => { test('browser_type', async ({ client, server }) => {
server.setContent('/', `
<!DOCTYPE html>
<html>
<input type='keypress' onkeypress="console.log('Key pressed:', event.key, ', Text:', event.target.value)"></input>
</html>
`, 'text/html');
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: {
url: `data:text/html,<input type='keypress' onkeypress="console.log('Key pressed:', event.key, ', Text:', event.target.value)"></input>`, url: server.PREFIX,
}, },
}); });
await client.callTool({ await client.callTool({
@ -158,11 +179,15 @@ test('browser_type', async ({ client }) => {
})).toHaveTextContent('[LOG] Key pressed: Enter , Text: Hi!'); })).toHaveTextContent('[LOG] Key pressed: Enter , Text: Hi!');
}); });
test('browser_type (slowly)', async ({ client }) => { test('browser_type (slowly)', async ({ client, server }) => {
server.setContent('/', `
<input type='text' onkeydown="console.log('Key pressed:', event.key, 'Text:', event.target.value)"></input>
`, 'text/html');
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: {
url: `data:text/html,<input type='text' onkeydown="console.log('Key pressed:', event.key, 'Text:', event.target.value)"></input>`, url: server.PREFIX,
}, },
}); });
await client.callTool({ await client.callTool({
@ -186,12 +211,18 @@ test('browser_type (slowly)', async ({ client }) => {
].join('\n')); ].join('\n'));
}); });
test('browser_resize', async ({ client }) => { test('browser_resize', async ({ client, server }) => {
server.setContent('/', `
<title>Resize Test</title>
<body>
<div id="size">Waiting for resize...</div>
<script>new ResizeObserver(() => { document.getElementById("size").textContent = \`Window size: \${window.innerWidth}x\${window.innerHeight}\`; }).observe(document.body);
</script>
</body>
`, 'text/html');
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Resize Test</title><body><div id="size">Waiting for resize...</div><script>new ResizeObserver(() => { document.getElementById("size").textContent = `Window size: ${window.innerWidth}x${window.innerHeight}`; }).observe(document.body);</script></body></html>',
},
}); });
const response = await client.callTool({ const response = await client.callTool({

View File

@ -19,12 +19,11 @@ import { test, expect } from './fixtures.js';
// https://github.com/microsoft/playwright/issues/35663 // https://github.com/microsoft/playwright/issues/35663
test.skip(({ mcpBrowser, mcpHeadless }) => mcpBrowser === 'webkit' && mcpHeadless); test.skip(({ mcpBrowser, mcpHeadless }) => mcpBrowser === 'webkit' && mcpHeadless);
test('alert dialog', async ({ client }) => { test('alert dialog', async ({ client, server }) => {
server.setContent('/', `<button onclick="alert('Alert')">Button</button>`, 'text/html');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><button onclick="alert(\'Alert\')">Button</button></html>',
},
})).toContainTextContent('- button "Button" [ref=s1e3]'); })).toContainTextContent('- button "Button" [ref=s1e3]');
expect(await client.callTool({ expect(await client.callTool({
@ -55,8 +54,8 @@ await page.getByRole('button', { name: 'Button' }).click();
// <internal code to handle "alert" dialog> // <internal code to handle "alert" dialog>
\`\`\` \`\`\`
- Page URL: data:text/html,<html><title>Title</title><button onclick="alert('Alert')">Button</button></html> - Page URL: ${server.PREFIX}
- Page Title: Title - Page Title:
- Page Snapshot - Page Snapshot
\`\`\`yaml \`\`\`yaml
- button "Button" [ref=s2e3] - button "Button" [ref=s2e3]
@ -64,13 +63,19 @@ await page.getByRole('button', { name: 'Button' }).click();
`); `);
}); });
test('two alert dialogs', async ({ client }) => { test('two alert dialogs', async ({ client, server }) => {
test.fixme(true, 'Race between the dialog and ariaSnapshot'); test.fixme(true, 'Race between the dialog and ariaSnapshot');
server.setContent('/', `
<title>Title</title>
<body>
<button onclick="alert('Alert 1');alert('Alert 2');">Button</button>
</body>
`, 'text/html');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><button onclick="alert(\'Alert 1\');alert(\'Alert 2\');">Button</button></html>',
},
})).toContainTextContent('- button "Button" [ref=s1e3]'); })).toContainTextContent('- button "Button" [ref=s1e3]');
expect(await client.callTool({ expect(await client.callTool({
@ -98,12 +103,17 @@ await page.getByRole('button', { name: 'Button' }).click();
expect(result).not.toContainTextContent('### Modal state'); expect(result).not.toContainTextContent('### Modal state');
}); });
test('confirm dialog (true)', async ({ client }) => { test('confirm dialog (true)', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<body>
<button onclick="document.body.textContent = confirm('Confirm')">Button</button>
</body>
`, 'text/html');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><button onclick="document.body.textContent = confirm(\'Confirm\')">Button</button></html>',
},
})).toContainTextContent('- button "Button" [ref=s1e3]'); })).toContainTextContent('- button "Button" [ref=s1e3]');
expect(await client.callTool({ expect(await client.callTool({
@ -130,12 +140,17 @@ test('confirm dialog (true)', async ({ client }) => {
\`\`\``); \`\`\``);
}); });
test('confirm dialog (false)', async ({ client }) => { test('confirm dialog (false)', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<body>
<button onclick="document.body.textContent = confirm('Confirm')">Button</button>
</body>
`, 'text/html');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><button onclick="document.body.textContent = confirm(\'Confirm\')">Button</button></html>',
},
})).toContainTextContent('- button "Button" [ref=s1e3]'); })).toContainTextContent('- button "Button" [ref=s1e3]');
expect(await client.callTool({ expect(await client.callTool({
@ -160,12 +175,17 @@ test('confirm dialog (false)', async ({ client }) => {
\`\`\``); \`\`\``);
}); });
test('prompt dialog', async ({ client }) => { test('prompt dialog', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<body>
<button onclick="document.body.textContent = prompt('Prompt')">Button</button>
</body>
`, 'text/html');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><button onclick="document.body.textContent = prompt(\'Prompt\')">Button</button></html>',
},
})).toContainTextContent('- button "Button" [ref=s1e3]'); })).toContainTextContent('- button "Button" [ref=s1e3]');
expect(await client.callTool({ expect(await client.callTool({

View File

@ -18,12 +18,15 @@ import { test, expect } from './fixtures.js';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
test('browser_file_upload', async ({ client, localOutputPath }) => { test('browser_file_upload', async ({ client, localOutputPath, server }) => {
server.setContent('/', `
<input type="file" />
<button>Button</button>
`, 'text/html');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><input type="file" /><button>Button</button></html>',
},
})).toContainTextContent(` })).toContainTextContent(`
\`\`\`yaml \`\`\`yaml
- button "Choose File" [ref=s1e3] - button "Choose File" [ref=s1e3]
@ -96,17 +99,18 @@ The tool "browser_file_upload" can only be used when there is related modal stat
} }
}); });
test('clicking on download link emits download', async ({ startClient, localOutputPath }) => { test('clicking on download link emits download', async ({ startClient, localOutputPath, server }) => {
const outputDir = localOutputPath('output'); const outputDir = localOutputPath('output');
const client = await startClient({ const client = await startClient({
config: { outputDir }, config: { outputDir },
}); });
server.setContent('/', `<a href="/download" download="test.txt">Download</a>`, 'text/html');
server.setContent('/download', 'Data', 'text/plain');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<a href="data:text/plain,Hello world!" download="test.txt">Download</a>',
},
})).toContainTextContent('- link "Download" [ref=s1e3]'); })).toContainTextContent('- link "Download" [ref=s1e3]');
await client.callTool({ await client.callTool({
name: 'browser_click', name: 'browser_click',
@ -133,7 +137,7 @@ test('navigating to download link emits download', async ({ client, server, mcpB
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: {
url: server.PREFIX + '/download', url: server.PREFIX + 'download',
}, },
})).toContainTextContent('### Downloads'); })).toContainTextContent('### Downloads');
}); });

View File

@ -16,12 +16,10 @@
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('test reopen browser', async ({ client }) => { test('test reopen browser', async ({ client, server }) => {
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
}); });
expect(await client.callTool({ expect(await client.callTool({
@ -31,19 +29,15 @@ test('test reopen browser', async ({ client }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`); })).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`);
}); });
test('executable path', async ({ startClient }) => { test('executable path', async ({ startClient, server }) => {
const client = await startClient({ args: [`--executable-path=bogus`] }); const client = await startClient({ args: [`--executable-path=bogus`] });
const response = await client.callTool({ const response = await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
}); });
expect(response).toContainTextContent(`executable doesn't exist`); expect(response).toContainTextContent(`executable doesn't exist`);
}); });

View File

@ -17,15 +17,11 @@
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('browser_network_requests', async ({ client, server }) => { test('browser_network_requests', async ({ client, server }) => {
server.route('/', (req, res) => { server.setContent('/', `
res.writeHead(200, { 'Content-Type': 'text/html' }); <button onclick="fetch('/json')">Click me</button>
res.end(`<button onclick="fetch('/json')">Click me</button>`); `, 'text/html');
});
server.route('/json', (req, res) => { server.setContent('/json', JSON.stringify({ name: 'John Doe' }), 'application/json');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ name: 'John Doe' }));
});
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
@ -45,5 +41,6 @@ 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',
arguments: {}, arguments: {},
})).toHaveTextContent(`[GET] ${`${server.PREFIX}/json`} => [200] OK`); })).toHaveTextContent(`[GET] ${`${server.PREFIX}`} => [200] OK
[GET] ${`${server.PREFIX}json`} => [200] OK`);
}); });

View File

@ -18,13 +18,11 @@ import fs from 'fs';
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('save as pdf unavailable', async ({ startClient }) => { test('save as pdf unavailable', async ({ startClient, server }) => {
const client = await startClient({ args: ['--caps="no-pdf"'] }); const client = await startClient({ args: ['--caps="no-pdf"'] });
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
}); });
expect(await client.callTool({ expect(await client.callTool({
@ -32,13 +30,12 @@ test('save as pdf unavailable', async ({ startClient }) => {
})).toHaveTextContent(/Tool \"browser_pdf_save\" not found/); })).toHaveTextContent(/Tool \"browser_pdf_save\" not found/);
}); });
test('save as pdf', async ({ client, mcpBrowser }) => { test('save as pdf', async ({ client, mcpBrowser, server }) => {
test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.'); test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`); })).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`);
const response = await client.callTool({ const response = await client.callTool({
@ -48,18 +45,16 @@ test('save as pdf', async ({ client, mcpBrowser }) => {
expect(response).toHaveTextContent(/Save page as.*page-[^:]+.pdf/); expect(response).toHaveTextContent(/Save page as.*page-[^:]+.pdf/);
}); });
test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser }, testInfo) => { test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, server, localOutputPath }) => {
test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.'); test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
const outputDir = testInfo.outputPath('output'); const outputDir = localOutputPath('output');
const client = await startClient({ const client = await startClient({
config: { outputDir }, config: { outputDir },
}); });
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`); })).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`);
expect(await client.callTool({ expect(await client.callTool({

View File

@ -31,11 +31,8 @@ const fetchPage = async (client: Client, url: string) => {
}; };
test('default to allow all', async ({ server, client }) => { test('default to allow all', async ({ server, client }) => {
server.route('/ppp', (_req, res) => { server.setContent('/ppp', 'content:PPP', 'text/html');
res.writeHead(200, { 'Content-Type': 'text/html' }); const result = await fetchPage(client, server.PREFIX + 'ppp');
res.end('content:PPP');
});
const result = await fetchPage(client, server.PREFIX + '/ppp');
expect(result).toContain('content:PPP'); expect(result).toContain('content:PPP');
}); });
@ -48,14 +45,11 @@ test('blocked works', async ({ startClient }) => {
}); });
test('allowed works', async ({ server, startClient }) => { test('allowed works', async ({ server, startClient }) => {
server.route('/ppp', (_req, res) => { server.setContent('/ppp', 'content:PPP', 'text/html');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('content:PPP');
});
const client = await startClient({ const client = await startClient({
args: ['--allowed-origins', `microsoft.com;${new URL(server.PREFIX).host};playwright.dev`] args: ['--allowed-origins', `microsoft.com;${new URL(server.PREFIX).host};playwright.dev`]
}); });
const result = await fetchPage(client, server.PREFIX + '/ppp'); const result = await fetchPage(client, server.PREFIX + 'ppp');
expect(result).toContain('content:PPP'); expect(result).toContain('content:PPP');
}); });
@ -79,13 +73,10 @@ test('allowed without blocked blocks all non-explicitly specified origins', asyn
}); });
test('blocked without allowed allows non-explicitly specified origins', async ({ server, startClient }) => { test('blocked without allowed allows non-explicitly specified origins', async ({ server, startClient }) => {
server.route('/ppp', (_req, res) => { server.setContent('/ppp', 'content:PPP', 'text/html');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('content:PPP');
});
const client = await startClient({ const client = await startClient({
args: ['--blocked-origins', 'example.com'], args: ['--blocked-origins', 'example.com'],
}); });
const result = await fetchPage(client, server.PREFIX + '/ppp'); const result = await fetchPage(client, server.PREFIX + 'ppp');
expect(result).toContain('content:PPP'); expect(result).toContain('content:PPP');
}); });

View File

@ -18,13 +18,11 @@ import fs from 'fs';
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('browser_take_screenshot (viewport)', async ({ client }) => { test('browser_take_screenshot (viewport)', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>', })).toContainTextContent(`Navigate to http://localhost`);
},
})).toContainTextContent(`Navigate to data:text/html`);
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@ -44,19 +42,17 @@ test('browser_take_screenshot (viewport)', async ({ client }) => {
}); });
}); });
test('browser_take_screenshot (element)', async ({ client }) => { test('browser_take_screenshot (element)', async ({ client, server }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><button>Hello, world!</button></html>', })).toContainTextContent(`[ref=s1e2]`);
},
})).toContainTextContent(`[ref=s1e3]`);
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
arguments: { arguments: {
element: 'hello button', element: 'hello button',
ref: 's1e3', ref: 's1e2',
}, },
})).toEqual({ })).toEqual({
content: [ content: [
@ -66,24 +62,22 @@ test('browser_take_screenshot (element)', async ({ client }) => {
type: 'image', type: 'image',
}, },
{ {
text: expect.stringContaining(`page.getByRole('button', { name: 'Hello, world!' }).screenshot`), text: expect.stringContaining(`page.getByText('Hello, world!').screenshot`),
type: 'text', type: 'text',
}, },
], ],
}); });
}); });
test('--output-dir should work', async ({ startClient, localOutputPath }) => { test('--output-dir should work', async ({ startClient, localOutputPath, server }) => {
const outputDir = localOutputPath('output'); const outputDir = localOutputPath('output');
const client = await startClient({ const client = await startClient({
args: ['--output-dir', outputDir], args: ['--output-dir', outputDir],
}); });
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>', })).toContainTextContent(`Navigate to http://localhost`);
},
})).toContainTextContent(`Navigate to data:text/html`);
await client.callTool({ await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@ -95,24 +89,20 @@ test('--output-dir should work', async ({ startClient, localOutputPath }) => {
}); });
for (const raw of [undefined, true]) { for (const raw of [undefined, true]) {
test(`browser_take_screenshot (raw: ${raw})`, async ({ startClient }, testInfo) => { test(`browser_take_screenshot (raw: ${raw})`, async ({ startClient, localOutputPath, server }) => {
const ext = raw ? 'png' : 'jpeg'; const ext = raw ? 'png' : 'jpeg';
const outputDir = testInfo.outputPath('output'); const outputDir = localOutputPath('output');
const client = await startClient({ const client = await startClient({
config: { outputDir }, config: { outputDir },
}); });
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>', })).toContainTextContent(`Navigate to http://localhost`);
},
})).toContainTextContent(`Navigate to data:text/html`);
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
arguments: { arguments: { raw },
raw,
},
})).toEqual({ })).toEqual({
content: [ content: [
{ {
@ -140,17 +130,15 @@ for (const raw of [undefined, true]) {
} }
test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient }, testInfo) => { test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient, localOutputPath, server }) => {
const outputDir = testInfo.outputPath('output'); const outputDir = localOutputPath('output');
const client = await startClient({ const client = await startClient({
config: { outputDir }, config: { outputDir },
}); });
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>', })).toContainTextContent(`Navigate to http://localhost`);
},
})).toContainTextContent(`Navigate to data:text/html`);
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@ -178,7 +166,7 @@ test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient }
expect(files[0]).toMatch(/^output.jpeg$/); expect(files[0]).toMatch(/^output.jpeg$/);
}); });
test('browser_take_screenshot (noImageResponses)', async ({ startClient }) => { test('browser_take_screenshot (noImageResponses)', async ({ startClient, server }) => {
const client = await startClient({ const client = await startClient({
config: { config: {
noImageResponses: true, noImageResponses: true,
@ -187,10 +175,8 @@ test('browser_take_screenshot (noImageResponses)', async ({ startClient }) => {
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>', })).toContainTextContent(`Navigate to http://localhost`);
},
})).toContainTextContent(`Navigate to data:text/html`);
await client.callTool({ await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
@ -210,15 +196,13 @@ test('browser_take_screenshot (noImageResponses)', async ({ startClient }) => {
}); });
}); });
test('browser_take_screenshot (cursor)', async ({ startClient }) => { test('browser_take_screenshot (cursor)', async ({ startClient, server }) => {
const client = await startClient({ clientName: 'cursor:vscode' }); const client = await startClient({ clientName: 'cursor:vscode' });
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.HELLO_WORLD },
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>', })).toContainTextContent(`Navigate to http://localhost`);
},
})).toContainTextContent(`Navigate to data:text/html`);
await client.callTool({ await client.callTool({
name: 'browser_take_screenshot', name: 'browser_take_screenshot',

View File

@ -141,7 +141,9 @@ test('close tab', async ({ client }) => {
\`\`\``); \`\`\``);
}); });
test('reuse first tab when navigating', async ({ startClient, cdpEndpoint }) => { test('reuse first tab when navigating', async ({ startClient, cdpEndpoint, server }) => {
server.setContent('/', `<title>Title</title><body>Body</body>`, 'text/html');
const browser = await chromium.connectOverCDP(await cdpEndpoint()); const browser = await chromium.connectOverCDP(await cdpEndpoint());
const [context] = browser.contexts(); const [context] = browser.contexts();
const pages = context.pages(); const pages = context.pages();
@ -149,9 +151,7 @@ test('reuse first tab when navigating', async ({ startClient, cdpEndpoint }) =>
const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] }); const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] });
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { arguments: { url: server.PREFIX },
url: 'data:text/html,<title>Title</title><body>Body</body>',
},
}); });
expect(pages.length).toBe(1); expect(pages.length).toBe(1);

View File

@ -38,6 +38,7 @@ export class TestServer {
readonly PORT: number; readonly PORT: number;
readonly PREFIX: string; readonly PREFIX: string;
readonly CROSS_PROCESS_PREFIX: string; readonly CROSS_PROCESS_PREFIX: string;
readonly HELLO_WORLD: string;
static async create(port: number): Promise<TestServer> { static async create(port: number): Promise<TestServer> {
const server = new TestServer(port); const server = new TestServer(port);
@ -67,8 +68,9 @@ export class TestServer {
const same_origin = 'localhost'; const same_origin = 'localhost';
const protocol = sslOptions ? 'https' : 'http'; const protocol = sslOptions ? 'https' : 'http';
this.PORT = port; this.PORT = port;
this.PREFIX = `${protocol}://${same_origin}:${port}`; this.PREFIX = `${protocol}://${same_origin}:${port}/`;
this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}`; this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}/`;
this.HELLO_WORLD = `${this.PREFIX}hello-world`;
} }
setCSP(path: string, csp: string) { setCSP(path: string, csp: string) {
@ -88,6 +90,13 @@ export class TestServer {
this._routes.set(path, handler); this._routes.set(path, handler);
} }
setContent(path: string, content: string, mimeType: string) {
this.route(path, (req, res) => {
res.writeHead(200, { 'Content-Type': mimeType });
res.end(mimeType === 'text/html' ? `<!DOCTYPE html>${content}` : content);
});
}
redirect(from: string, to: string) { redirect(from: string, to: string) {
this.route(from, (req, res) => { this.route(from, (req, res) => {
const headers = this._extraHeaders.get(req.url!) || {}; const headers = this._extraHeaders.get(req.url!) || {};
@ -120,6 +129,15 @@ export class TestServer {
for (const subscriber of this._requestSubscribers.values()) for (const subscriber of this._requestSubscribers.values())
subscriber[rejectSymbol].call(null, error); subscriber[rejectSymbol].call(null, error);
this._requestSubscribers.clear(); this._requestSubscribers.clear();
this.setContent('/favicon.ico', '', 'image/x-icon');
this.setContent('/', ``, 'text/html');
this.setContent('/hello-world', `
<title>Title</title>
<body>Hello, world!</body>
`, 'text/html');
} }
_onRequest(request: http.IncomingMessage, response: http.ServerResponse) { _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
@ -144,7 +162,11 @@ export class TestServer {
this._requestSubscribers.delete(path); this._requestSubscribers.delete(path);
} }
const handler = this._routes.get(path); const handler = this._routes.get(path);
if (handler) if (handler) {
handler.call(null, request, response); handler.call(null, request, response);
} else {
response.writeHead(404);
response.end();
}
} }
} }