From 1eee30fd45530927aedbb79e7621ec950c595690 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:56:43 -0700 Subject: [PATCH] feat: add fullPage mode to browser_take_screenshot (#704) --- README.md | 1 + src/tools/screenshot.ts | 17 ++++++++++++-- tests/screenshot.spec.ts | 49 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5731962..0448f12 100644 --- a/README.md +++ b/README.md @@ -534,6 +534,7 @@ http.createServer(async (req, res) => { - `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. + - `fullPage` (boolean, optional): When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots. - Read-only: **true** diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index 5e41491..5160f2e 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -28,11 +28,17 @@ const screenshotSchema = z.object({ 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.'), + fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.'), }).refine(data => { return !!data.element === !!data.ref; }, { message: 'Both element and ref must be provided or neither.', path: ['ref', 'element'] +}).refine(data => { + return !(data.fullPage && (data.element || data.ref)); +}, { + message: 'fullPage cannot be used with element screenshots.', + path: ['fullPage'] }); const screenshot = defineTool({ @@ -50,11 +56,18 @@ const screenshot = defineTool({ const snapshot = tab.snapshotOrDie(); const fileType = params.raw ? 'png' : 'jpeg'; 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 options: playwright.PageScreenshotOptions = { + type: fileType, + quality: fileType === 'png' ? undefined : 50, + scale: 'css', + path: fileName, + ...(params.fullPage !== undefined && { fullPage: params.fullPage }) + }; const isElementScreenshot = params.element && params.ref; + const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport'); const code = [ - `// Screenshot ${isElementScreenshot ? params.element : 'viewport'} and save it as ${fileName}`, + `// Screenshot ${screenshotTarget} and save it as ${fileName}`, ]; const locator = params.ref ? snapshot.refLocator({ element: params.element || '', ref: params.ref }) : null; diff --git a/tests/screenshot.spec.ts b/tests/screenshot.spec.ts index 9a63098..78dc09d 100644 --- a/tests/screenshot.spec.ts +++ b/tests/screenshot.spec.ts @@ -201,3 +201,52 @@ test('browser_take_screenshot (imageResponses=omit)', async ({ startClient, serv ], }); }); + +test('browser_take_screenshot (fullPage: true)', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toContainTextContent(`Navigate to http://localhost`); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + arguments: { fullPage: true }, + })).toEqual({ + content: [ + { + data: expect.any(String), + mimeType: 'image/jpeg', + type: 'image', + }, + { + text: expect.stringContaining(`Screenshot full page and save it as`), + type: 'text', + }, + ], + }); +}); + +test('browser_take_screenshot (fullPage with element should error)', async ({ startClient, server }, testInfo) => { + const { client } = await startClient({ + config: { outputDir: testInfo.outputPath('output') }, + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toContainTextContent(`[ref=e1]`); + + const result = await client.callTool({ + name: 'browser_take_screenshot', + arguments: { + fullPage: true, + element: 'hello button', + ref: 'e1', + }, + }); + + expect(result.isError).toBe(true); + expect(result.content?.[0]?.text).toContain('fullPage cannot be used with element screenshots'); +});