diff --git a/src/tools/screenshot.ts b/src/tools/screenshot.ts index 9edaa6f01..9523126be 100644 --- a/src/tools/screenshot.ts +++ b/src/tools/screenshot.ts @@ -43,10 +43,20 @@ async function getSourceBox( return {x: 0, y: 0, width: dims.width, height: dims.height}; } const viewport = page.viewport(); - if (!viewport) { + if (viewport) { + return {x: 0, y: 0, width: viewport.width, height: viewport.height}; + } + // The browser is launched and connected with `defaultViewport: null`, so + // `page.viewport()` stays null until something emulates one. Fall back to the + // window's own dimensions, which is the area a viewport screenshot captures. + const dims = await page.evaluate(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + if (dims.width <= 0 || dims.height <= 0) { return undefined; } - return {x: 0, y: 0, width: viewport.width, height: viewport.height}; + return {x: 0, y: 0, width: dims.width, height: dims.height}; } function computeDownscaleClip( diff --git a/tests/tools/screenshot.test.ts b/tests/tools/screenshot.test.ts index a55e13a63..89ab098de 100644 --- a/tests/tools/screenshot.test.ts +++ b/tests/tools/screenshot.test.ts @@ -345,6 +345,43 @@ describe('screenshot', () => { }); }); + it('downscales viewport screenshot when no viewport is emulated', async () => { + const tool = screenshot({ + screenshotMaxWidth: 100, + } as ParsedArguments); + await withMcpContext(async (response, context) => { + const page = context.getSelectedMcpPage().pptrPage; + // No setViewport call here: the browser is launched and connected with + // `defaultViewport: null`, so this is what a page looks like unless the + // emulate tool has set a viewport. + assert.equal(page.viewport(), null); + await page.setContent( + html`
`, + ); + const source = await page.evaluate(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + + await tool.handler( + {params: {format: 'png'}, page: context.getSelectedMcpPage()}, + response, + context, + ); + + assert.equal(response.images.length, 1); + const buf = Buffer.from(response.images[0].data, 'base64'); + assert.equal(pngWidth(buf), 100); + // The window size comes from the environment rather than an emulated + // viewport, so allow a pixel of rounding slack on the derived height. + const expectedHeight = Math.round(source.height * (100 / source.width)); + assert.ok( + Math.abs(pngHeight(buf) - expectedHeight) <= 1, + `expected height ~${expectedHeight}, got ${pngHeight(buf)}`, + ); + }); + }); + it('downscales using the smaller scale when both max-width and max-height are set', async () => { const tool = screenshot({ screenshotMaxWidth: 400,