From f20ae22ec6e780bedbc9a532dafa48663935ea75 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 24 May 2025 11:44:57 -0700 Subject: [PATCH] chore: roll Playwright, remove localOutputDir (#471) --- examples/generate-test.md | 2 +- package-lock.json | 26 +++++++++++++------------- package.json | 4 ++-- playwright.config.ts | 9 ++++++++- src/pageSnapshot.ts | 4 ++-- src/tools/screenshot.ts | 2 +- src/tools/snapshot.ts | 12 ++++++------ tests/config.spec.ts | 10 +++++----- tests/files.spec.ts | 17 +++++++---------- tests/fixtures.ts | 8 -------- tests/launch.spec.ts | 4 ++-- tests/pdf.spec.ts | 9 ++++----- tests/screenshot.spec.ts | 34 +++++++++++++++++----------------- tests/sse.spec.ts | 4 ++-- tests/trace.spec.ts | 5 +++-- 15 files changed, 73 insertions(+), 77 deletions(-) diff --git a/examples/generate-test.md b/examples/generate-test.md index 13209f8..b6d8dd8 100644 --- a/examples/generate-test.md +++ b/examples/generate-test.md @@ -1,4 +1,4 @@ -Generate test for scenario: +Use Playwright tools to generate test for scenario: ## GitHub PR Checks Navigation Checklist diff --git a/package-lock.json b/package-lock.json index 1b2d97c..13f7e12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.11.0", "commander": "^13.1.0", - "playwright": "1.53.0-alpha-1746832516000", + "playwright": "1.53.0-alpha-2025-05-23", "zod-to-json-schema": "^3.24.4" }, "bin": { @@ -20,7 +20,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.19.0", - "@playwright/test": "1.53.0-alpha-1746832516000", + "@playwright/test": "1.53.0-alpha-2025-05-23", "@stylistic/eslint-plugin": "^3.0.1", "@types/node": "^22.13.10", "@typescript-eslint/eslint-plugin": "^8.26.1", @@ -286,13 +286,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.53.0-alpha-1746832516000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0-alpha-1746832516000.tgz", - "integrity": "sha512-Sec+6uzpA4MfwmQqJFBFVazffynqVwLO5swDxG7WoqgpUdn9gQX4K4tDG64SV6f4nOpwdM5LKTasPSXu02nn/Q==", + "version": "1.53.0-alpha-2025-05-23", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0-alpha-2025-05-23.tgz", + "integrity": "sha512-WdTIHB2I5IuBs8q/CSnjauuhm3o1sShdgO+lKCncRh0nD24PTyyUiE8yBrq4OazrX1toGHlawV1HwEIdrq+fcg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.53.0-alpha-1746832516000" + "playwright": "1.53.0-alpha-2025-05-23" }, "bin": { "playwright": "cli.js" @@ -3298,12 +3298,12 @@ } }, "node_modules/playwright": { - "version": "1.53.0-alpha-1746832516000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0-alpha-1746832516000.tgz", - "integrity": "sha512-kcC1B2XJr4VaDAcVzi61SbYGkodq1QIqQXuPieXsNgZZ7cEKWzO2sI42yp2yie6wlCx0oLkSS2Q6jWSRVRLeaw==", + "version": "1.53.0-alpha-2025-05-23", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0-alpha-2025-05-23.tgz", + "integrity": "sha512-86hfHKdPcBAjDguEb6doNG76uj2fUbFBCCqew4/0KwOnLrpAPJ2Uyeygt+zsj2k9ok+n7NWtsbpxSmXj6kYieA==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.53.0-alpha-1746832516000" + "playwright-core": "1.53.0-alpha-2025-05-23" }, "bin": { "playwright": "cli.js" @@ -3316,9 +3316,9 @@ } }, "node_modules/playwright-core": { - "version": "1.53.0-alpha-1746832516000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0-alpha-1746832516000.tgz", - "integrity": "sha512-4O98y4zV0rOP6CepMLC/VGuzqGaR1sS9AVh+i0CghWMQHM/8bxPJI8W38QndO0JU0V5nBD6j7DQeNt1mJ+CZ+g==", + "version": "1.53.0-alpha-2025-05-23", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0-alpha-2025-05-23.tgz", + "integrity": "sha512-60XHM1EGJl5ugdUwMNYbmJoj6zTAAZO0Rr5OTApFwj2gqQC5vERbB6XewD8a2MbxAMXVaZThKFcABr1VZi6NkQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/package.json b/package.json index 7a63df1..4b1a3c4 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,13 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.11.0", "commander": "^13.1.0", - "playwright": "1.53.0-alpha-1746832516000", + "playwright": "1.53.0-alpha-2025-05-23", "zod-to-json-schema": "^3.24.4" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.19.0", - "@playwright/test": "1.53.0-alpha-1746832516000", + "@playwright/test": "1.53.0-alpha-2025-05-23", "@stylistic/eslint-plugin": "^3.0.1", "@types/node": "^22.13.10", "@typescript-eslint/eslint-plugin": "^8.26.1", diff --git a/playwright.config.ts b/playwright.config.ts index 76e0344..9c8ba59 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -29,7 +29,14 @@ export default defineConfig({ { name: 'chrome' }, { name: 'msedge', use: { mcpBrowser: 'msedge' } }, { name: 'chromium', use: { mcpBrowser: 'chromium' } }, - ...process.env.MCP_IN_DOCKER ? [{ name: 'chromium-docker', use: { mcpBrowser: 'chromium', mcpMode: 'docker' as const } }] : [], + ...process.env.MCP_IN_DOCKER ? [{ + name: 'chromium-docker', + grep: /browser_navigate|browser_click/, + use: { + mcpBrowser: 'chromium', + mcpMode: 'docker' as const + } + }] : [], { name: 'firefox', use: { mcpBrowser: 'firefox' } }, { name: 'webkit', use: { mcpBrowser: 'webkit' } }, ], diff --git a/src/pageSnapshot.ts b/src/pageSnapshot.ts index 2ec22c1..55e4b91 100644 --- a/src/pageSnapshot.ts +++ b/src/pageSnapshot.ts @@ -52,7 +52,7 @@ export class PageSnapshot { ].join('\n'); } - refLocator(ref: string): playwright.Locator { - return this._page.locator(`aria-ref=${ref}`); + refLocator(params: { element: string, ref: string }): playwright.Locator { + return this._page.locator(`aria-ref=${params.ref}`); } } diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index db5b7f1..3317103 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -57,7 +57,7 @@ const screenshot = defineTool({ `// Screenshot ${isElementScreenshot ? params.element : 'viewport'} and save it as ${fileName}`, ]; - const locator = params.ref ? snapshot.refLocator(params.ref) : null; + const locator = params.ref ? snapshot.refLocator({ element: params.element || '', ref: params.ref }) : null; if (locator) code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`); diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 576d578..49aaf52 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -58,7 +58,7 @@ const click = defineTool({ handle: async (context, params) => { const tab = context.currentTabOrDie(); - const locator = tab.snapshotOrDie().refLocator(params.ref); + const locator = tab.snapshotOrDie().refLocator(params); const code = [ `// Click ${params.element}`, @@ -91,8 +91,8 @@ const drag = defineTool({ handle: async (context, params) => { const snapshot = context.currentTabOrDie().snapshotOrDie(); - const startLocator = snapshot.refLocator(params.startRef); - const endLocator = snapshot.refLocator(params.endRef); + const startLocator = snapshot.refLocator({ ref: params.startRef, element: params.startElement }); + const endLocator = snapshot.refLocator({ ref: params.endRef, element: params.endElement }); const code = [ `// Drag ${params.startElement} to ${params.endElement}`, @@ -120,7 +120,7 @@ const hover = defineTool({ handle: async (context, params) => { const snapshot = context.currentTabOrDie().snapshotOrDie(); - const locator = snapshot.refLocator(params.ref); + const locator = snapshot.refLocator(params); const code = [ `// Hover over ${params.element}`, @@ -154,7 +154,7 @@ const type = defineTool({ handle: async (context, params) => { const snapshot = context.currentTabOrDie().snapshotOrDie(); - const locator = snapshot.refLocator(params.ref); + const locator = snapshot.refLocator(params); const code: string[] = []; const steps: (() => Promise)[] = []; @@ -200,7 +200,7 @@ const selectOption = defineTool({ handle: async (context, params) => { const snapshot = context.currentTabOrDie().snapshotOrDie(); - const locator = snapshot.refLocator(params.ref); + const locator = snapshot.refLocator(params); const code = [ `// Select options [${params.values.join(', ')}] in ${params.element}`, diff --git a/tests/config.spec.ts b/tests/config.spec.ts index 8e87daa..03c3d81 100644 --- a/tests/config.spec.ts +++ b/tests/config.spec.ts @@ -19,7 +19,7 @@ import fs from 'node:fs'; import { Config } from '../config.js'; import { test, expect } from './fixtures.js'; -test('config user data dir', async ({ startClient, localOutputPath, server }) => { +test('config user data dir', async ({ startClient, server }, testInfo) => { server.setContent('/', ` Title Hello, world! @@ -27,10 +27,10 @@ test('config user data dir', async ({ startClient, localOutputPath, server }) => const config: Config = { browser: { - userDataDir: localOutputPath('user-data-dir'), + userDataDir: testInfo.outputPath('user-data-dir'), }, }; - const configPath = localOutputPath('config.json'); + const configPath = testInfo.outputPath('config.json'); await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2)); const client = await startClient({ args: ['--config', configPath] }); @@ -45,13 +45,13 @@ test('config user data dir', async ({ startClient, localOutputPath, server }) => test.describe(() => { test.use({ mcpBrowser: '' }); - test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient, localOutputPath }) => { + test('browserName', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/458' } }, async ({ startClient }, testInfo) => { const config: Config = { browser: { browserName: 'firefox', }, }; - const configPath = localOutputPath('config.json'); + const configPath = testInfo.outputPath('config.json'); await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2)); const client = await startClient({ args: ['--config', configPath] }); diff --git a/tests/files.spec.ts b/tests/files.spec.ts index 47e337b..600d54f 100644 --- a/tests/files.spec.ts +++ b/tests/files.spec.ts @@ -16,9 +16,8 @@ import { test, expect } from './fixtures.js'; import fs from 'fs/promises'; -import path from 'path'; -test('browser_file_upload', async ({ client, localOutputPath, server }) => { +test('browser_file_upload', async ({ client, server }, testInfo) => { server.setContent('/', ` @@ -54,7 +53,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 = localOutputPath('test.txt'); + const filePath = testInfo.outputPath('test.txt'); await fs.writeFile(filePath, 'Hello, world!'); { @@ -101,10 +100,9 @@ The tool "browser_file_upload" can only be used when there is related modal stat } }); -test('clicking on download link emits download', async ({ startClient, localOutputPath, server }) => { - const outputDir = localOutputPath('output'); +test('clicking on download link emits download', async ({ startClient, server }, testInfo) => { const client = await startClient({ - config: { outputDir }, + config: { outputDir: testInfo.outputPath('output') }, }); server.setContent('/', `Download`, 'text/html'); @@ -123,13 +121,12 @@ test('clicking on download link emits download', async ({ startClient, localOutp }); await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent(` ### Downloads -- Downloaded file test.txt to ${path.join(outputDir, 'test.txt')}`); +- Downloaded file test.txt to ${testInfo.outputPath('output', 'test.txt')}`); }); -test('navigating to download link emits download', async ({ startClient, localOutputPath, mcpBrowser, server }) => { - const outputDir = localOutputPath('output'); +test('navigating to download link emits download', async ({ startClient, server, mcpBrowser }, testInfo) => { const client = await startClient({ - args: ['--output-dir', outputDir], + config: { outputDir: testInfo.outputPath('output') }, }); test.skip(mcpBrowser === 'webkit' && process.platform === 'linux', 'https://github.com/microsoft/playwright/blob/8e08fdb52c27bb75de9bf87627bf740fadab2122/tests/library/download.spec.ts#L436'); diff --git a/tests/fixtures.ts b/tests/fixtures.ts index acf6cf0..598af17 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -46,7 +46,6 @@ type TestFixtures = { server: TestServer; httpsServer: TestServer; mcpHeadless: boolean; - localOutputPath: (filePath: string) => string; }; type WorkerFixtures = { @@ -129,13 +128,6 @@ export const test = baseTest.extend( 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); diff --git a/tests/launch.spec.ts b/tests/launch.spec.ts index 3670ef1..713e412 100644 --- a/tests/launch.spec.ts +++ b/tests/launch.spec.ts @@ -104,8 +104,8 @@ test('isolated context', async ({ startClient, server }) => { expect(response2).toContainTextContent(`Storage: NO`); }); -test('isolated context with storage state', async ({ startClient, server, localOutputPath }) => { - const storageStatePath = localOutputPath('storage-state.json'); +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: [ { diff --git a/tests/pdf.spec.ts b/tests/pdf.spec.ts index 224b026..7b599c8 100644 --- a/tests/pdf.spec.ts +++ b/tests/pdf.spec.ts @@ -30,10 +30,9 @@ test('save as pdf unavailable', async ({ startClient, server }) => { })).toHaveTextContent(/Tool \"browser_pdf_save\" not found/); }); -test('save as pdf', async ({ startClient, mcpBrowser, server, localOutputPath }) => { - const outputDir = localOutputPath('output'); +test('save as pdf', async ({ startClient, mcpBrowser, server }, testInfo) => { const client = await startClient({ - config: { outputDir }, + config: { outputDir: testInfo.outputPath('output') }, }); test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.'); @@ -49,9 +48,9 @@ test('save as pdf', async ({ startClient, mcpBrowser, server, localOutputPath }) expect(response).toHaveTextContent(/Save page as.*page-[^:]+.pdf/); }); -test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser, server, localOutputPath }) => { +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 outputDir = localOutputPath('output'); const client = await startClient({ config: { outputDir }, }); diff --git a/tests/screenshot.spec.ts b/tests/screenshot.spec.ts index ffc30d6..b1d73f6 100644 --- a/tests/screenshot.spec.ts +++ b/tests/screenshot.spec.ts @@ -18,10 +18,9 @@ import fs from 'fs'; import { test, expect } from './fixtures.js'; -test('browser_take_screenshot (viewport)', async ({ startClient, server, localOutputPath }) => { - const outputDir = localOutputPath('output'); +test('browser_take_screenshot (viewport)', async ({ startClient, server }, testInfo) => { const client = await startClient({ - args: ['--output-dir', outputDir], + config: { outputDir: testInfo.outputPath('output') }, }); expect(await client.callTool({ name: 'browser_navigate', @@ -45,10 +44,9 @@ test('browser_take_screenshot (viewport)', async ({ startClient, server, localOu }); }); -test('browser_take_screenshot (element)', async ({ startClient, server, localOutputPath }) => { - const outputDir = localOutputPath('output'); +test('browser_take_screenshot (element)', async ({ startClient, server }, testInfo) => { const client = await startClient({ - args: ['--output-dir', outputDir], + config: { outputDir: testInfo.outputPath('output') }, }); expect(await client.callTool({ name: 'browser_navigate', @@ -76,10 +74,10 @@ test('browser_take_screenshot (element)', async ({ startClient, server, localOut }); }); -test('--output-dir should work', async ({ startClient, localOutputPath, server }) => { - const outputDir = localOutputPath('output'); +test('--output-dir should work', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); const client = await startClient({ - args: ['--output-dir', outputDir], + config: { outputDir }, }); expect(await client.callTool({ name: 'browser_navigate', @@ -97,9 +95,9 @@ test('--output-dir should work', async ({ startClient, localOutputPath, server } }); for (const raw of [undefined, true]) { - test(`browser_take_screenshot (raw: ${raw})`, async ({ startClient, localOutputPath, server }) => { + test(`browser_take_screenshot (raw: ${raw})`, async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); const ext = raw ? 'png' : 'jpeg'; - const outputDir = localOutputPath('output'); const client = await startClient({ config: { outputDir }, }); @@ -138,8 +136,8 @@ for (const raw of [undefined, true]) { } -test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient, localOutputPath, server }) => { - const outputDir = localOutputPath('output'); +test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); const client = await startClient({ config: { outputDir }, }); @@ -174,10 +172,11 @@ test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient, expect(files[0]).toMatch(/^output\.jpeg$/); }); -test('browser_take_screenshot (noImageResponses)', async ({ startClient, server, localOutputPath }) => { +test('browser_take_screenshot (noImageResponses)', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); const client = await startClient({ config: { - outputDir: localOutputPath('output'), + outputDir, noImageResponses: true, }, }); @@ -203,8 +202,9 @@ test('browser_take_screenshot (noImageResponses)', async ({ startClient, server, }); }); -test('browser_take_screenshot (cursor)', async ({ startClient, server, localOutputPath }) => { - const outputDir = localOutputPath('output'); +test('browser_take_screenshot (cursor)', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const client = await startClient({ clientName: 'cursor:vscode', config: { outputDir }, diff --git a/tests/sse.spec.ts b/tests/sse.spec.ts index 73a0a25..266d456 100644 --- a/tests/sse.spec.ts +++ b/tests/sse.spec.ts @@ -65,8 +65,8 @@ test('streamable http transport', async ({ serverEndpoint }) => { expect(transport.sessionId, 'has session support').toBeDefined(); }); -test('sse transport via public API', async ({ server, localOutputPath }) => { - const userDataDir = localOutputPath('user-data-dir'); +test('sse transport via public API', async ({ server }, testInfo) => { + const userDataDir = testInfo.outputPath('user-data-dir'); const sessions = new Map(); const mcpServer = http.createServer(async (req, res) => { if (req.method === 'GET') { diff --git a/tests/trace.spec.ts b/tests/trace.spec.ts index 1ca75bb..b3e78b7 100644 --- a/tests/trace.spec.ts +++ b/tests/trace.spec.ts @@ -19,8 +19,9 @@ import path from 'path'; import { test, expect } from './fixtures.js'; -test('check that trace is saved', async ({ startClient, server, localOutputPath }) => { - const outputDir = localOutputPath('output'); +test('check that trace is saved', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const client = await startClient({ args: ['--save-trace', `--output-dir=${outputDir}`], });