test: run tests on MCP server inside Docker (#361)

https://github.com/microsoft/playwright-mcp/issues/346
This commit is contained in:
Max Schmitt
2025-05-07 18:04:20 +02:00
committed by GitHub
parent a115c31953
commit 09ba7989c3
8 changed files with 84 additions and 33 deletions

View File

@@ -19,13 +19,13 @@ import fs from 'node:fs';
import { Config } from '../config.js';
import { test, expect } from './fixtures.js';
test('config user data dir', async ({ startClient, mcpBrowser }, testInfo) => {
test('config user data dir', async ({ startClient, localOutputPath }) => {
const config: Config = {
browser: {
userDataDir: testInfo.outputPath('user-data-dir'),
userDataDir: localOutputPath('user-data-dir'),
},
};
const configPath = testInfo.outputPath('config.json');
const configPath = localOutputPath('config.json');
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
const client = await startClient({ args: ['--config', configPath] });

View File

@@ -18,7 +18,7 @@ import { test, expect } from './fixtures.js';
import fs from 'fs/promises';
import path from 'path';
test('browser_file_upload', async ({ client }) => {
test('browser_file_upload', async ({ client, localOutputPath }) => {
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
@@ -50,7 +50,7 @@ The tool "browser_file_upload" can only be used when there is related modal stat
})).toContainTextContent(`### Modal state
- [File chooser]: can be handled by the "browser_file_upload" tool`);
const filePath = test.info().outputPath('test.txt');
const filePath = localOutputPath('test.txt');
await fs.writeFile(filePath, 'Hello, world!');
{
@@ -96,8 +96,8 @@ The tool "browser_file_upload" can only be used when there is related modal stat
}
});
test('clicking on download link emits download', async ({ startClient }, testInfo) => {
const outputDir = testInfo.outputPath('output');
test('clicking on download link emits download', async ({ startClient, localOutputPath }) => {
const outputDir = localOutputPath('output');
const client = await startClient({
config: { outputDir },
});

View File

@@ -29,6 +29,7 @@ import type { Config } from '../config';
export type TestOptions = {
mcpBrowser: string | undefined;
mcpMode: 'docker' | undefined;
};
type TestFixtures = {
@@ -40,6 +41,7 @@ type TestFixtures = {
server: TestServer;
httpsServer: TestServer;
mcpHeadless: boolean;
localOutputPath: (filePath: string) => string;
};
type WorkerFixtures = {
@@ -56,12 +58,13 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
await use(await startClient({ args: ['--vision'] }));
},
startClient: async ({ mcpHeadless, mcpBrowser }, use, testInfo) => {
startClient: async ({ mcpHeadless, mcpBrowser, mcpMode }, use, testInfo) => {
const userDataDir = testInfo.outputPath('user-data-dir');
const configDir = path.dirname(test.info().config.configFile!);
let client: Client | undefined;
await use(async options => {
const args = ['--user-data-dir', userDataDir];
const args = ['--user-data-dir', path.relative(configDir, userDataDir)];
if (mcpHeadless)
args.push('--headless');
if (mcpBrowser)
@@ -71,15 +74,11 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
if (options?.config) {
const configFile = testInfo.outputPath('config.json');
await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2));
args.push(`--config=${configFile}`);
args.push(`--config=${path.relative(configDir, configFile)}`);
}
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);
const transport = new StdioClientTransport({
command: 'node',
args: [path.join(path.dirname(__filename), '../cli.js'), ...args],
});
client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' });
const transport = createTransport(args, mcpMode);
await client.connect(transport);
await client.ping();
return client;
@@ -130,6 +129,15 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
mcpBrowser: ['chrome', { option: true }],
mcpMode: [undefined, { option: true }],
localOutputPath: async ({ mcpMode }, use, testInfo) => {
await use(filePath => {
test.skip(mcpMode === 'docker', 'Mounting files is not supported in docker mode');
return testInfo.outputPath(filePath);
});
},
_workerServers: [async ({}, use, workerInfo) => {
const port = 8907 + workerInfo.workerIndex * 4;
const server = await TestServer.create(port);
@@ -156,6 +164,23 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
},
});
function createTransport(args: string[], mcpMode: TestOptions['mcpMode']) {
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);
if (mcpMode === 'docker') {
const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`];
return new StdioClientTransport({
command: 'docker',
args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args],
});
}
return new StdioClientTransport({
command: 'node',
args: [path.join(path.dirname(__filename), '../cli.js'), ...args],
cwd: path.join(path.dirname(__filename), '..'),
});
}
type Response = Awaited<ReturnType<Client['callTool']>>;
export const expect = baseExpect.extend({

View File

@@ -20,6 +20,7 @@ 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) => {

View File

@@ -45,5 +45,5 @@ test('browser_network_requests', async ({ client, server }) => {
await expect.poll(() => client.callTool({
name: 'browser_network_requests',
arguments: {},
})).toHaveTextContent(`[GET] http://localhost:${server.PORT}/json => [200] OK`);
})).toHaveTextContent(`[GET] ${`${server.PREFIX}/json`} => [200] OK`);
});

View File

@@ -73,8 +73,8 @@ test('browser_take_screenshot (element)', async ({ client }) => {
});
});
test('--output-dir should work', async ({ startClient }, testInfo) => {
const outputDir = testInfo.outputPath('output');
test('--output-dir should work', async ({ startClient, localOutputPath }) => {
const outputDir = localOutputPath('output');
const client = await startClient({
args: ['--output-dir', outputDir],
});
@@ -95,8 +95,8 @@ test('--output-dir should work', async ({ startClient }, testInfo) => {
});
test('browser_take_screenshot (outputDir)', async ({ startClient }, testInfo) => {
const outputDir = testInfo.outputPath('output');
test('browser_take_screenshot (outputDir)', async ({ startClient, localOutputPath }) => {
const outputDir = localOutputPath('output');
const client = await startClient({
config: { outputDir },
});