fix: downscale viewport screenshots when no viewport is emulated (#2380)

Fixes #2379.

`getSourceBox()` bails when `page.viewport()` is null, which is the
normal case – the browser is launched and connected with
`defaultViewport: null`, and only `emulate` sets a viewport. So
`--screenshot-max-width`/`--screenshot-max-height` never produced a clip
for a plain viewport screenshot.

This falls back to the window's own `innerWidth`/`innerHeight`. That's
the area a viewport screenshot already captures, so the clip only
applies `scale` and the framing is unchanged. Full page and element
shots keep their existing branches.

## Testing

Added `downscales viewport screenshot when no viewport is emulated`,
which omits the `setViewport()` call the existing downscale tests make –
it fails on `main` (`expected: 100, actual: 1200`, the window width) and
passes here. Full `tests/tools/screenshot.test.ts` is green.
This commit is contained in:
Thomas Bachem
2026-07-22 21:26:11 +02:00
committed by GitHub
parent 75c7048c80
commit 39c4140f11
2 changed files with 49 additions and 2 deletions
+12 -2
View File
@@ -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(
+37
View File
@@ -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`<div style="width:100vw;height:100vh;background:red"></div>`,
);
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,