chore: fix operation over cdp (#440)

Ref https://github.com/microsoft/playwright-mcp/issues/439
This commit is contained in:
Pavel Feldman 2025-05-17 08:20:22 -07:00 committed by GitHub
parent c2b7fb29de
commit 1318e39fac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 46 additions and 53 deletions

View File

@ -40,6 +40,9 @@ export class PageSnapshot {
} }
private async _build() { private async _build() {
// FIXME: Rountrip evaluate to ensure _snapshotForAI works.
// This probably broke once we moved off locator snapshots
await this._page.evaluate(() => 1);
const snapshot = await callOnPageNoTrace(this._page, page => (page as PageEx)._snapshotForAI()); const snapshot = await callOnPageNoTrace(this._page, page => (page as PageEx)._snapshotForAI());
this._text = [ this._text = [
`- Page Snapshot`, `- Page Snapshot`,

View File

@ -16,16 +16,21 @@
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
test('cdp server', async ({ cdpEndpoint, startClient, server }) => { test('cdp server', async ({ cdpServer, startClient, server }) => {
const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] }); await cdpServer.start();
const client = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD }, arguments: { url: server.HELLO_WORLD },
})).toContainTextContent(`- generic [ref=e1]: Hello, world!`); })).toContainTextContent(`- generic [ref=e1]: Hello, world!`);
}); });
test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => { test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] }); 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({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',
@ -43,18 +48,17 @@ test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => {
// <internal code to capture accessibility snapshot> // <internal code to capture accessibility snapshot>
\`\`\` \`\`\`
- Page URL: data:text/html,hello world - Page URL: ${server.HELLO_WORLD}
- Page Title: - Page Title: Title
- Page Snapshot - Page Snapshot
\`\`\`yaml \`\`\`yaml
- generic [ref=e1]: hello world - generic [ref=e1]: Hello, world!
\`\`\` \`\`\`
`); `);
}); });
test('should throw connection error and allow re-connecting', async ({ cdpEndpoint, startClient, server }) => { test('should throw connection error and allow re-connecting', async ({ cdpServer, startClient, server }) => {
const port = 3200 + test.info().parallelIndex; const client = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
const client = await startClient({ args: [`--cdp-endpoint=http://localhost:${port}`] });
server.setContent('/', ` server.setContent('/', `
<title>Title</title> <title>Title</title>
@ -65,7 +69,7 @@ test('should throw connection error and allow re-connecting', async ({ cdpEndpoi
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },
})).toContainTextContent(`Error: browserType.connectOverCDP: connect ECONNREFUSED`); })).toContainTextContent(`Error: browserType.connectOverCDP: connect ECONNREFUSED`);
await cdpEndpoint(port); await cdpServer.start();
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.PREFIX },

View File

@ -22,22 +22,27 @@ import { chromium } from 'playwright';
import { test as baseTest, expect as baseExpect } from '@playwright/test'; import { test as baseTest, expect as baseExpect } from '@playwright/test';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { TestServer } from './testserver/index.ts'; import { TestServer } from './testserver/index.ts';
import type { Config } from '../config'; import type { Config } from '../config';
import type { BrowserContext } from 'playwright';
export type TestOptions = { export type TestOptions = {
mcpBrowser: string | undefined; mcpBrowser: string | undefined;
mcpMode: 'docker' | undefined; mcpMode: 'docker' | undefined;
}; };
type CDPServer = {
endpoint: string;
start: () => Promise<BrowserContext>;
};
type TestFixtures = { type TestFixtures = {
client: Client; client: Client;
visionClient: Client; visionClient: Client;
startClient: (options?: { clientName?: string, args?: string[], config?: Config }) => Promise<Client>; startClient: (options?: { clientName?: string, args?: string[], config?: Config }) => Promise<Client>;
wsEndpoint: string; wsEndpoint: string;
cdpEndpoint: (port?: number) => Promise<string>; cdpServer: CDPServer;
server: TestServer; server: TestServer;
httpsServer: TestServer; httpsServer: TestServer;
mcpHeadless: boolean; mcpHeadless: boolean;
@ -95,39 +100,25 @@ export const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>(
await browserServer.close(); await browserServer.close();
}, },
cdpEndpoint: async ({ }, use, testInfo) => { cdpServer: async ({ mcpBrowser }, use, testInfo) => {
let browserProcess: ChildProcessWithoutNullStreams | undefined; test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser!), 'CDP is not supported for non-Chromium browsers');
await use(async port => { let browserContext: BrowserContext | undefined;
if (!port) const port = 3200 + test.info().parallelIndex;
port = 3200 + test.info().parallelIndex; await use({
if (browserProcess) endpoint: `http://localhost:${port}`,
return `http://localhost:${port}`; start: async () => {
browserProcess = spawn(chromium.executablePath(), [ browserContext = await chromium.launchPersistentContext(testInfo.outputPath('cdp-user-data-dir'), {
`--user-data-dir=${testInfo.outputPath('user-data-dir')}`, channel: mcpBrowser,
`--remote-debugging-port=${port}`, headless: true,
`--no-first-run`, args: [
`--no-sandbox`, `--remote-debugging-port=${port}`,
`--headless`, ],
'--use-mock-keychain',
`data:text/html,hello world`,
], {
stdio: 'pipe',
});
await new Promise<void>(resolve => {
browserProcess!.stderr.on('data', data => {
if (data.toString().includes('DevTools listening on '))
resolve();
}); });
}); return browserContext;
return `http://localhost:${port}`; }
});
await new Promise<void>(resolve => {
if (!browserProcess)
return resolve();
browserProcess.on('exit', () => resolve());
browserProcess.kill();
}); });
await browserContext?.close();
}, },
mcpHeadless: async ({ headless }, use) => { mcpHeadless: async ({ headless }, use) => {

View File

@ -14,8 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { chromium } from 'playwright';
import { test, expect } from './fixtures.js'; import { test, expect } from './fixtures.js';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
@ -139,17 +137,14 @@ test('close tab', async ({ client }) => {
\`\`\``); \`\`\``);
}); });
test('reuse first tab when navigating', async ({ startClient, cdpEndpoint, server }) => { test('reuse first tab when navigating', async ({ startClient, cdpServer, server }) => {
server.setContent('/', `<title>Title</title><body>Body</body>`, 'text/html'); const browserContext = await cdpServer.start();
const pages = browserContext.pages();
const browser = await chromium.connectOverCDP(await cdpEndpoint()); const client = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] });
const [context] = browser.contexts();
const pages = context.pages();
const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] });
await client.callTool({ await client.callTool({
name: 'browser_navigate', name: 'browser_navigate',
arguments: { url: server.PREFIX }, arguments: { url: server.HELLO_WORLD },
}); });
expect(pages.length).toBe(1); expect(pages.length).toBe(1);