chore: use mcp implementation in Playwright (#992)

This commit is contained in:
Pavel Feldman
2025-09-04 06:40:19 -07:00
committed by GitHub
parent d142f13d80
commit c58b2a93da
124 changed files with 49 additions and 17534 deletions

View File

@@ -1,93 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { test, expect } from './fixtures';
test('cdp server', async ({ cdpServer, startClient, server }) => {
await cdpServer.start();
const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
});
test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
const browserContext = await cdpServer.start();
const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
const [page] = browserContext.pages();
await page.goto(server.HELLO_WORLD);
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Hello, world!',
ref: 'f0',
},
})).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',
})).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 }) => {
const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
server.setContent('/', `
<title>Title</title>
<body>Hello, world!</body>
`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).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 },
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
});
test('does not support --device', async () => {
const result = spawnSync('node', [
path.join(__filename, '../../cli.js'), '--device=Pixel 5', '--cdp-endpoint=http://localhost:1234',
]);
expect(result.error).toBeUndefined();
expect(result.status).toBe(1);
expect(result.stderr.toString()).toContain('Device emulation is not supported with cdpEndpoint.');
});

View File

@@ -38,62 +38,3 @@ test('browser_click', async ({ client, server, mcpBrowser }) => {
pageState: expect.stringContaining(`- button "Submit" ${mcpBrowser !== 'webkit' || process.platform === 'linux' ? '[active] ' : ''}[ref=e2]`),
});
});
test('browser_click (double)', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<script>
function handle() {
document.querySelector('h1').textContent = 'Double clicked';
}
</script>
<h1 ondblclick="handle()">Click me</h1>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Click me',
ref: 'e2',
doubleClick: true,
},
})).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 }) => {
server.setContent('/', `
<button oncontextmenu="handle">Menu</button>
<script>
document.addEventListener('contextmenu', event => {
event.preventDefault();
document.querySelector('button').textContent = 'Right clicked';
});
</script>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
const result = await client.callTool({
name: 'browser_click',
arguments: {
element: 'Menu',
ref: 'e2',
button: 'right',
},
});
expect(result).toHaveResponse({
code: `await page.getByRole('button', { name: 'Menu' }).click({ button: 'right' });`,
pageState: expect.stringContaining(`- button "Right clicked"`),
});
});

View File

@@ -1,80 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'node:fs';
import { Config } from '../config';
import { test, expect } from './fixtures';
import { configFromCLIOptions } from '../lib/browser/config';
test('config user data dir', async ({ startClient, server, mcpMode }, testInfo) => {
server.setContent('/', `
<title>Title</title>
<body>Hello, world!</body>
`, 'text/html');
const config: Config = {
browser: {
userDataDir: testInfo.outputPath('user-data-dir'),
},
};
const configPath = testInfo.outputPath('config.json');
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
const { client } = await startClient({ args: ['--config', configPath] });
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Hello, world!`),
});
const files = await fs.promises.readdir(config.browser!.userDataDir!);
expect(files.length).toBeGreaterThan(0);
});
test.describe(() => {
test.use({ mcpBrowser: '' });
test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient, mcpMode }, testInfo) => {
const config: Config = {
browser: {
browserName: 'firefox',
},
};
const configPath = testInfo.outputPath('config.json');
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
const { client } = await startClient({ args: ['--config', configPath] });
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: 'data:text/html,<script>document.title = navigator.userAgent</script>' },
})).toHaveResponse({
pageState: expect.stringContaining(`Firefox`),
});
});
});
test.describe('sandbox configuration', () => {
test('should enable sandbox by default (no --no-sandbox flag)', async () => {
const config = configFromCLIOptions({ sandbox: undefined });
expect(config.browser?.launchOptions?.chromiumSandbox).toBeUndefined();
});
test('should disable sandbox when --no-sandbox flag is passed', async () => {
const config = configFromCLIOptions({ sandbox: false });
expect(config.browser?.launchOptions?.chromiumSandbox).toBe(false);
});
});

View File

@@ -1,100 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
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({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
});
const resource = await client.callTool({
name: 'browser_console_messages',
});
expect(resource).toHaveResponse({
result: `[LOG] Hello, world! @ ${server.PREFIX}:4
[ERROR] Error @ ${server.PREFIX}:5`,
});
});
test('browser_console_messages (page error)', async ({ client, server }) => {
server.setContent('/', `
<!DOCTYPE html>
<html>
<script>
throw new Error("Error in script");
</script>
</html>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
});
const resource = await client.callTool({
name: 'browser_console_messages',
});
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 }) => {
server.setContent('/', `
<!DOCTYPE html>
<html>
<button onclick="console.log('Hello, world!');">Click me</button>
</html>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
});
const response = await client.callTool({
name: 'browser_click',
arguments: {
element: 'Click me',
ref: 'e2',
},
});
expect(response).toHaveResponse({
consoleMessages: expect.stringContaining(`- [LOG] Hello, world! @`),
});
});

View File

@@ -30,161 +30,3 @@ test('browser_navigate', async ({ client, server }) => {
\`\`\``,
});
});
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_select_option',
arguments: {
element: 'Select',
ref: 'e2',
values: ['bar'],
},
})).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 }) => {
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_select_option',
arguments: {
element: 'Select',
ref: 'e2',
values: ['bar', 'baz'],
},
})).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]`),
});
});
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
const response = await client.callTool({
name: 'browser_resize',
arguments: {
width: 390,
height: 780,
},
});
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 }) => {
server.setContent('/', `
<button>Button 1</button>
<button>Button 2</button>
<script>
document.querySelector('button').addEventListener('click', () => {
document.querySelectorAll('button')[1].remove();
});
</script>
`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
})).toHaveResponse({
pageState: expect.stringContaining(`
- button "Button 1" [ref=e2]
- button "Button 2" [ref=e3]`),
});
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button 1',
ref: 'e2',
},
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button 2',
ref: 'e3',
},
})).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 }) => {
server.setContent('/', `
<div style="visibility: hidden;">
<div style="visibility: visible;">
<button>Button</button>
</div>
</div>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_snapshot'
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button"`),
});
});

View File

@@ -1,45 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('--device should work', async ({ startClient, server, mcpMode }) => {
const { client } = await startClient({
args: ['--device', 'iPhone 15'],
});
server.route('/', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body></body>
<script>
document.body.textContent = window.innerWidth + "x" + window.innerHeight;
</script>
`);
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
})).toHaveResponse({
pageState: expect.stringContaining(`393x659`),
});
});

View File

@@ -1,255 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('alert dialog', async ({ client, server }) => {
server.setContent('/', `<button onclick="alert('Alert')">Button</button>`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button',
ref: 'e2',
},
})).toHaveResponse({
code: `await page.getByRole('button', { name: 'Button' }).click();`,
modalState: `- ["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`,
});
expect(await client.callTool({
name: 'browser_handle_dialog',
arguments: {
accept: true,
},
})).toHaveResponse({
modalState: undefined,
pageState: expect.stringContaining(`- button "Button"`),
});
});
test('two alert dialogs', async ({ client, server }) => {
server.setContent('/', `
<title>Title</title>
<body>
<button onclick="alert('Alert 1');alert('Alert 2');">Button</button>
</body>
`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button',
ref: 'e2',
},
})).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',
arguments: {
accept: true,
},
});
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',
arguments: {
accept: true,
},
});
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 }) => {
server.setContent('/', `
<title>Title</title>
<body>
<button onclick="document.body.textContent = confirm('Confirm')">Button</button>
</body>
`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button',
ref: 'e2',
},
})).toHaveResponse({
modalState: expect.stringContaining(`- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`),
});
expect(await client.callTool({
name: 'browser_handle_dialog',
arguments: {
accept: true,
},
})).toHaveResponse({
modalState: undefined,
pageState: expect.stringContaining(`- generic [active] [ref=e1]: "true"`),
});
});
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button',
ref: 'e2',
},
})).toHaveResponse({
modalState: expect.stringContaining(`- ["confirm" dialog with message "Confirm"]: can be handled by the "browser_handle_dialog" tool`),
});
expect(await client.callTool({
name: 'browser_handle_dialog',
arguments: {
accept: false,
},
})).toHaveResponse({
modalState: undefined,
pageState: expect.stringContaining(`- generic [active] [ref=e1]: "false"`),
});
});
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({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button',
ref: 'e2',
},
})).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',
arguments: {
accept: true,
promptText: 'Answer',
},
});
expect(result).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Answer`),
});
});
test('alert dialog w/ race', async ({ client, server }) => {
server.setContent('/', `<button onclick="setTimeout(() => alert('Alert'), 100)">Button</button>`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`- button "Button" [ref=e2]`),
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button',
ref: 'e2',
},
})).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',
arguments: {
accept: true,
},
});
expect(result).toHaveResponse({
modalState: undefined,
pageState: expect.stringContaining(`- Page URL: ${server.PREFIX}
- Page Title:
- Page Snapshot:
\`\`\`yaml
- button "Button"`),
});
});

View File

@@ -1,99 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('browser_evaluate', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
pageState: expect.stringContaining(`- Page Title: Title`),
});
expect(await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => document.title',
},
})).toHaveResponse({
result: `"Title"`,
code: `await page.evaluate('() => document.title');`,
});
});
test('browser_evaluate (element)', async ({ client, server }) => {
server.setContent('/', `
<body style="background-color: red">Hello, world!</body>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_evaluate',
arguments: {
function: 'element => element.style.backgroundColor',
element: 'body',
ref: 'e1',
},
})).toHaveResponse({
result: `"red"`,
code: `await page.getByText('Hello, world!').evaluate('element => element.style.backgroundColor');`,
});
});
test('browser_evaluate object', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
pageState: expect.stringContaining(`- Page Title: Title`),
});
expect(await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => ({ title: document.title, url: document.URL })',
},
})).toHaveResponse({
result: JSON.stringify({ title: 'Title', url: server.HELLO_WORLD }, null, 2),
code: `await page.evaluate('() => ({ title: document.title, url: document.URL })');`,
});
});
test('browser_evaluate (error)', async ({ client, server }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
pageState: expect.stringContaining(`- Page Title: Title`),
});
const result = await client.callTool({
name: 'browser_evaluate',
arguments: {
function: '() => nonExistentVariable',
},
});
expect(result.isError).toBe(true);
expect(result.content?.[0]?.text).toContain('nonExistentVariable');
// Check for common error patterns across browsers
const errorText = result.content?.[0]?.text || '';
expect(errorText).toMatch(/not defined|Can't find variable/);
});

View File

@@ -1,151 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs/promises';
import { test, expect } from './fixtures';
test('browser_file_upload', async ({ client, server }, testInfo) => {
server.setContent('/', `
<input type="file" />
<button>Button</button>
`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]:
- button "Choose File" [ref=e2]
- button "Button" [ref=e3]`),
});
{
expect(await client.callTool({
name: 'browser_file_upload',
arguments: { paths: [] },
})).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({
name: 'browser_click',
arguments: {
element: 'Textbox',
ref: 'e2',
},
})).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!');
{
const response = await client.callTool({
name: 'browser_file_upload',
arguments: {
paths: [filePath],
},
});
expect(response).toHaveResponse({
code: expect.stringContaining(`await fileChooser.setFiles(`),
modalState: undefined,
});
}
{
const response = await client.callTool({
name: 'browser_click',
arguments: {
element: 'Textbox',
ref: 'e2',
},
});
expect(response).toHaveResponse({
modalState: `- [File chooser]: can be handled by the "browser_file_upload" tool`,
});
}
{
const response = await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button',
ref: 'e3',
},
});
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`),
});
}
});
test('clicking on download link emits download', async ({ startClient, server, mcpMode }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
server.setContent('/', `<a href="/download" download="test.txt">Download</a>`, 'text/html');
server.setContent('/download', 'Data', 'text/plain');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`- link "Download" [ref=e2]`),
});
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Download link',
ref: 'e2',
},
});
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) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
test.skip(mcpBrowser !== 'chromium', 'This test is racy');
server.route('/download', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Disposition': 'attachment; filename=test.txt',
});
res.end('Hello world!');
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX + 'download',
},
})).toHaveResponse({
downloads: expect.stringContaining(`- Downloaded file test.txt to`),
});
});

View File

@@ -1,123 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('browser_fill_form (textbox)', async ({ client, server }) => {
server.setContent('/', `
<!DOCTYPE html>
<html>
<body>
<form>
<label>
<input type="text" id="name" name="name" />
Name
</label>
<label>
<input type="email" id="email" name="email" />
Email
</label>
<label>
<input type="range" id="age" name="age" min="18" max="100" />
Age
</label>
<label>
<select id="country" name="country">
<option value="">Choose a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>
Country
</label>
<label>
<input type="checkbox" name="subscribe" value="newsletter" />
Subscribe to newsletter
</label>
</form>
</body>
</html>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_fill_form',
arguments: {
fields: [
{
name: 'Name textbox',
type: 'textbox',
ref: 'e4',
value: 'John Doe'
},
{
name: 'Email textbox',
type: 'textbox',
ref: 'e6',
value: 'john.doe@example.com'
},
{
name: 'Age textbox',
type: 'slider',
ref: 'e8',
value: '25'
},
{
name: 'Country select',
type: 'combobox',
ref: 'e10',
value: 'United States'
},
{
name: 'Subscribe checkbox',
type: 'checkbox',
ref: 'e12',
value: 'true'
},
]
},
})).toHaveResponse({
code: `await page.getByRole('textbox', { name: 'Name' }).fill('John Doe');
await page.getByRole('textbox', { name: 'Email' }).fill('john.doe@example.com');
await page.getByRole('slider', { name: 'Age' }).fill('25');
await page.getByLabel('Choose a country United').selectOption('United States');
await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked('true');`,
});
const response = await client.callTool({
name: 'browser_snapshot',
arguments: {
},
});
expect.soft(response).toHaveResponse({
pageState: expect.stringMatching(/textbox "Name".*John Doe/),
});
expect.soft(response).toHaveResponse({
pageState: expect.stringMatching(/textbox "Email".*john.doe@example.com/),
});
expect.soft(response).toHaveResponse({
pageState: expect.stringMatching(/slider "Age".*"25"/),
});
expect.soft(response).toHaveResponse({
pageState: expect.stringContaining('option \"United States\" [selected]'),
});
expect.soft(response).toHaveResponse({
pageState: expect.stringContaining('checkbox \"Subscribe to newsletter\" [checked]'),
});
});

View File

@@ -1,49 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
for (const mcpHeadless of [false, true]) {
test.describe(`mcpHeadless: ${mcpHeadless}`, () => {
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) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<body></body>
<script>
document.body.textContent = navigator.userAgent;
</script>
`);
});
const response = await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
});
expect(response).toHaveResponse({
pageState: (mcpHeadless ? expect : expect.not).stringContaining(`HeadlessChrome`),
});
});
});
}

View File

@@ -1,255 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { ChildProcess, spawn } from 'child_process';
import path from 'path';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { test as baseTest, expect } from './fixtures';
import type { Config } from '../config.d.ts';
const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({
serverEndpoint: async ({ mcpHeadless }, use, testInfo) => {
let cp: ChildProcess | undefined;
const userDataDir = testInfo.outputPath('user-data-dir');
await use(async (options?: { args?: string[], noPort?: boolean }) => {
if (cp)
throw new Error('Process already running');
cp = spawn('node', [
path.join(path.dirname(__filename), '../cli.js'),
...(options?.noPort ? [] : ['--port=0']),
'--user-data-dir=' + userDataDir,
...(mcpHeadless ? ['--headless'] : []),
...(options?.args || []),
], {
stdio: 'pipe',
env: {
...process.env,
DEBUG: 'pw:mcp:test',
DEBUG_COLORS: '0',
DEBUG_HIDE_DATE: '1',
},
});
let stderr = '';
const url = await new Promise<string>(resolve => cp!.stderr?.on('data', data => {
stderr += data.toString();
const match = stderr.match(/Listening on (http:\/\/.*)/);
if (match)
resolve(match[1]);
}));
return { url: new URL(url), stderr: () => stderr };
});
cp?.kill('SIGTERM');
},
});
test('http transport', async ({ serverEndpoint }) => {
const { url } = await serverEndpoint();
const transport = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(transport);
await client.ping();
});
test('http transport (config)', async ({ serverEndpoint }) => {
const config: Config = {
server: {
port: 0,
}
};
const configFile = test.info().outputPath('config.json');
await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2));
const { url } = await serverEndpoint({ noPort: true, args: ['--config=' + configFile] });
const transport = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(transport);
await client.ping();
});
test('http transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint({ args: ['--isolated'] });
const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client1 = new Client({ name: 'test', version: '1.0.0' });
await client1.connect(transport1);
await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
/**
* src/client/streamableHttp.ts
* Clients that no longer need a particular session
* (e.g., because the user is leaving the client application) SHOULD send an
* HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly
* terminate the session.
*/
await transport1.terminateSession();
await client1.close();
const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client2 = new Client({ name: 'test', version: '1.0.0' });
await client2.connect(transport2);
await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await transport2.terminateSession();
await client2.close();
await expect(async () => {
const lines = stderr().split('\n');
expect(lines.filter(line => line.match(/create http session/)).length).toBe(2);
expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2);
expect(lines.filter(line => line.match(/create context/)).length).toBe(2);
expect(lines.filter(line => line.match(/close context/)).length).toBe(2);
expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(2);
}).toPass();
});
test('http transport browser lifecycle (isolated, multiclient)', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint({ args: ['--isolated'] });
const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client1 = new Client({ name: 'test', version: '1.0.0' });
await client1.connect(transport1);
await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client2 = new Client({ name: 'test', version: '1.0.0' });
await client2.connect(transport2);
await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await transport1.terminateSession();
await client1.close();
const transport3 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client3 = new Client({ name: 'test', version: '1.0.0' });
await client3.connect(transport3);
await client3.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await transport2.terminateSession();
await client2.close();
await transport3.terminateSession();
await client3.close();
await expect(async () => {
const lines = stderr().split('\n');
expect(lines.filter(line => line.match(/create http session/)).length).toBe(3);
expect(lines.filter(line => line.match(/delete http session/)).length).toBe(3);
expect(lines.filter(line => line.match(/create context/)).length).toBe(3);
expect(lines.filter(line => line.match(/close context/)).length).toBe(3);
expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(3);
expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(3);
expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(1);
expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(1);
}).toPass();
});
test('http transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint();
const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client1 = new Client({ name: 'test', version: '1.0.0' });
await client1.connect(transport1);
await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await transport1.terminateSession();
await client1.close();
const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client2 = new Client({ name: 'test', version: '1.0.0' });
await client2.connect(transport2);
await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await transport2.terminateSession();
await client2.close();
await expect(async () => {
const lines = stderr().split('\n');
expect(lines.filter(line => line.match(/create http session/)).length).toBe(2);
expect(lines.filter(line => line.match(/delete http session/)).length).toBe(2);
expect(lines.filter(line => line.match(/create context/)).length).toBe(2);
expect(lines.filter(line => line.match(/close context/)).length).toBe(2);
expect(lines.filter(line => line.match(/create browser context \(persistent\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/close browser context \(persistent\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/lock user data dir/)).length).toBe(2);
expect(lines.filter(line => line.match(/release user data dir/)).length).toBe(2);
}).toPass();
});
test('http transport browser lifecycle (persistent, multiclient)', async ({ serverEndpoint, server }) => {
const { url } = await serverEndpoint();
const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client1 = new Client({ name: 'test', version: '1.0.0' });
await client1.connect(transport1);
await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url));
const client2 = new Client({ name: 'test', version: '1.0.0' });
await client2.connect(transport2);
const response = await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(response.isError).toBe(true);
expect(response.content?.[0].text).toContain('use --isolated to run multiple instances of the same browser');
await client1.close();
await client2.close();
});
test('http transport (default)', async ({ serverEndpoint }) => {
const { url } = await serverEndpoint();
const transport = new StreamableHTTPClientTransport(url);
const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(transport);
await client.ping();
expect(transport.sessionId, 'has session support').toBeDefined();
});

View File

@@ -1,46 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('stitched aria frames', async ({ client }) => {
expect(await client.callTool({
name: 'browser_navigate',
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>`,
},
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]:
- heading "Hello" [level=1] [ref=e2]
- iframe [ref=e3]:
- generic [active] [ref=f1e1]:
- button "World" [ref=f1e2]
- main [ref=f1e3]:
- iframe [ref=f1e4]:
- paragraph [ref=f2e2]: Nested
\`\`\``),
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'World',
ref: 'f1e2',
},
})).toHaveResponse({
code: `await page.locator('iframe').first().contentFrame().getByRole('button', { name: 'World' }).click();`,
});
});

View File

@@ -1,26 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('browser_install', async ({ client, mcpBrowser }) => {
test.skip(mcpBrowser !== 'chromium', 'Test only chromium');
expect(await client.callTool({
name: 'browser_install',
})).toHaveResponse({
tabs: expect.stringContaining(`No open tabs`),
});
});

View File

@@ -1,169 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect, formatOutput } from './fixtures';
test('test reopen browser', async ({ startClient, server, mcpMode }) => {
const { client, stderr } = await startClient();
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(await client.callTool({
name: 'browser_close',
})).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 },
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
await client.close();
if (process.platform === 'win32')
return;
await expect.poll(() => formatOutput(stderr()), { timeout: 0 }).toEqual([
'create context',
'create browser context (persistent)',
'lock user data dir',
'close context',
'close browser context (persistent)',
'release user data dir',
'close browser context complete (persistent)',
'create browser context (persistent)',
'lock user data dir',
'close context',
'close browser context (persistent)',
'release user data dir',
'close browser context complete (persistent)',
]);
});
test('executable path', async ({ startClient, server }) => {
const { client } = await startClient({ args: [`--executable-path=bogus`] });
const response = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(response).toHaveResponse({
result: expect.stringContaining(`executable doesn't exist`),
isError: true,
});
});
test('persistent context', async ({ startClient, server }) => {
server.setContent('/', `
<body>
</body>
<script>
document.body.textContent = localStorage.getItem('test') ? 'Storage: YES' : 'Storage: NO';
localStorage.setItem('test', 'test');
</script>
`, 'text/html');
const { client } = await startClient();
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: NO`),
});
await new Promise(resolve => setTimeout(resolve, 3000));
await client.callTool({
name: 'browser_close',
});
const { client: client2 } = await startClient();
expect(await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: YES`),
});
});
test('isolated context', async ({ startClient, server }) => {
server.setContent('/', `
<body>
</body>
<script>
document.body.textContent = localStorage.getItem('test') ? 'Storage: YES' : 'Storage: NO';
localStorage.setItem('test', 'test');
</script>
`, 'text/html');
const { client: client1 } = await startClient({ args: [`--isolated`] });
expect(await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: NO`),
});
await client1.callTool({
name: 'browser_close',
});
const { client: client2 } = await startClient({ args: [`--isolated`] });
expect(await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: NO`),
});
});
test('isolated context with storage state', async ({ startClient, server }, testInfo) => {
const storageStatePath = testInfo.outputPath('storage-state.json');
await fs.promises.writeFile(storageStatePath, JSON.stringify({
origins: [
{
origin: server.PREFIX,
localStorage: [{ name: 'test', value: 'session-value' }],
},
],
}));
server.setContent('/', `
<body>
</body>
<script>
document.body.textContent = 'Storage: ' + localStorage.getItem('test');
</script>
`, 'text/html');
const { client } = await startClient({ args: [
`--isolated`,
`--storage-state=${storageStatePath}`,
] });
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
pageState: expect.stringContaining(`Storage: session-value`),
});
});

View File

@@ -1,217 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { runMainBackend, runOnPauseBackendLoop } from '../src/sdk/mdb';
import { test, expect } from './fixtures';
import type * as mcpServer from '../src/sdk/server';
import type { ServerBackendOnPause } from '../src/sdk/mdb';
test('call top level tool', async () => {
const { mdbUrl } = await startMDBAndCLI();
const mdbClient = await createMDBClient(mdbUrl);
const { tools } = await mdbClient.client.listTools();
expect(tools).toEqual([{
name: 'cli_echo',
description: 'Echo a message',
inputSchema: expect.any(Object),
}, {
name: 'cli_pause_in_gdb',
description: 'Pause in gdb',
inputSchema: expect.any(Object),
}, {
name: 'cli_pause_in_gdb_twice',
description: 'Pause in gdb twice',
inputSchema: expect.any(Object),
}
]);
const echoResult = await mdbClient.client.callTool({
name: 'cli_echo',
arguments: {
message: 'Hello, world!',
},
});
expect(echoResult.content).toEqual([{ type: 'text', text: 'Echo: Hello, world!' }]);
await mdbClient.close();
});
test('pause on error', async () => {
const { mdbUrl } = await startMDBAndCLI();
const mdbClient = await createMDBClient(mdbUrl);
// Make a call that results in a recoverable error.
const interruptResult = await mdbClient.client.callTool({
name: 'cli_pause_in_gdb',
arguments: {},
});
expect(interruptResult.content).toEqual([{ type: 'text', text: 'Paused on exception' }]);
// List new inner tools.
const { tools } = await mdbClient.client.listTools();
expect(tools).toEqual([
expect.objectContaining({
name: 'gdb_bt',
}),
expect.objectContaining({
name: 'gdb_continue',
}),
]);
// Call the new inner tool.
const btResult = await mdbClient.client.callTool({
name: 'gdb_bt',
arguments: {},
});
expect(btResult.content).toEqual([{ type: 'text', text: 'Backtrace' }]);
// Continue execution.
const continueResult = await mdbClient.client.callTool({
name: 'gdb_continue',
arguments: {},
});
expect(continueResult.content).toEqual([{ type: 'text', text: 'Done' }]);
await mdbClient.close();
});
test('pause on error twice', async () => {
const { mdbUrl } = await startMDBAndCLI();
const mdbClient = await createMDBClient(mdbUrl);
// Make a call that results in a recoverable error.
const result = await mdbClient.client.callTool({
name: 'cli_pause_in_gdb_twice',
arguments: {},
});
expect(result.content).toEqual([{ type: 'text', text: 'Paused on exception 1' }]);
// Continue execution.
const continueResult1 = await mdbClient.client.callTool({
name: 'gdb_continue',
arguments: {},
});
expect(continueResult1.content).toEqual([{ type: 'text', text: 'Paused on exception 2' }]);
const continueResult2 = await mdbClient.client.callTool({
name: 'gdb_continue',
arguments: {},
});
expect(continueResult2.content).toEqual([{ type: 'text', text: 'Done' }]);
await mdbClient.close();
});
async function startMDBAndCLI(): Promise<{ mdbUrl: string }> {
const mdbUrlBox = { mdbUrl: undefined as string | undefined };
const cliBackendFactory = {
name: 'CLI',
nameInConfig: 'cli',
version: '0.0.0',
create: () => new CLIBackend(mdbUrlBox)
};
const mdbUrl = (await runMainBackend(cliBackendFactory, { port: 0 }))!;
mdbUrlBox.mdbUrl = mdbUrl;
return { mdbUrl };
}
async function createMDBClient(mdbUrl: string): Promise<{ client: Client, close: () => Promise<void> }> {
const client = new Client({ name: 'Internal client', version: '0.0.0' });
const transport = new StreamableHTTPClientTransport(new URL(mdbUrl));
await client.connect(transport);
return {
client,
close: async () => {
await transport.terminateSession();
await client.close();
}
};
}
class CLIBackend implements mcpServer.ServerBackend {
constructor(private readonly mdbUrlBox: { mdbUrl: string | undefined }) {}
async listTools(): Promise<mcpServer.Tool[]> {
return [{
name: 'cli_echo',
description: 'Echo a message',
inputSchema: zodToJsonSchema(z.object({ message: z.string() })) as any,
}, {
name: 'cli_pause_in_gdb',
description: 'Pause in gdb',
inputSchema: zodToJsonSchema(z.object({})) as any,
}, {
name: 'cli_pause_in_gdb_twice',
description: 'Pause in gdb twice',
inputSchema: zodToJsonSchema(z.object({})) as any,
}];
}
async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise<mcpServer.CallToolResult> {
if (name === 'cli_echo')
return { content: [{ type: 'text', text: 'Echo: ' + (args?.message as string) }] };
if (name === 'cli_pause_in_gdb') {
await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception');
return { content: [{ type: 'text', text: 'Done' }] };
}
if (name === 'cli_pause_in_gdb_twice') {
await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception 1');
await runOnPauseBackendLoop(this.mdbUrlBox.mdbUrl!, new GDBBackend(), 'Paused on exception 2');
return { content: [{ type: 'text', text: 'Done' }] };
}
throw new Error(`Unknown tool: ${name}`);
}
}
class GDBBackend implements ServerBackendOnPause {
private _server!: mcpServer.Server;
async initialize(server: mcpServer.Server): Promise<void> {
this._server = server;
}
async listTools(): Promise<mcpServer.Tool[]> {
return [{
name: 'gdb_bt',
description: 'Print backtrace',
inputSchema: zodToJsonSchema(z.object({})) as any,
}, {
name: 'gdb_continue',
description: 'Continue execution',
inputSchema: zodToJsonSchema(z.object({})) as any,
}];
}
async callTool(name: string, args: mcpServer.CallToolRequest['params']['arguments']): Promise<mcpServer.CallToolResult> {
if (name === 'gdb_bt')
return { content: [{ type: 'text', text: 'Backtrace' }] };
if (name === 'gdb_continue') {
(this as ServerBackendOnPause).requestSelfDestruct?.();
// Stall
await new Promise(f => setTimeout(f, 1000));
}
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@@ -1,47 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('browser_network_requests', async ({ client, server }) => {
server.setContent('/', `
<button onclick="fetch('/json')">Click me</button>
`, 'text/html');
server.setContent('/json', JSON.stringify({ name: 'John Doe' }), 'application/json');
await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
});
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Click me button',
ref: 'e2',
},
});
await expect.poll(() => client.callTool({
name: 'browser_network_requests',
})).toHaveResponse({
result: expect.stringContaining(`[GET] ${`${server.PREFIX}`} => [200] OK
[GET] ${`${server.PREFIX}json`} => [200] OK`),
});
});

View File

@@ -1,88 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './fixtures';
test('save as pdf unavailable', async ({ startClient, server }) => {
const { client } = await startClient();
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(await client.callTool({
name: 'browser_pdf_save',
})).toHaveResponse({
result: 'Error: Tool "browser_pdf_save" not found',
isError: true,
});
});
test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output'), capabilities: ['pdf'] },
});
test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).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/),
});
});
test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, server }, testInfo) => {
const outputDir = testInfo.outputPath('output');
test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
const { client } = await startClient({
config: { outputDir, capabilities: ['pdf'] },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
expect(await client.callTool({
name: 'browser_pdf_save',
arguments: {
filename: 'output.pdf',
},
})).toHaveResponse({
result: expect.stringContaining(`output.pdf`),
code: expect.stringContaining(`await page.pdf(`),
});
const files = [...fs.readdirSync(outputDir)];
expect(fs.existsSync(outputDir)).toBeTruthy();
const pdfFiles = files.filter(f => f.endsWith('.pdf'));
expect(pdfFiles).toHaveLength(1);
expect(pdfFiles[0]).toMatch(/^output.pdf$/);
});

View File

@@ -1,82 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { test, expect } from './fixtures.ts';
const BLOCK_MESSAGE = /Blocked by Web Inspector|NS_ERROR_FAILURE|net::ERR_BLOCKED_BY_CLIENT/g;
const fetchPage = async (client: Client, url: string) => {
const result = await client.callTool({
name: 'browser_navigate',
arguments: {
url,
},
});
return JSON.stringify(result, null, 2);
};
test('default to allow all', async ({ server, client }) => {
server.setContent('/ppp', 'content:PPP', 'text/html');
const result = await fetchPage(client, server.PREFIX + 'ppp');
expect(result).toContain('content:PPP');
});
test('blocked works', async ({ startClient }) => {
const { client } = await startClient({
args: ['--blocked-origins', 'microsoft.com;example.com;playwright.dev']
});
const result = await fetchPage(client, 'https://example.com/');
expect(result).toMatch(BLOCK_MESSAGE);
});
test('allowed works', async ({ server, startClient }) => {
server.setContent('/ppp', 'content:PPP', 'text/html');
const { client } = await startClient({
args: ['--allowed-origins', `microsoft.com;${new URL(server.PREFIX).host};playwright.dev`]
});
const result = await fetchPage(client, server.PREFIX + 'ppp');
expect(result).toContain('content:PPP');
});
test('blocked takes precedence', async ({ startClient }) => {
const { client } = await startClient({
args: [
'--blocked-origins', 'example.com',
'--allowed-origins', 'example.com',
],
});
const result = await fetchPage(client, 'https://example.com/');
expect(result).toMatch(BLOCK_MESSAGE);
});
test('allowed without blocked blocks all non-explicitly specified origins', async ({ startClient }) => {
const { client } = await startClient({
args: ['--allowed-origins', 'playwright.dev'],
});
const result = await fetchPage(client, 'https://example.com/');
expect(result).toMatch(BLOCK_MESSAGE);
});
test('blocked without allowed allows non-explicitly specified origins', async ({ server, startClient }) => {
server.setContent('/ppp', 'content:PPP', 'text/html');
const { client } = await startClient({
args: ['--blocked-origins', 'example.com'],
});
const result = await fetchPage(client, server.PREFIX + 'ppp');
expect(result).toContain('content:PPP');
});

View File

@@ -1,83 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { pathToFileURL } from 'url';
import { test, expect } from './fixtures';
const p = process.platform === 'win32' ? 'c:\\non\\existent\\folder' : '/non/existent/folder';
test('should use separate user data by root path', async ({ startClient, server }, testInfo) => {
const { client } = await startClient({
clientName: 'Visual Studio Code',
roots: [
{
name: 'test',
uri: 'file://' + p.replace(/\\/g, '/'),
}
],
});
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
const hash = createHash(p);
const [file] = await fs.promises.readdir(testInfo.outputPath('ms-playwright'));
expect(file).toContain(hash);
});
test('check that trace is saved in workspace', async ({ startClient, server }, testInfo) => {
const rootPath = testInfo.outputPath('workspace');
const { client } = await startClient({
args: ['--save-trace'],
clientName: 'My client',
roots: [
{
name: 'workspace',
uri: pathToFileURL(rootPath).toString(),
},
],
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
const [file] = await fs.promises.readdir(path.join(rootPath, '.playwright-mcp'));
expect(file).toContain('traces');
});
test('should list all tools when listRoots is slow', async ({ startClient }) => {
const { client } = await startClient({
clientName: 'Another custom client',
roots: [],
rootsResponseDelay: 1000,
});
const tools = await client.listTools();
expect(tools.tools.length).toBeGreaterThan(10);
});
function createHash(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
}

View File

@@ -1,327 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './fixtures';
test('browser_take_screenshot (viewport)', async ({ startClient, server }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
})).toHaveResponse({
code: expect.stringContaining(`await page.screenshot`),
attachments: [{
data: expect.any(String),
mimeType: 'image/png',
type: 'image',
}],
});
});
test('browser_take_screenshot (element)', async ({ startClient, server }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
pageState: expect.stringContaining(`[ref=e1]`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
arguments: {
element: 'hello button',
ref: 'e1',
},
})).toEqual({
content: [
{
text: expect.stringContaining(`page.getByText('Hello, world!').screenshot`),
type: 'text',
},
{
data: expect.any(String),
mimeType: 'image/png',
type: 'image',
},
],
});
});
test('--output-dir should work', async ({ startClient, server }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const { client } = await startClient({
config: { outputDir },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
await client.callTool({
name: 'browser_take_screenshot',
});
expect(fs.existsSync(outputDir)).toBeTruthy();
const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png'));
expect(files).toHaveLength(1);
expect(files[0]).toMatch(/^page-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.png$/);
});
for (const type of ['png', 'jpeg']) {
test(`browser_take_screenshot (type: ${type})`, async ({ startClient, server }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const { client } = await startClient({
config: { outputDir },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
arguments: { type },
})).toEqual({
content: [
{
text: expect.stringMatching(
new RegExp(`page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\-\\d{3}Z\\.${type}`)
),
type: 'text',
},
{
data: expect.any(String),
mimeType: `image/${type}`,
type: 'image',
},
],
});
const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith(`.${type}`));
expect(fs.existsSync(outputDir)).toBeTruthy();
expect(files).toHaveLength(1);
expect(files[0]).toMatch(
new RegExp(`^page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${type}$`)
);
});
}
test('browser_take_screenshot (default type should be png)', async ({ startClient, server }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const { client } = await startClient({
config: { outputDir },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
code: `await page.goto('${server.PREFIX}');`,
});
expect(await client.callTool({
name: 'browser_take_screenshot',
})).toEqual({
content: [
{
text: expect.stringMatching(
new RegExp(`page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\-\\d{3}Z\\.png`)
),
type: 'text',
},
{
data: expect.any(String),
mimeType: 'image/png',
type: 'image',
},
],
});
const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png'));
expect(fs.existsSync(outputDir)).toBeTruthy();
expect(files).toHaveLength(1);
expect(files[0]).toMatch(/^page-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.png$/);
});
test('browser_take_screenshot (filename: "output.png")', async ({ startClient, server }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const { client } = await startClient({
config: { outputDir },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
arguments: {
filename: 'output.png',
},
})).toEqual({
content: [
{
text: expect.stringContaining(`output.png`),
type: 'text',
},
{
data: expect.any(String),
mimeType: 'image/png',
type: 'image',
},
],
});
const files = [...fs.readdirSync(outputDir)].filter(f => f.endsWith('.png'));
expect(fs.existsSync(outputDir)).toBeTruthy();
expect(files).toHaveLength(1);
expect(files[0]).toMatch(/^output\.png$/);
});
test('browser_take_screenshot (imageResponses=omit)', async ({ startClient, server }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const { client } = await startClient({
config: {
outputDir,
imageResponses: 'omit',
},
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
await client.callTool({
name: 'browser_take_screenshot',
});
expect(await client.callTool({
name: 'browser_take_screenshot',
})).toEqual({
content: [
{
text: expect.stringContaining(`await page.screenshot`),
type: 'text',
},
],
});
});
test('browser_take_screenshot (fullPage: true)', async ({ startClient, server }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
expect(await client.callTool({
name: 'browser_take_screenshot',
arguments: { fullPage: true },
})).toEqual({
content: [
{
text: expect.stringContaining('fullPage: true'),
type: 'text',
}
],
});
});
test('browser_take_screenshot (fullPage with element should error)', async ({ startClient, server }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
pageState: expect.stringContaining(`[ref=e1]`),
});
const result = await client.callTool({
name: 'browser_take_screenshot',
arguments: {
fullPage: true,
element: 'hello button',
ref: 'e1',
},
});
expect(result.isError).toBe(true);
expect(result.content?.[0]?.text).toContain('fullPage cannot be used with element screenshots');
});
test('browser_take_screenshot (viewport without snapshot)', async ({ startClient, server }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
// Ensure we have a tab but don't navigate anywhere (no snapshot captured)
expect(await client.callTool({
name: 'browser_tabs',
arguments: {
action: 'list',
},
})).toHaveResponse({
tabs: `- 0: (current) [] (about:blank)`,
});
// This should work without requiring a snapshot since it's a viewport screenshot
expect(await client.callTool({
name: 'browser_take_screenshot',
})).toEqual({
content: [
{
text: expect.stringContaining(`page.screenshot`),
type: 'text',
},
{
data: expect.any(String),
mimeType: 'image/png',
type: 'image',
},
],
});
});

View File

@@ -1,275 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import path from 'path';
import { test, expect } from './fixtures';
test('session log should record tool calls', async ({ startClient, server }, testInfo) => {
const { client, stderr } = await startClient({
args: [
'--save-session',
'--output-dir', testInfo.outputPath('output'),
],
});
server.setContent('/', `<title>Title</title><button>Submit</button>`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Submit button',
ref: 'e2',
},
})).toHaveResponse({
code: `await page.getByRole('button', { name: 'Submit' }).click();`,
pageState: expect.stringContaining(`- button "Submit"`),
});
const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0];
const sessionFolder = output.substring('Session: '.length);
await expect.poll(() => readSessionLog(sessionFolder)).toBe(`
### Tool call: browser_navigate
- Args
\`\`\`json
{
"url": "http://localhost:${server.PORT}/"
}
\`\`\`
- Code
\`\`\`js
await page.goto('http://localhost:${server.PORT}/');
\`\`\`
- Snapshot: 001.snapshot.yml
### Tool call: browser_click
- Args
\`\`\`json
{
"element": "Submit button",
"ref": "e2"
}
\`\`\`
- Code
\`\`\`js
await page.getByRole('button', { name: 'Submit' }).click();
\`\`\`
- Snapshot: 002.snapshot.yml
`);
});
test('session log should record user action', async ({ cdpServer, startClient }, testInfo) => {
const browserContext = await cdpServer.start();
const { client, stderr } = await startClient({
args: [
'--save-session',
'--output-dir', testInfo.outputPath('output'),
`--cdp-endpoint=${cdpServer.endpoint}`,
],
});
// Force browser context creation.
await client.callTool({
name: 'browser_snapshot',
});
const [page] = browserContext.pages();
await page.setContent(`
<button>Button 1</button>
<button>Button 2</button>
`);
await page.getByRole('button', { name: 'Button 1' }).click();
const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0];
const sessionFolder = output.substring('Session: '.length);
await expect.poll(() => readSessionLog(sessionFolder)).toBe(`
### Tool call: browser_snapshot
- Args
\`\`\`json
{}
\`\`\`
- Snapshot: 001.snapshot.yml
### User action: click
- Args
\`\`\`json
{
"name": "click",
"ref": "e2",
"button": "left",
"modifiers": 0,
"clickCount": 1
}
\`\`\`
- Code
\`\`\`js
await page.getByRole('button', { name: 'Button 1' }).click();
\`\`\`
- Snapshot: 002.snapshot.yml
`);
});
test('session log should update user action', async ({ cdpServer, startClient }, testInfo) => {
const browserContext = await cdpServer.start();
const { client, stderr } = await startClient({
args: [
'--save-session',
'--output-dir', testInfo.outputPath('output'),
`--cdp-endpoint=${cdpServer.endpoint}`,
],
});
// Force browser context creation.
await client.callTool({
name: 'browser_snapshot',
});
const [page] = browserContext.pages();
await page.setContent(`
<button>Button 1</button>
<button>Button 2</button>
`);
await page.getByRole('button', { name: 'Button 1' }).dblclick();
const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0];
const sessionFolder = output.substring('Session: '.length);
await expect.poll(() => readSessionLog(sessionFolder)).toBe(`
### Tool call: browser_snapshot
- Args
\`\`\`json
{}
\`\`\`
- Snapshot: 001.snapshot.yml
### User action: click
- Args
\`\`\`json
{
"name": "click",
"ref": "e2",
"button": "left",
"modifiers": 0,
"clickCount": 2
}
\`\`\`
- Code
\`\`\`js
await page.getByRole('button', { name: 'Button 1' }).dblclick();
\`\`\`
- Snapshot: 002.snapshot.yml
`);
});
test('session log should record tool calls and user actions', async ({ cdpServer, startClient }, testInfo) => {
const browserContext = await cdpServer.start();
const { client, stderr } = await startClient({
args: [
'--save-session',
'--output-dir', testInfo.outputPath('output'),
`--cdp-endpoint=${cdpServer.endpoint}`,
],
});
const [page] = browserContext.pages();
await page.setContent(`
<button>Button 1</button>
<button>Button 2</button>
`);
await client.callTool({
name: 'browser_snapshot',
});
// Manual action.
await page.getByRole('button', { name: 'Button 1' }).click();
// This is to simulate a delay after the user action before the tool action.
await new Promise(resolve => setTimeout(resolve, 1000));
// Tool action.
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button 2',
ref: 'e3',
},
});
const output = stderr().split('\n').filter(line => line.startsWith('Session: '))[0];
const sessionFolder = output.substring('Session: '.length);
await expect.poll(() => readSessionLog(sessionFolder)).toBe(`
### Tool call: browser_snapshot
- Args
\`\`\`json
{}
\`\`\`
- Snapshot: 001.snapshot.yml
### User action: click
- Args
\`\`\`json
{
"name": "click",
"ref": "e2",
"button": "left",
"modifiers": 0,
"clickCount": 1
}
\`\`\`
- Code
\`\`\`js
await page.getByRole('button', { name: 'Button 1' }).click();
\`\`\`
- Snapshot: 002.snapshot.yml
### Tool call: browser_click
- Args
\`\`\`json
{
"element": "Button 2",
"ref": "e3"
}
\`\`\`
- Code
\`\`\`js
await page.getByRole('button', { name: 'Button 2' }).click();
\`\`\`
- Snapshot: 003.snapshot.yml
`);
});
async function readSessionLog(sessionFolder: string): Promise<string> {
return await fs.promises.readFile(path.join(sessionFolder, 'session.md'), 'utf8').catch(() => '');
}

View File

@@ -1,232 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { ChildProcess, spawn } from 'child_process';
import path from 'path';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { test as baseTest, expect } from './fixtures';
import type { Config } from '../config.d.ts';
const test = baseTest.extend<{ serverEndpoint: (options?: { args?: string[], noPort?: boolean }) => Promise<{ url: URL, stderr: () => string }> }>({
serverEndpoint: async ({ mcpHeadless }, use, testInfo) => {
let cp: ChildProcess | undefined;
const userDataDir = testInfo.outputPath('user-data-dir');
await use(async (options?: { args?: string[], noPort?: boolean }) => {
if (cp)
throw new Error('Process already running');
cp = spawn('node', [
path.join(path.dirname(__filename), '../cli.js'),
...(options?.noPort ? [] : ['--port=0']),
'--user-data-dir=' + userDataDir,
...(mcpHeadless ? ['--headless'] : []),
...(options?.args || []),
], {
stdio: 'pipe',
env: {
...process.env,
DEBUG: 'pw:mcp:test',
DEBUG_COLORS: '0',
DEBUG_HIDE_DATE: '1',
},
});
let stderr = '';
const url = await new Promise<string>(resolve => cp!.stderr?.on('data', data => {
stderr += data.toString();
const match = stderr.match(/Listening on (http:\/\/.*)/);
if (match)
resolve(match[1]);
}));
return { url: new URL(url), stderr: () => stderr };
});
cp?.kill('SIGTERM');
},
});
test('sse transport', async ({ serverEndpoint }) => {
const { url } = await serverEndpoint();
const transport = new SSEClientTransport(new URL('/sse', url));
const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(transport);
await client.ping();
});
test('sse transport (config)', async ({ serverEndpoint }) => {
const config: Config = {
server: {
port: 0,
}
};
const configFile = test.info().outputPath('config.json');
await fs.promises.writeFile(configFile, JSON.stringify(config, null, 2));
const { url } = await serverEndpoint({ noPort: true, args: ['--config=' + configFile] });
const transport = new SSEClientTransport(new URL('/sse', url));
const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(transport);
await client.ping();
});
test('sse transport browser lifecycle (isolated)', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint({ args: ['--isolated'] });
const transport1 = new SSEClientTransport(new URL('/sse', url));
const client1 = new Client({ name: 'test', version: '1.0.0' });
await client1.connect(transport1);
await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await client1.close();
const transport2 = new SSEClientTransport(new URL('/sse', url));
const client2 = new Client({ name: 'test', version: '1.0.0' });
await client2.connect(transport2);
await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await client2.close();
await expect(async () => {
const lines = stderr().split('\n');
expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(2);
expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(2);
expect(lines.filter(line => line.match(/create context/)).length).toBe(2);
expect(lines.filter(line => line.match(/close context/)).length).toBe(2);
expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(2);
}).toPass();
});
test('sse transport browser lifecycle (isolated, multiclient)', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint({ args: ['--isolated'] });
const transport1 = new SSEClientTransport(new URL('/sse', url));
const client1 = new Client({ name: 'test', version: '1.0.0' });
await client1.connect(transport1);
await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
const transport2 = new SSEClientTransport(new URL('/sse', url));
const client2 = new Client({ name: 'test', version: '1.0.0' });
await client2.connect(transport2);
await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await client1.close();
const transport3 = new SSEClientTransport(new URL('/sse', url));
const client3 = new Client({ name: 'test', version: '1.0.0' });
await client3.connect(transport3);
await client3.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await client2.close();
await client3.close();
await expect(async () => {
const lines = stderr().split('\n');
expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(3);
expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(3);
expect(lines.filter(line => line.match(/create context/)).length).toBe(3);
expect(lines.filter(line => line.match(/close context/)).length).toBe(3);
expect(lines.filter(line => line.match(/create browser context \(isolated\)/)).length).toBe(3);
expect(lines.filter(line => line.match(/close browser context \(isolated\)/)).length).toBe(3);
expect(lines.filter(line => line.match(/obtain browser \(isolated\)/)).length).toBe(1);
expect(lines.filter(line => line.match(/close browser \(isolated\)/)).length).toBe(1);
}).toPass();
});
test('sse transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => {
const { url, stderr } = await serverEndpoint();
const transport1 = new SSEClientTransport(new URL('/sse', url));
const client1 = new Client({ name: 'test', version: '1.0.0' });
await client1.connect(transport1);
await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await client1.close();
const transport2 = new SSEClientTransport(new URL('/sse', url));
const client2 = new Client({ name: 'test', version: '1.0.0' });
await client2.connect(transport2);
await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
await client2.close();
await expect(async () => {
const lines = stderr().split('\n');
expect(lines.filter(line => line.match(/create SSE session/)).length).toBe(2);
expect(lines.filter(line => line.match(/delete SSE session/)).length).toBe(2);
expect(lines.filter(line => line.match(/create context/)).length).toBe(2);
expect(lines.filter(line => line.match(/close context/)).length).toBe(2);
expect(lines.filter(line => line.match(/create browser context \(persistent\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/close browser context \(persistent\)/)).length).toBe(2);
expect(lines.filter(line => line.match(/lock user data dir/)).length).toBe(2);
expect(lines.filter(line => line.match(/release user data dir/)).length).toBe(2);
}).toPass();
});
test('sse transport browser lifecycle (persistent, multiclient)', async ({ serverEndpoint, server }) => {
const { url } = await serverEndpoint();
const transport1 = new SSEClientTransport(new URL('/sse', url));
const client1 = new Client({ name: 'test', version: '1.0.0' });
await client1.connect(transport1);
await client1.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
const transport2 = new SSEClientTransport(new URL('/sse', url));
const client2 = new Client({ name: 'test', version: '1.0.0' });
await client2.connect(transport2);
const response = await client2.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(response.isError).toBe(true);
expect(response.content?.[0].text).toContain('use --isolated to run multiple instances of the same browser');
await client1.close();
await client2.close();
});

View File

@@ -1,155 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
async function createTab(client: Client, title: string, body: string) {
await client.callTool({
name: 'browser_tabs',
arguments: {
action: 'new',
},
});
return await client.callTool({
name: 'browser_navigate',
arguments: {
url: `data:text/html,<title>${title}</title><body>${body}</body>`,
},
});
}
test('list initial tabs', async ({ client }) => {
expect(await client.callTool({
name: 'browser_tabs',
arguments: {
action: 'list',
},
})).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_tabs',
arguments: {
action: 'list',
},
})).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 }) => {
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
\`\`\``),
});
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>)`,
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');
expect(await client.callTool({
name: 'browser_tabs',
arguments: {
action: 'select',
index: 1,
},
})).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>)`,
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
\`\`\``),
});
expect(await client.callTool({
name: 'browser_tabs',
arguments: {
action: 'select',
index: 0,
},
})).toHaveResponse({
tabs: `- 0: (current) [] (about:blank)
- 1: [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>)`,
pageState: expect.stringContaining(`- Page URL: about:blank`),
});
});
test('close tab', async ({ client }) => {
await createTab(client, 'Tab one', 'Body one');
await createTab(client, 'Tab two', 'Body two');
expect(await client.callTool({
name: 'browser_tabs',
arguments: {
action: 'close',
index: 2,
},
})).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 }) => {
const browserContext = await cdpServer.start();
const pages = browserContext.pages();
const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
});
expect(pages.length).toBe(1);
expect(await pages[0].title()).toBe('Title');
});

View File

@@ -1,37 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './fixtures';
test('check that trace is saved', async ({ startClient, server, mcpMode }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const { client } = await startClient({
args: ['--save-trace', `--output-dir=${outputDir}`],
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
code: expect.stringContaining(`page.goto('http://localhost`),
});
const [file] = await fs.promises.readdir(outputDir);
expect(file).toContain('traces');
});

View File

@@ -1,138 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
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({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
});
{
const response = await client.callTool({
name: 'browser_type',
arguments: {
element: 'textbox',
ref: 'e2',
text: 'Hi!',
submit: true,
},
});
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',
})).toHaveResponse({
result: expect.stringContaining(`[LOG] Key pressed: Enter , Text: Hi!`),
});
});
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({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
});
{
const response = await client.callTool({
name: 'browser_type',
arguments: {
element: 'textbox',
ref: 'e2',
text: 'Hi!',
slowly: true,
},
});
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).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 }) => {
server.setContent('/', `
<input type='text' oninput="console.log('New value: ' + event.target.value)"></input>
`, 'text/html');
{
const response = await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
});
expect(response).toHaveResponse({
pageState: expect.stringContaining(`- textbox`),
});
}
{
const response = await client.callTool({
name: 'browser_type',
arguments: {
element: 'textbox',
ref: 'e2',
text: 'Hi!',
},
});
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).toHaveResponse({
result: expect.stringContaining(`[LOG] New value: Hi!`),
});
}
});

View File

@@ -1,522 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test.use({ mcpArgs: ['--caps=verify'] });
test('browser_verify_element_visible', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<button>Submit</button>
<h1>Welcome</h1>
<div role="alert" aria-label="Success message"></div>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_element_visible',
arguments: {
role: 'button',
accessibleName: 'Submit',
},
})).toHaveResponse({
result: 'Done',
code: `await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();`,
});
expect(await client.callTool({
name: 'browser_verify_element_visible',
arguments: {
role: 'heading',
accessibleName: 'Welcome',
},
})).toHaveResponse({
result: 'Done',
code: `await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();`,
});
expect(await client.callTool({
name: 'browser_verify_element_visible',
arguments: {
role: 'alert',
accessibleName: 'Success message',
},
})).toHaveResponse({
result: 'Done',
code: `await expect(page.getByRole('alert', { name: 'Success message' })).toBeVisible();`,
});
});
test('browser_verify_element_visible (not found)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<button>Submit</button>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_element_visible',
arguments: {
role: 'button',
accessibleName: 'Cancel',
},
})).toHaveResponse({
isError: true,
result: 'Element with role "button" and accessible name "Cancel" not found',
});
});
test('browser_verify_text_visible', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<p>Hello world</p>
<div>Welcome to our site</div>
<span>Status: Active</span>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_text_visible',
arguments: {
text: 'Hello world',
},
})).toHaveResponse({
result: 'Done',
code: `await expect(page.getByText('Hello world')).toBeVisible();`,
});
expect(await client.callTool({
name: 'browser_verify_text_visible',
arguments: {
text: 'Welcome to our site',
},
})).toHaveResponse({
result: 'Done',
code: `await expect(page.getByText('Welcome to our site')).toBeVisible();`,
});
expect(await client.callTool({
name: 'browser_verify_text_visible',
arguments: {
text: 'Status: Active',
},
})).toHaveResponse({
result: 'Done',
code: `await expect(page.getByText('Status: Active')).toBeVisible();`,
});
});
test('browser_verify_text_visible (not found)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<p>Hello world</p>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_text_visible',
arguments: {
text: 'Goodbye world',
},
})).toHaveResponse({
isError: true,
result: 'Text not found',
});
});
test('browser_verify_text_visible (with quotes)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<p>She said "Hello world"</p>
<div>It's a beautiful day</div>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_text_visible',
arguments: {
text: 'She said "Hello world"',
},
})).toHaveResponse({
result: 'Done',
code: `await expect(page.getByText('She said "Hello world"')).toBeVisible();`,
});
expect(await client.callTool({
name: 'browser_verify_text_visible',
arguments: {
text: "It's a beautiful day",
},
})).toHaveResponse({
result: 'Done',
code: `await expect(page.getByText('It\\'s a beautiful day')).toBeVisible();`,
});
});
test('browser_verify_list_visible', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
</ul>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_list_visible',
arguments: {
element: 'Fruit list',
ref: 'e2',
items: ['Apple', 'Banana', 'Cherry'],
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.locator('body')).toMatchAriaSnapshot(\`
- list:
- listitem: "Apple"
- listitem: "Banana"
- listitem: "Cherry"
\`);`),
});
});
test('browser_verify_list_visible (partial items)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
<li>Date</li>
</ul>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_list_visible',
arguments: {
element: 'Fruit list',
ref: 'e2',
items: ['Apple', 'Cherry'],
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.locator('body')).toMatchAriaSnapshot(\`
- list:
- listitem: "Apple"
- listitem: "Cherry"
\`);`),
});
});
test('browser_verify_list_visible (item not found)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_list_visible',
arguments: {
element: 'Fruit list',
ref: 'e2',
items: ['Apple', 'Cherry'],
},
})).toHaveResponse({
isError: true,
result: 'Item "Cherry" not found',
});
});
test('browser_verify_value (textbox)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<form>
<input type="text" aria-label="Name" value="John Doe" />
<input type="email" aria-label="Email" value="john@example.com" />
</form>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'textbox',
element: 'Name textbox',
ref: 'e3',
value: 'John Doe',
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.getByRole('textbox', { name: 'Name' })).toHaveValue('John Doe');`),
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'textbox',
element: 'Email textbox',
ref: 'e4',
value: 'john@example.com',
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.getByRole('textbox', { name: 'Email' })).toHaveValue('john@example.com');`),
});
});
test('browser_verify_value (textbox wrong value)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<form>
<input type="text" name="name" value="John Doe" />
</form>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'textbox',
element: 'Name textbox',
ref: 'e3',
value: 'Jane Smith',
},
})).toHaveResponse({
isError: true,
result: 'Expected value "Jane Smith", but got "John Doe"',
});
});
test('browser_verify_value (checkbox checked)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<form>
<input type="checkbox" name="subscribe" checked />
<label for="subscribe">Subscribe to newsletter</label>
</form>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'checkbox',
element: 'Subscribe checkbox',
ref: 'e3',
value: 'true',
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.getByRole('checkbox')).toBeChecked();`),
});
});
test('browser_verify_value (checkbox unchecked)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<form>
<input type="checkbox" name="subscribe" />
<label for="subscribe">Subscribe to newsletter</label>
</form>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'checkbox',
element: 'Subscribe checkbox',
ref: 'e3',
value: 'false',
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.getByRole('checkbox')).not.toBeChecked();`),
});
});
test('browser_verify_value (checkbox wrong value)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<form>
<input type="checkbox" name="subscribe" checked />
<label for="subscribe">Subscribe to newsletter</label>
</form>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'checkbox',
element: 'Subscribe checkbox',
ref: 'e3',
value: 'false',
},
})).toHaveResponse({
isError: true,
result: 'Expected value "false", but got "true"',
});
});
test('browser_verify_value (radio checked)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<form>
<label for="red">Red</label>
<input id="red" type="radio" name="color" value="red" checked />
<label for="blue">Blue</label>
<input id="blue" type="radio" name="color" value="blue" />
</form>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'radio',
element: 'Color radio',
ref: 'e3',
value: 'true',
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.getByRole('radio', { name: 'Red' })).toBeChecked();`),
});
});
test('browser_verify_value (slider)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<form>
<input type="range" name="volume" min="0" max="100" value="75" />
<label>Volume</label>
</form>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'slider',
element: 'Volume slider',
ref: 'e3',
value: '75',
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.getByRole('slider')).toHaveValue('75');`),
});
});
test('browser_verify_value (combobox)', async ({ client, server }) => {
server.setContent('/', `
<title>Test Page</title>
<form>
<select name="country">
<option>Choose a country</option>
<option selected>United States</option>
<option>United Kingdom</option>
</select>
</form>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_verify_value',
arguments: {
type: 'combobox',
element: 'Country select',
ref: 'e3',
value: 'United States',
},
})).toHaveResponse({
result: 'Done',
code: expect.stringContaining(`await expect(page.getByRole('combobox')).toHaveValue('United States');`),
});
});

View File

@@ -1,119 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('browser_connect(vscode) works', async ({ startClient, playwright, browserName }) => {
const { client } = await startClient({
args: ['--vscode'],
});
const server = await playwright[browserName].launchServer();
expect(await client.callTool({
name: 'browser_connect',
arguments: {
connectionString: server.wsEndpoint(),
lib: require.resolve('playwright'),
}
})).toHaveResponse({
result: 'Successfully connected.'
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,foo'
}
})).toHaveResponse({
pageState: expect.stringContaining('foo'),
});
await server.close();
expect(await client.callTool({
name: 'browser_snapshot',
arguments: {}
}), 'it actually used the server').toHaveResponse({
isError: true,
result: expect.stringContaining('ECONNREFUSED')
});
});
test('browser_connect(debugController) works', async ({ startClient }) => {
test.skip(!globalThis.WebSocket, 'WebSocket is not supported in this environment');
const { client } = await startClient({
args: ['--vscode'],
});
expect(await client.callTool({
name: 'browser_connect',
arguments: {
debugController: true,
}
})).toHaveResponse({
result: 'No open browsers.'
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,foo'
}
})).toHaveResponse({
pageState: expect.stringContaining('foo'),
});
const response = await client.callTool({
name: 'browser_connect',
arguments: {
debugController: true,
}
});
expect(response.content?.[0].text).toMatch(/Version: \d+\.\d+\.\d+/);
const url = new URL(response.content?.[0].text.match(/URL: (.*)/)?.[1]);
const messages: unknown[] = [];
const socket = new WebSocket(url);
socket.onmessage = event => {
messages.push(JSON.parse(event.data));
};
await new Promise((resolve, reject) => {
socket.onopen = resolve;
socket.onerror = reject;
});
socket.send(JSON.stringify({
id: '1',
guid: 'DebugController',
method: 'setReportStateChanged',
params: {
enabled: true,
},
metadata: {},
}));
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,bar'
}
})).toHaveResponse({
pageState: expect.stringContaining('bar'),
});
await expect.poll(() => messages).toContainEqual(expect.objectContaining({ method: 'stateChanged' }));
});

View File

@@ -1,107 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('browser_wait_for(text)', async ({ client, server }) => {
server.setContent('/', `
<script>
function update() {
setTimeout(() => {
document.querySelector('div').textContent = 'Text to appear';
}, 1000);
}
</script>
<body>
<button onclick="update()">Click me</button>
<div>Text to disappear</div>
</body>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Click me',
ref: 'e2',
},
});
expect(await client.callTool({
name: 'browser_wait_for',
arguments: { text: 'Text to appear' },
code: `await page.getByText("Text to appear").first().waitFor({ state: 'visible' });`,
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`),
});
});
test('browser_wait_for(textGone)', async ({ client, server }) => {
server.setContent('/', `
<script>
function update() {
setTimeout(() => {
document.querySelector('div').textContent = 'Text to appear';
}, 1000);
}
</script>
<body>
<button onclick="update()">Click me</button>
<div>Text to disappear</div>
</body>
`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Click me',
ref: 'e2',
},
});
expect(await client.callTool({
name: 'browser_wait_for',
arguments: { textGone: 'Text to disappear' },
code: `await page.getByText("Text to disappear").first().waitFor({ state: 'hidden' });`,
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [ref=e3]: Text to appear`),
});
});
test('browser_wait_for(time)', async ({ client, server }) => {
server.setContent('/', `<body><div>Hello World</div></body>`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_wait_for',
arguments: { time: 1 },
})).toHaveResponse({
code: `await new Promise(f => setTimeout(f, 1 * 1000));`,
});
});

View File

@@ -1,40 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from './fixtures';
test('do not falsely advertise user agent as a test driver', async ({ client, server, mcpBrowser }) => {
test.skip(mcpBrowser === 'firefox');
test.skip(mcpBrowser === 'webkit');
server.route('/', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<body></body>
<script>
document.body.textContent = 'webdriver: ' + navigator.webdriver;
</script>
`);
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX,
},
})).toHaveResponse({
pageState: expect.stringContaining(`webdriver: false`),
});
});