3 Commits

Author SHA1 Message Date
Pavel Feldman
878be97668 chore: mark v0.0.18 (#315) 2025-04-30 13:07:55 -07:00
Pavel Feldman
6d6b1a384b chore: fix merge config (#311) 2025-04-30 08:41:19 -07:00
Pavel Feldman
fd22def4c5 chore: fix test harness, close the client (#312) 2025-04-30 08:07:54 -07:00
6 changed files with 78 additions and 19 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@playwright/mcp", "name": "@playwright/mcp",
"version": "0.0.17", "version": "0.0.18",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@playwright/mcp", "name": "@playwright/mcp",
"version": "0.0.17", "version": "0.0.18",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.10.1", "@modelcontextprotocol/sdk": "^1.10.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@playwright/mcp", "name": "@playwright/mcp",
"version": "0.0.17", "version": "0.0.18",
"description": "Playwright Tools for MCP", "description": "Playwright Tools for MCP",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -89,7 +89,7 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
const launchOptions: LaunchOptions = { const launchOptions: LaunchOptions = {
channel, channel,
executablePath: cliOptions.executablePath, executablePath: cliOptions.executablePath,
headless: cliOptions.headless ?? false, headless: cliOptions.headless,
}; };
if (browserName === 'chromium') if (browserName === 'chromium')
@@ -158,24 +158,33 @@ export async function outputFile(config: Config, name: string): Promise<string>
return path.join(result, fileName); return path.join(result, fileName);
} }
function pickDefined<T extends object>(obj: T | undefined): Partial<T> {
return Object.fromEntries(
Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined)
) as Partial<T>;
}
function mergeConfig(base: Config, overrides: Config): Config { function mergeConfig(base: Config, overrides: Config): Config {
const browser: Config['browser'] = { const browser: Config['browser'] = {
...base.browser, ...pickDefined(base.browser),
...overrides.browser, ...pickDefined(overrides.browser),
launchOptions: { launchOptions: {
...base.browser?.launchOptions, ...pickDefined(base.browser?.launchOptions),
...overrides.browser?.launchOptions, ...pickDefined(overrides.browser?.launchOptions),
...{ assistantMode: true }, ...{ assistantMode: true },
}, },
contextOptions: { contextOptions: {
...base.browser?.contextOptions, ...pickDefined(base.browser?.contextOptions),
...overrides.browser?.contextOptions, ...pickDefined(overrides.browser?.contextOptions),
}, },
}; };
if (browser.browserName !== 'chromium')
delete browser.launchOptions.channel;
return { return {
...base, ...pickDefined(base),
...overrides, ...pickDefined(overrides),
browser, browser,
}; };
} }

View File

@@ -41,6 +41,7 @@ program
.option('--config <path>', 'Path to the configuration file.') .option('--config <path>', 'Path to the configuration file.')
.action(async options => { .action(async options => {
const config = await resolveConfig(options); const config = await resolveConfig(options);
console.error(config);
const serverList = new ServerList(() => createServer(config)); const serverList = new ServerList(() => createServer(config));
setupExitWatchdog(serverList); setupExitWatchdog(serverList);

View File

@@ -34,11 +34,11 @@ type TestFixtures = {
cdpEndpoint: string; cdpEndpoint: string;
server: TestServer; server: TestServer;
httpsServer: TestServer; httpsServer: TestServer;
mcpHeadless: boolean;
mcpBrowser: string | undefined;
}; };
type WorkerFixtures = { type WorkerFixtures = {
mcpHeadless: boolean;
mcpBrowser: string | undefined;
_workerServers: { server: TestServer, httpsServer: TestServer }; _workerServers: { server: TestServer, httpsServer: TestServer };
}; };
@@ -54,7 +54,7 @@ export const test = baseTest.extend<TestFixtures, WorkerFixtures>({
startClient: async ({ mcpHeadless, mcpBrowser }, use, testInfo) => { startClient: async ({ mcpHeadless, mcpBrowser }, use, testInfo) => {
const userDataDir = testInfo.outputPath('user-data-dir'); const userDataDir = testInfo.outputPath('user-data-dir');
let client: StdioClientTransport | undefined; let client: Client | undefined;
await use(async options => { await use(async options => {
const args = ['--user-data-dir', userDataDir]; const args = ['--user-data-dir', userDataDir];
@@ -73,7 +73,7 @@ export const test = baseTest.extend<TestFixtures, WorkerFixtures>({
command: 'node', command: 'node',
args: [path.join(__dirname, '../cli.js'), ...args], args: [path.join(__dirname, '../cli.js'), ...args],
}); });
const client = new Client({ name: 'test', version: '1.0.0' }); client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(transport); await client.connect(transport);
await client.ping(); await client.ping();
return client; return client;
@@ -112,11 +112,11 @@ export const test = baseTest.extend<TestFixtures, WorkerFixtures>({
browserProcess.kill(); browserProcess.kill();
}, },
mcpHeadless: [async ({ headless }, use) => { mcpHeadless: async ({ headless }, use) => {
await use(headless); await use(headless);
}, { scope: 'worker' }], },
mcpBrowser: ['chrome', { option: true, scope: 'worker' }], mcpBrowser: ['chrome', { option: true }],
_workerServers: [async ({}, use, workerInfo) => { _workerServers: [async ({}, use, workerInfo) => {
const port = 8907 + workerInfo.workerIndex * 4; const port = 8907 + workerInfo.workerIndex * 4;

49
tests/headed.spec.ts Normal file
View File

@@ -0,0 +1,49 @@
/**
* 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('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).toContainTextContent(`Mozilla/5.0`);
if (mcpHeadless)
expect(response).toContainTextContent(`HeadlessChrome`);
else
expect(response).not.toContainTextContent(`HeadlessChrome`);
});
});
}