diff --git a/src/pageSnapshot.ts b/src/pageSnapshot.ts index c7850d5..2ec22c1 100644 --- a/src/pageSnapshot.ts +++ b/src/pageSnapshot.ts @@ -40,6 +40,9 @@ export class PageSnapshot { } 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()); this._text = [ `- Page Snapshot`, diff --git a/tests/cdp.spec.ts b/tests/cdp.spec.ts index 4d2ff65..23f2cea 100644 --- a/tests/cdp.spec.ts +++ b/tests/cdp.spec.ts @@ -16,16 +16,21 @@ import { test, expect } from './fixtures.js'; -test('cdp server', async ({ cdpEndpoint, startClient, server }) => { - const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] }); +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 }, })).toContainTextContent(`- generic [ref=e1]: Hello, world!`); }); -test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => { - const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] }); +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', @@ -43,18 +48,17 @@ test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => { // \`\`\` -- Page URL: data:text/html,hello world -- Page Title: +- Page URL: ${server.HELLO_WORLD} +- Page Title: Title - Page Snapshot \`\`\`yaml -- generic [ref=e1]: hello world +- generic [ref=e1]: Hello, world! \`\`\` `); }); -test('should throw connection error and allow re-connecting', async ({ cdpEndpoint, startClient, server }) => { - const port = 3200 + test.info().parallelIndex; - const client = await startClient({ args: [`--cdp-endpoint=http://localhost:${port}`] }); +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 @@ -65,7 +69,7 @@ test('should throw connection error and allow re-connecting', async ({ cdpEndpoi name: 'browser_navigate', arguments: { url: server.PREFIX }, })).toContainTextContent(`Error: browserType.connectOverCDP: connect ECONNREFUSED`); - await cdpEndpoint(port); + await cdpServer.start(); expect(await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX }, diff --git a/tests/fixtures.ts b/tests/fixtures.ts index 0779097..acf6cf0 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -22,22 +22,27 @@ import { chromium } from 'playwright'; import { test as baseTest, expect as baseExpect } from '@playwright/test'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; import { TestServer } from './testserver/index.ts'; import type { Config } from '../config'; +import type { BrowserContext } from 'playwright'; export type TestOptions = { mcpBrowser: string | undefined; mcpMode: 'docker' | undefined; }; +type CDPServer = { + endpoint: string; + start: () => Promise; +}; + type TestFixtures = { client: Client; visionClient: Client; startClient: (options?: { clientName?: string, args?: string[], config?: Config }) => Promise; wsEndpoint: string; - cdpEndpoint: (port?: number) => Promise; + cdpServer: CDPServer; server: TestServer; httpsServer: TestServer; mcpHeadless: boolean; @@ -95,39 +100,25 @@ export const test = baseTest.extend( await browserServer.close(); }, - cdpEndpoint: async ({ }, use, testInfo) => { - let browserProcess: ChildProcessWithoutNullStreams | undefined; + cdpServer: async ({ mcpBrowser }, use, testInfo) => { + test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser!), 'CDP is not supported for non-Chromium browsers'); - await use(async port => { - if (!port) - port = 3200 + test.info().parallelIndex; - if (browserProcess) - return `http://localhost:${port}`; - browserProcess = spawn(chromium.executablePath(), [ - `--user-data-dir=${testInfo.outputPath('user-data-dir')}`, - `--remote-debugging-port=${port}`, - `--no-first-run`, - `--no-sandbox`, - `--headless`, - '--use-mock-keychain', - `data:text/html,hello world`, - ], { - stdio: 'pipe', - }); - await new Promise(resolve => { - browserProcess!.stderr.on('data', data => { - if (data.toString().includes('DevTools listening on ')) - resolve(); + let browserContext: BrowserContext | undefined; + const port = 3200 + test.info().parallelIndex; + await use({ + endpoint: `http://localhost:${port}`, + start: async () => { + browserContext = await chromium.launchPersistentContext(testInfo.outputPath('cdp-user-data-dir'), { + channel: mcpBrowser, + headless: true, + args: [ + `--remote-debugging-port=${port}`, + ], }); - }); - return `http://localhost:${port}`; - }); - await new Promise(resolve => { - if (!browserProcess) - return resolve(); - browserProcess.on('exit', () => resolve()); - browserProcess.kill(); + return browserContext; + } }); + await browserContext?.close(); }, mcpHeadless: async ({ headless }, use) => { diff --git a/tests/tabs.spec.ts b/tests/tabs.spec.ts index 1972782..3242aa4 100644 --- a/tests/tabs.spec.ts +++ b/tests/tabs.spec.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { chromium } from 'playwright'; - import { test, expect } from './fixtures.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 }) => { - server.setContent('/', `TitleBody`, 'text/html'); +test('reuse first tab when navigating', async ({ startClient, cdpServer, server }) => { + const browserContext = await cdpServer.start(); + const pages = browserContext.pages(); - const browser = await chromium.connectOverCDP(await cdpEndpoint()); - const [context] = browser.contexts(); - const pages = context.pages(); - - const client = await startClient({ args: [`--cdp-endpoint=${await cdpEndpoint()}`] }); + const client = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); await client.callTool({ name: 'browser_navigate', - arguments: { url: server.PREFIX }, + arguments: { url: server.HELLO_WORLD }, }); expect(pages.length).toBe(1);