From 3e8fc820a6b94f5ce7f09a92515cb76259982a60 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 30 Mar 2026 15:56:05 -0700 Subject: [PATCH] chore: roll 1.59.0-alpha-1774903871000 (#1497) Fixes https://github.com/microsoft/playwright-mcp/issues/1496 --- Dockerfile | 6 ++- README.md | 54 +++++++++++++++------ package-lock.json | 56 ++++++++++------------ package.json | 2 +- packages/extension/tests/extension.spec.ts | 20 ++++---- packages/playwright-mcp/config.d.ts | 21 ++++---- packages/playwright-mcp/package.json | 4 +- packages/playwright-mcp/tests/fixtures.ts | 33 +++++++++---- 8 files changed, 117 insertions(+), 79 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8d98355..b39a4ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,6 @@ FROM base ARG PLAYWRIGHT_BROWSERS_PATH ARG USERNAME=node ENV NODE_ENV=production -ENV PLAYWRIGHT_MCP_OUTPUT_DIR=/tmp/playwright-output # Set the correct ownership for the runtime user on production `node_modules` RUN chown -R ${USERNAME}:${USERNAME} node_modules @@ -63,5 +62,8 @@ USER ${USERNAME} COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH} COPY --chown=${USERNAME}:${USERNAME} packages/playwright-mcp/cli.js packages/playwright-mcp/package.json ./ +# Current working directory must be writable as MCP may need to create default output dir in it. +WORKDIR /home/${USERNAME} + # Run in headless and only with chromium (other browsers need more dependencies not included in this image) -ENTRYPOINT ["node", "cli.js", "--headless", "--browser", "chromium", "--no-sandbox"] +ENTRYPOINT ["node", "/app/cli.js", "--headless", "--browser", "chromium", "--no-sandbox"] diff --git a/README.md b/README.md index 259980a..3d1288e 100644 --- a/README.md +++ b/README.md @@ -390,7 +390,7 @@ Playwright MCP server supports following arguments. They can be provided in the | --save-session | Whether to save the Playwright MCP session into the output directory.
*env* `PLAYWRIGHT_MCP_SAVE_SESSION` | | --secrets | path to a file containing secrets in the dotenv format
*env* `PLAYWRIGHT_MCP_SECRETS` | | --shared-browser-context | reuse the same browser context between all connected HTTP clients.
*env* `PLAYWRIGHT_MCP_SHARED_BROWSER_CONTEXT` | -| --snapshot-mode | when taking snapshots for responses, specifies the mode to use. Can be "incremental", "full", or "none". Default is incremental.
*env* `PLAYWRIGHT_MCP_SNAPSHOT_MODE` | +| --snapshot-mode | when taking snapshots for responses, specifies the mode to use. Can be "full" or "none". Default is "full".
*env* `PLAYWRIGHT_MCP_SNAPSHOT_MODE` | | --storage-state | path to the storage state file for isolated sessions.
*env* `PLAYWRIGHT_MCP_STORAGE_STATE` | | --test-id-attribute | specify the attribute to use for test ids, defaults to "data-testid"
*env* `PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE` | | --timeout-action | specify action timeout in milliseconds, defaults to 5000ms
*env* `PLAYWRIGHT_MCP_TIMEOUT_ACTION` | @@ -604,9 +604,9 @@ npx @playwright/mcp@latest --config path/to/config.json sharedBrowserContext?: boolean; /** - * Secrets are used to prevent LLM from getting sensitive data while - * automating scenarios such as authentication. - * Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative. + * Secrets are used to replace matching plain text in the tool responses to prevent the LLM + * from accidentally getting sensitive data. It is a convenience and not a security feature, + * make sure to always examine information coming in and from the tool on the client. */ secrets?: Record; @@ -615,11 +615,6 @@ npx @playwright/mcp@latest --config path/to/config.json */ outputDir?: string; - /** - * Whether to save snapshots, console messages, network logs and other session logs to a file or to the standard output. Defaults to "stdout". - */ - outputMode?: 'file' | 'stdout'; - console?: { /** * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info". @@ -678,12 +673,14 @@ npx @playwright/mcp@latest --config path/to/config.json /** * When taking snapshots for responses, specifies the mode to use. */ - mode?: 'incremental' | 'full' | 'none'; + mode?: 'full' | 'none'; }; /** - * Whether to allow file uploads from anywhere on the file system. - * By default (false), file uploads are restricted to paths within the MCP roots only. + * allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally + * wandering outside its intended workspace. It is a convenience defense to catch unintended + * file access, not a secure boundary; a deliberate attempt to reach other directories can be + * easily worked around, so always rely on client-level permissions for true security. */ allowUnrestrictedFileAccess?: boolean; @@ -845,6 +842,7 @@ http.createServer(async (req, res) => { - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element - `ref` (string, optional): Exact target element reference from the page snapshot - `selector` (string, optional): CSS or role selector for the target element, when "ref" is not available. + - `filename` (string, optional): Filename to save the result to. If not provided, result is returned as text. - Read-only: **false** @@ -909,7 +907,10 @@ http.createServer(async (req, res) => { - Title: List network requests - Description: Returns all network requests since loading the page - Parameters: - - `includeStatic` (boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false. + - `static` (boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false. + - `requestBody` (boolean): Whether to include request body. Defaults to false. + - `requestHeaders` (boolean): Whether to include request headers. Defaults to false. + - `filter` (string, optional): Only return requests whose URL matches this regexp (e.g. "/api/.*user"). - `filename` (string, optional): Filename to save the network requests to. If not provided, requests are returned as text. - Read-only: **true** @@ -938,7 +939,8 @@ http.createServer(async (req, res) => { - Title: Run Playwright code - Description: Run Playwright code snippet - Parameters: - - `code` (string): A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }` + - `code` (string, optional): A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }` + - `filename` (string, optional): Load code from the specified file. If both code and filename are provided, code will be ignored. - Read-only: **false** @@ -961,6 +963,7 @@ http.createServer(async (req, res) => { - Parameters: - `filename` (string, optional): Save snapshot to markdown file instead of returning it in the response. - `selector` (string, optional): Element selector of the root element to capture a partial snapshot instead of the whole page + - `depth` (number, optional): Limit the depth of the snapshot tree - Read-only: **true** @@ -1250,6 +1253,16 @@ http.createServer(async (req, res) => { +- **browser_resume** + - Title: Resume paused script execution + - Description: Resume script execution after it was paused. When called with step set to true, execution will pause again before the next action. + - Parameters: + - `step` (boolean, optional): When true, execution will pause again before the next action, allowing step-by-step debugging. + - `location` (string, optional): Pause execution at a specific :, e.g. "example.spec.ts:42". + - Read-only: **false** + + + - **browser_start_tracing** - Title: Start tracing - Description: Start trace recording @@ -1262,6 +1275,7 @@ http.createServer(async (req, res) => { - Title: Start video - Description: Start video recording - Parameters: + - `filename` (string, optional): Filename to save the video. - `size` (object, optional): Video size - Read-only: **true** @@ -1278,8 +1292,18 @@ http.createServer(async (req, res) => { - **browser_stop_video** - Title: Stop video - Description: Stop video recording + - Parameters: None + - Read-only: **true** + + + +- **browser_video_chapter** + - Title: Video chapter + - Description: Add a chapter marker to the video recording. Shows a full-screen chapter card with blurred backdrop. - Parameters: - - `filename` (string, optional): Filename to save the video + - `title` (string): Chapter title + - `description` (string, optional): Chapter description + - `duration` (number, optional): Duration in milliseconds to show the chapter card - Read-only: **true** diff --git a/package-lock.json b/package-lock.json index 07c3ff0..1da8880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ ], "devDependencies": { "@modelcontextprotocol/sdk": "^1.25.2", - "@playwright/test": "1.59.0-alpha-1773608981000", + "@playwright/test": "1.59.0-alpha-1774903871000", "@types/node": "^24.3.0" } }, @@ -854,13 +854,13 @@ "link": true }, "node_modules/@playwright/test": { - "version": "1.59.0-alpha-1773608981000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.0-alpha-1773608981000.tgz", - "integrity": "sha512-px+GAf8KIaMcPsCUPG3+xqPRSIPHgnizH7ygUjo6OXT1AigXTNCsIIVrPY3C5GjouM2MI4CQOkIKcSEjO84ZTg==", + "version": "1.59.0-alpha-1774903871000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.0-alpha-1774903871000.tgz", + "integrity": "sha512-VyTJsUhaCrpK5Uq5R493NkmkT8cdxICX2Bbpa/I5fJwQO/3xl3n3bJ7MOv6GYKPyCZDzISh4jXqs5yMnoGZPJw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.59.0-alpha-1773608981000" + "playwright": "1.59.0-alpha-1774903871000" }, "bin": { "playwright": "cli.js" @@ -2585,11 +2585,10 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", "dev": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/express" @@ -2603,11 +2602,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -2626,12 +2624,12 @@ } }, "node_modules/playwright": { - "version": "1.59.0-alpha-1773608981000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.0-alpha-1773608981000.tgz", - "integrity": "sha512-nb+BzawNj48eH6NdxecsysLuhCAB/p18FG7LLJp3MBfRGUkCAFtax0CFo/BhD+r0V4+0EW7llPK0p4cJQEIwUQ==", + "version": "1.59.0-alpha-1774903871000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.0-alpha-1774903871000.tgz", + "integrity": "sha512-wxkJjGiZ9LLtwxvdlOV1hPBubJe5kiJ2VXED0kmqT6Q/mRt0yNcdU580JUAa7e4um4AMUpRGnmq3ZNxpbnITng==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.59.0-alpha-1773608981000" + "playwright-core": "1.59.0-alpha-1774903871000" }, "bin": { "playwright": "cli.js" @@ -2648,9 +2646,9 @@ "link": true }, "node_modules/playwright-core": { - "version": "1.59.0-alpha-1773608981000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.0-alpha-1773608981000.tgz", - "integrity": "sha512-w6E5Q0Wleek3Wp7gtlSPGXuKeQ5eg6QPPJNNwgMHQRpkxgqOwgN2mX7x6Z52HJE10HFC88U5HQzOLMbag928Lg==", + "version": "1.59.0-alpha-1774903871000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.0-alpha-1774903871000.tgz", + "integrity": "sha512-Em/rbCpGHNblUwtO92KTnhYla4vehZpTCXy8iML2LPeTwRjk/4AOYcSKjrpAh05mQqPv9iz75KZXaNUddgiwpA==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -3102,11 +3100,10 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -3356,11 +3353,10 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -3448,8 +3444,8 @@ "version": "0.0.68", "license": "Apache-2.0", "dependencies": { - "playwright": "1.59.0-alpha-1773608981000", - "playwright-core": "1.59.0-alpha-1773608981000" + "playwright": "1.59.0-alpha-1774903871000", + "playwright-core": "1.59.0-alpha-1774903871000" }, "bin": { "playwright-mcp": "cli.js" diff --git a/package.json b/package.json index c61c06a..5fac965 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ ], "devDependencies": { "@modelcontextprotocol/sdk": "^1.25.2", - "@playwright/test": "1.59.0-alpha-1773608981000", + "@playwright/test": "1.59.0-alpha-1774903871000", "@types/node": "^24.3.0" } } diff --git a/packages/extension/tests/extension.spec.ts b/packages/extension/tests/extension.spec.ts index e78470d..0ac98a3 100644 --- a/packages/extension/tests/extension.spec.ts +++ b/packages/extension/tests/extension.spec.ts @@ -123,6 +123,14 @@ const test = base.extend({ }, }); +function cliEnv() { + return { + PLAYWRIGHT_SERVER_REGISTRY: test.info().outputPath('registry'), + PLAYWRIGHT_DAEMON_SESSION_DIR: test.info().outputPath('daemon'), + PLAYWRIGHT_SOCKETS_DIR: path.join(test.info().project.outputDir, 'ds', String(test.info().parallelIndex)), + }; +} + async function runCli( args: string[], options: { mcpBrowser?: string, testInfo: any }, @@ -133,7 +141,7 @@ async function runCli( const testInfo = options.testInfo; // Path to the terminal CLI - const cliPath = path.join(__dirname, '../../../node_modules/playwright/lib/cli/client/program.js'); + const cliPath = path.join(__dirname, '../../../node_modules/playwright-core/lib/tools/cli-client/cli.js'); return new Promise((resolve, reject) => { let stdout = ''; @@ -143,9 +151,7 @@ async function runCli( cwd: testInfo.outputPath(), env: { ...process.env, - PLAYWRIGHT_DAEMON_INSTALL_DIR: testInfo.outputPath(), - PLAYWRIGHT_DAEMON_SESSION_DIR: testInfo.outputPath('daemon'), - PLAYWRIGHT_DAEMON_SOCKETS_DIR: path.join(testInfo.project.outputDir, 'daemon-sockets'), + ...cliEnv(), PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser, PLAYWRIGHT_MCP_HEADLESS: 'false', }, @@ -213,8 +219,7 @@ test(`navigate with extension`, async ({ browserWithExtension, startClient, serv }); const selectorPage = await confirmationPagePromise; - // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector - await selectorPage.getByRole('button', { name: 'Allow' }).click(); + await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click(); expect(await navigateResponse).toHaveResponse({ snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), @@ -295,8 +300,7 @@ testWithOldExtensionVersion(`works with old extension version`, async ({ browser }); const selectorPage = await confirmationPagePromise; - // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector - await selectorPage.getByRole('button', { name: 'Allow' }).click(); + await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click(); expect(await navigateResponse).toHaveResponse({ snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`), diff --git a/packages/playwright-mcp/config.d.ts b/packages/playwright-mcp/config.d.ts index f664670..26cc075 100644 --- a/packages/playwright-mcp/config.d.ts +++ b/packages/playwright-mcp/config.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type * as playwright from 'playwright'; +import type * as playwright from '../../..'; export type ToolCapability = 'config' | @@ -143,9 +143,9 @@ export type Config = { sharedBrowserContext?: boolean; /** - * Secrets are used to prevent LLM from getting sensitive data while - * automating scenarios such as authentication. - * Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative. + * Secrets are used to replace matching plain text in the tool responses to prevent the LLM + * from accidentally getting sensitive data. It is a convenience and not a security feature, + * make sure to always examine information coming in and from the tool on the client. */ secrets?: Record; @@ -154,11 +154,6 @@ export type Config = { */ outputDir?: string; - /** - * Whether to save snapshots, console messages, network logs and other session logs to a file or to the standard output. Defaults to "stdout". - */ - outputMode?: 'file' | 'stdout'; - console?: { /** * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info". @@ -217,12 +212,14 @@ export type Config = { /** * When taking snapshots for responses, specifies the mode to use. */ - mode?: 'incremental' | 'full' | 'none'; + mode?: 'full' | 'none'; }; /** - * Whether to allow file uploads from anywhere on the file system. - * By default (false), file uploads are restricted to paths within the MCP roots only. + * allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally + * wandering outside its intended workspace. It is a convenience defense to catch unintended + * file access, not a secure boundary; a deliberate attempt to reach other directories can be + * easily worked around, so always rely on client-level permissions for true security. */ allowUnrestrictedFileAccess?: boolean; diff --git a/packages/playwright-mcp/package.json b/packages/playwright-mcp/package.json index 33afa92..c154990 100644 --- a/packages/playwright-mcp/package.json +++ b/packages/playwright-mcp/package.json @@ -33,8 +33,8 @@ } }, "dependencies": { - "playwright": "1.59.0-alpha-1773608981000", - "playwright-core": "1.59.0-alpha-1773608981000" + "playwright": "1.59.0-alpha-1774903871000", + "playwright-core": "1.59.0-alpha-1774903871000" }, "bin": { "playwright-mcp": "cli.js" diff --git a/packages/playwright-mcp/tests/fixtures.ts b/packages/playwright-mcp/tests/fixtures.ts index 4732985..c7a9714 100644 --- a/packages/playwright-mcp/tests/fixtures.ts +++ b/packages/playwright-mcp/tests/fixtures.ts @@ -74,10 +74,10 @@ export const test = baseTest.extend( }, startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => { - const configDir = path.dirname(test.info().config.configFile!); const clients: Client[] = []; await use(async options => { + const cwd = testInfo.outputPath(); const args: string[] = mcpArgs ?? []; if (process.env.CI && process.platform === 'linux') args.push('--no-sandbox'); @@ -90,7 +90,7 @@ export const test = baseTest.extend( if (options?.config) { const configFile = testInfo.outputPath('config.json'); await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2)); - args.push(`--config=${path.relative(configDir, configFile)}`); + args.push(`--config=${path.relative(cwd, configFile)}`); } const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined); @@ -103,7 +103,7 @@ export const test = baseTest.extend( }; }); } - const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken); + const { transport, stderr } = await createTransport(args, cwd, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken); let stderrBuffer = ''; stderr?.on('data', data => { if (process.env.PWMCP_DEBUG) @@ -182,12 +182,14 @@ export const test = baseTest.extend( }, }); -async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{ +async function createTransport(args: string[], cwd: string, mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{ transport: Transport, stderr: Stream | null, }> { if (mcpMode === 'docker') { - const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`]; + const relCwd = path.relative(test.info().project.outputDir, cwd); + const dockerCwd = path.posix.join('/app/test-results', relCwd.split(path.sep).join('/')); + const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`, '-w', dockerCwd]; const transport = new StdioClientTransport({ command: 'docker', args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args], @@ -201,7 +203,7 @@ async function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], const transport = new StdioClientTransport({ command: 'node', args: [path.join(__dirname, '../cli.js'), ...args], - cwd: path.dirname(test.info().config.configFile!), + cwd, stderr: 'pipe', env: { ...process.env, @@ -222,7 +224,7 @@ type Response = Awaited>; export const expect = baseExpect.extend({ toHaveResponse(response: Response, object: any) { - const parsed = parseResponse(response); + const parsed = parseResponse(response, test.info().outputPath()); const isNot = this.isNot; try { if (isNot) @@ -246,7 +248,7 @@ export function formatOutput(output: string): string[] { return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean); } -function parseResponse(response: any) { +function parseResponse(response: any, cwd: string) { const text = response.content[0].text; const sections = parseSections(text); @@ -255,7 +257,7 @@ function parseResponse(response: any) { const code = sections.get('Ran Playwright code'); const tabs = sections.get('Open tabs'); const pageState = sections.get('Page state'); - const snapshot = sections.get('Snapshot'); + const snapshotSection = sections.get('Snapshot'); const consoleMessages = sections.get('New console messages'); const modalState = sections.get('Modal state'); const downloads = sections.get('Downloads'); @@ -263,6 +265,19 @@ function parseResponse(response: any) { const isError = response.isError; const attachments = response.content.slice(1); + let snapshot: string | undefined; + if (snapshotSection) { + const match = snapshotSection.match(/\[Snapshot\]\(([^)]+)\)/); + if (match) { + try { + snapshot = fs.readFileSync(path.resolve(cwd, match[1]), 'utf-8'); + } catch { + } + } else { + snapshot = snapshotSection.replace(/^```yaml\n?/, '').replace(/\n?```$/, ''); + } + } + return { error, result,