From 697a69a8c2115d0c9c48ee71edb40baf1143e865 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 28 Apr 2025 16:35:33 -0700 Subject: [PATCH] chore: allow specifying output dir (#285) Ref: https://github.com/microsoft/playwright-mcp/issues/279 --- config.d.ts | 5 +++++ src/config.ts | 11 +++++++++++ src/tools/pdf.ts | 7 ++----- src/tools/snapshot.ts | 7 ++----- tests/fixtures.ts | 10 +++++++++- tests/screenshot.spec.ts | 23 +++++++++++++++++++++++ 6 files changed, 52 insertions(+), 11 deletions(-) diff --git a/config.d.ts b/config.d.ts index efddda8..d08af7d 100644 --- a/config.d.ts +++ b/config.d.ts @@ -80,4 +80,9 @@ export type Config = { * Run server that uses screenshots (Aria snapshots are used by default). */ vision?: boolean; + + /** + * The directory to save output files. + */ + outputDir?: string; }; diff --git a/src/config.ts b/src/config.ts index 8c56f89..858e7ed 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,8 +14,12 @@ * limitations under the License. */ +import fs from 'fs'; import net from 'net'; import os from 'os'; +import path from 'path'; + +import { sanitizeForFilePath } from './tools/utils'; import type { Config } from '../config'; import type { LaunchOptions, BrowserContextOptions } from 'playwright'; @@ -84,3 +88,10 @@ async function findFreePort() { server.on('error', reject); }); } + +export async function outputFile(config: Config, name: string): Promise { + const result = config.outputDir ?? os.tmpdir(); + await fs.promises.mkdir(result, { recursive: true }); + const fileName = sanitizeForFilePath(name); + return path.join(result, fileName); +} diff --git a/src/tools/pdf.ts b/src/tools/pdf.ts index 1929fe3..52cccbf 100644 --- a/src/tools/pdf.ts +++ b/src/tools/pdf.ts @@ -14,14 +14,11 @@ * limitations under the License. */ -import os from 'os'; -import path from 'path'; - import { z } from 'zod'; import { defineTool } from './tool'; -import { sanitizeForFilePath } from './utils'; import * as javascript from '../javascript'; +import { outputFile } from '../config'; const pdf = defineTool({ capability: 'pdf', @@ -34,7 +31,7 @@ const pdf = defineTool({ handle: async context => { const tab = context.currentTabOrDie(); - const fileName = path.join(os.tmpdir(), sanitizeForFilePath(`page-${new Date().toISOString()}`)) + '.pdf'; + const fileName = await outputFile(context.config, `page-${new Date().toISOString()}'.pdf'`); const code = [ `// Save page as ${fileName}`, diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 44ce4e0..8f87511 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -14,14 +14,11 @@ * limitations under the License. */ -import path from 'path'; -import os from 'os'; - import { z } from 'zod'; -import { sanitizeForFilePath } from './utils'; import { defineTool } from './tool'; import * as javascript from '../javascript'; +import { outputFile } from '../config'; import type * as playwright from 'playwright'; @@ -232,7 +229,7 @@ const screenshot = defineTool({ const tab = context.currentTabOrDie(); const snapshot = tab.snapshotOrDie(); const fileType = params.raw ? 'png' : 'jpeg'; - const fileName = path.join(os.tmpdir(), sanitizeForFilePath(`page-${new Date().toISOString()}`)) + `.${fileType}`; + const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.${fileType}`); const options: playwright.PageScreenshotOptions = { type: fileType, quality: fileType === 'png' ? undefined : 50, scale: 'css', path: fileName }; const isElementScreenshot = params.element && params.ref; diff --git a/tests/fixtures.ts b/tests/fixtures.ts index 43b8d9b..ce8b03f 100644 --- a/tests/fixtures.ts +++ b/tests/fixtures.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import fs from 'fs'; import path from 'path'; import { chromium } from 'playwright'; @@ -23,10 +24,12 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { spawn } from 'child_process'; import { TestServer } from './testserver'; +import type { Config } from '../config'; + type TestFixtures = { client: Client; visionClient: Client; - startClient: (options?: { args?: string[] }) => Promise; + startClient: (options?: { args?: string[], config?: Config }) => Promise; wsEndpoint: string; cdpEndpoint: string; server: TestServer; @@ -61,6 +64,11 @@ export const test = baseTest.extend({ args.push(`--browser=${mcpBrowser}`); if (options?.args) args.push(...options.args); + if (options?.config) { + const configFile = testInfo.outputPath('config.json'); + await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2)); + args.push(`--config=${configFile}`); + } const transport = new StdioClientTransport({ command: 'node', args: [path.join(__dirname, '../cli.js'), ...args], diff --git a/tests/screenshot.spec.ts b/tests/screenshot.spec.ts index e052569..5a94249 100644 --- a/tests/screenshot.spec.ts +++ b/tests/screenshot.spec.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { test, expect } from './fixtures'; test('browser_take_screenshot (viewport)', async ({ client }) => { @@ -70,3 +72,24 @@ test('browser_take_screenshot (element)', async ({ client }) => { ], }); }); + +test('browser_take_screenshot (outputDir)', async ({ startClient }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const client = await startClient({ + config: { outputDir }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: 'data:text/html,TitleHello, world!', + }, + })).toContainTextContent(`Navigate to data:text/html`); + + await client.callTool({ + name: 'browser_take_screenshot', + arguments: {}, + }); + + expect(fs.existsSync(outputDir)).toBeTruthy(); + expect([...fs.readdirSync(outputDir)]).toHaveLength(1); +});