chore: support custom filename in screenshot function (#349)

This commit is contained in:
おがどら 2025-05-09 03:04:18 +09:00 committed by GitHub
parent 09ba7989c3
commit 85c85bd2fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 116 additions and 10 deletions

View File

@ -341,6 +341,7 @@ X Y coordinate space, based on the provided screenshot.
- Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.
- Parameters:
- `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.
- `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.
- `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.
- `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.
- Read-only: **true**
@ -501,7 +502,8 @@ X Y coordinate space, based on the provided screenshot.
- **browser_pdf_save**
- Title: Save as PDF
- Description: Save page as PDF
- Parameters: None
- Parameters:
- `filename` (string, optional): File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.
- Read-only: **true**
### Utilities

View File

@ -20,6 +20,10 @@ import { defineTool } from './tool.js';
import * as javascript from '../javascript.js';
import { outputFile } from '../config.js';
const pdfSchema = z.object({
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
});
const pdf = defineTool({
capability: 'pdf',
@ -27,13 +31,13 @@ const pdf = defineTool({
name: 'browser_pdf_save',
title: 'Save as PDF',
description: 'Save page as PDF',
inputSchema: z.object({}),
inputSchema: pdfSchema,
type: 'readOnly',
},
handle: async context => {
handle: async (context, params) => {
const tab = context.currentTabOrDie();
const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.pdf`);
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
const code = [
`// Save page as ${fileName}`,

View File

@ -220,6 +220,7 @@ const selectOption = defineTool({
const screenshotSchema = z.object({
raw: z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
}).refine(data => {
@ -243,7 +244,7 @@ const screenshot = defineTool({
const tab = context.currentTabOrDie();
const snapshot = tab.snapshotOrDie();
const fileType = params.raw ? 'png' : 'jpeg';
const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.${fileType}`);
const fileName = await outputFile(context.config, params.filename ?? `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;

View File

@ -14,6 +14,8 @@
* limitations under the License.
*/
import fs from 'fs';
import { test, expect } from './fixtures.js';
test('save as pdf unavailable', async ({ startClient }) => {
@ -45,3 +47,38 @@ test('save as pdf', async ({ client, mcpBrowser }) => {
});
expect(response).toHaveTextContent(/Save page as.*page-[^:]+.pdf/);
});
test('save as pdf (filename: output.pdf)', async ({ startClient, mcpBrowser }, testInfo) => {
test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
const outputDir = testInfo.outputPath('output');
const client = await startClient({
config: { outputDir },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toContainTextContent(`- generic [ref=s1e2]: Hello, world!`);
expect(await client.callTool({
name: 'browser_pdf_save',
arguments: {
filename: 'output.pdf',
},
})).toEqual({
content: [
{
type: 'text',
text: expect.stringContaining(`output.pdf`),
},
],
});
const files = [...fs.readdirSync(outputDir)];
expect(fs.existsSync(outputDir)).toBeTruthy();
expect(files).toHaveLength(1);
expect(files[0]).toMatch(/^output.pdf$/);
});

View File

@ -94,9 +94,54 @@ test('--output-dir should work', async ({ startClient, localOutputPath }) => {
expect([...fs.readdirSync(outputDir)]).toHaveLength(1);
});
for (const raw of [undefined, true]) {
test(`browser_take_screenshot (raw: ${raw})`, async ({ startClient }, testInfo) => {
const ext = raw ? 'png' : 'jpeg';
const outputDir = testInfo.outputPath('output');
const client = await startClient({
config: { outputDir },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
},
})).toContainTextContent(`Navigate to data:text/html`);
test('browser_take_screenshot (outputDir)', async ({ startClient, localOutputPath }) => {
const outputDir = localOutputPath('output');
expect(await client.callTool({
name: 'browser_take_screenshot',
arguments: {
raw,
},
})).toEqual({
content: [
{
data: expect.any(String),
mimeType: `image/${ext}`,
type: 'image',
},
{
text: expect.stringMatching(
new RegExp(`page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}\\-\\d{3}Z\\.${ext}`)
),
type: 'text',
},
],
});
const files = [...fs.readdirSync(outputDir)];
expect(fs.existsSync(outputDir)).toBeTruthy();
expect(files).toHaveLength(1);
expect(files[0]).toMatch(
new RegExp(`^page-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z\\.${ext}$`)
);
});
}
test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const client = await startClient({
config: { outputDir },
});
@ -107,13 +152,30 @@ test('browser_take_screenshot (outputDir)', async ({ startClient, localOutputPat
},
})).toContainTextContent(`Navigate to data:text/html`);
await client.callTool({
expect(await client.callTool({
name: 'browser_take_screenshot',
arguments: {},
arguments: {
filename: 'output.jpeg',
},
})).toEqual({
content: [
{
data: expect.any(String),
mimeType: 'image/jpeg',
type: 'image',
},
{
text: expect.stringContaining(`output.jpeg`),
type: 'text',
},
],
});
const files = [...fs.readdirSync(outputDir)];
expect(fs.existsSync(outputDir)).toBeTruthy();
expect([...fs.readdirSync(outputDir)]).toHaveLength(1);
expect(files).toHaveLength(1);
expect(files[0]).toMatch(/^output.jpeg$/);
});
test('browser_take_screenshot (noImageResponses)', async ({ startClient }) => {