项目文件夹

文件
Coquinate 03e02a2d76 feat(screenshot): add WebP format support with quality parameter (#220)
## Summary

This PR adds WebP format support to the screenshot tool, providing
superior compression compared to JPEG while maintaining image quality.

## Problem

Currently, the Chrome DevTools MCP only supports PNG and JPEG formats
for screenshots. This leads to:
- Missing out on WebP's superior compression (25-34% better than JPEG at
equivalent quality)
- Larger file sizes than necessary for AI assistants with image size
limits
- No access to a modern format that offers both better compression and
transparency support
- Bug: `saveTemporaryFile` always saved files with `.png` extension
regardless of the specified format

## Solution

Added WebP as a supported format in the screenshot tool schema and
extended the quality parameter to work with WebP (0-100 range, same as
JPEG). Also fixed the file extension bug in `saveTemporaryFile`.

## Changes

-  Added `webp` to screenshot format enum alongside `png` and `jpeg`
-  Extended quality parameter description to include WebP support
-  Updated `saveTemporaryFile` to properly handle WebP MIME type and
file extension
-  Fixed bug where all screenshots were saved as `.png` regardless of
format
-  Updated type definitions in `ToolDefinition.ts` for WebP support
-  Updated documentation via `npm run docs`

## Testing

Puppeteer 24.22.3 (used by this project) has full WebP support including
quality parameter. Expected compression improvements based on WebP
benchmarks:
- **PNG (baseline):** 128 KB
- **JPEG quality 50:** 84 KB (34% reduction vs PNG)
- **WebP quality 50:** ~60 KB (53% reduction vs PNG, 29% better than
JPEG)
- **WebP quality 75:** ~90 KB (optimal quality/size balance)

All existing tests pass (131/131).

## Impact

This change is **backward compatible** - WebP is an optional format that
doesn't affect existing PNG/JPEG usage. It particularly helps with:
- AI assistants that have image size limits
- Reducing bandwidth when capturing many screenshots  
- Providing transparency support with better compression than PNG
- Offering a modern, universally-supported image format (Chrome,
Firefox, Safari, Edge)

## Example Usage

```javascript
// High quality WebP
await take_screenshot({
  format: 'webp',
  quality: 85,
  fullPage: false
})

// Optimized for size
await take_screenshot({
  format: 'webp',
  quality: 50,  // ~29% smaller than JPEG quality 50
  fullPage: true
})
```

## Checklist

- [x] Code follows conventional commits
- [x] Documentation updated with `npm run docs`
- [x] All tests passing (131/131)
- [x] Backward compatible
- [x] Bug fix included (file extension handling)
- [ ] CLA signed (will complete if needed)

Fixes #[issue-number] (if applicable)

---------

Co-authored-by: aberemia24 <aberemia@gmail.com>
Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
2025-10-01 10:11:04 +02:00

234 行
7.4 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {rm, stat, mkdir, chmod, writeFile} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {describe, it} from 'node:test';
import {screenshot} from '../../src/tools/screenshot.js';
import {screenshots} from '../snapshot.js';
import {withBrowser} from '../utils.js';
describe('screenshot', () => {
describe('browser_take_screenshot', () => {
it('with default options', async () => {
await withBrowser(async (response, context) => {
const fixture = screenshots.basic;
const page = context.getSelectedPage();
await page.setContent(fixture.html);
await screenshot.handler({params: {format: 'png'}}, response, context);
assert.equal(response.images.length, 1);
assert.equal(response.images[0].mimeType, 'image/png');
assert.equal(
response.responseLines.at(0),
"Took a screenshot of the current page's viewport.",
);
});
});
it('with jpeg', async () => {
await withBrowser(async (response, context) => {
await screenshot.handler({params: {format: 'jpeg'}}, response, context);
assert.equal(response.images.length, 1);
assert.equal(response.images[0].mimeType, 'image/jpeg');
assert.equal(
response.responseLines.at(0),
"Took a screenshot of the current page's viewport.",
);
});
});
it('with webp', async () => {
await withBrowser(async (response, context) => {
await screenshot.handler({params: {format: 'webp'}}, response, context);
assert.equal(response.images.length, 1);
assert.equal(response.images[0].mimeType, 'image/webp');
assert.equal(
response.responseLines.at(0),
"Took a screenshot of the current page's viewport.",
);
});
});
it('with full page', async () => {
await withBrowser(async (response, context) => {
const fixture = screenshots.viewportOverflow;
const page = context.getSelectedPage();
await page.setContent(fixture.html);
await screenshot.handler(
{params: {format: 'png', fullPage: true}},
response,
context,
);
assert.equal(response.images.length, 1);
assert.equal(response.images[0].mimeType, 'image/png');
assert.equal(
response.responseLines.at(0),
'Took a screenshot of the full current page.',
);
});
});
it('with full page resulting in a large screenshot', async () => {
await withBrowser(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(
`<div style="color:blue;">test</div>`.repeat(7_000),
);
await screenshot.handler(
{params: {format: 'png', fullPage: true}},
response,
context,
);
assert.equal(response.images.length, 0);
assert.equal(
response.responseLines.at(0),
'Took a screenshot of the full current page.',
);
assert.ok(
response.responseLines.at(1)?.match(/Saved screenshot to.*\.png/),
);
});
});
it('with element uid', async () => {
await withBrowser(async (response, context) => {
const fixture = screenshots.button;
const page = context.getSelectedPage();
await page.setContent(fixture.html);
await context.createTextSnapshot();
await screenshot.handler(
{
params: {
format: 'png',
uid: '1_1',
},
},
response,
context,
);
assert.equal(response.images.length, 1);
assert.equal(response.images[0].mimeType, 'image/png');
assert.equal(
response.responseLines.at(0),
'Took a screenshot of node with uid "1_1".',
);
});
});
it('with filePath', async () => {
await withBrowser(async (response, context) => {
const filePath = join(tmpdir(), 'test-screenshot.png');
try {
const fixture = screenshots.basic;
const page = context.getSelectedPage();
await page.setContent(fixture.html);
await screenshot.handler(
{params: {format: 'png', filePath}},
response,
context,
);
assert.equal(response.images.length, 0);
assert.equal(
response.responseLines.at(0),
"Took a screenshot of the current page's viewport.",
);
assert.equal(
response.responseLines.at(1),
`Saved screenshot to ${filePath}.`,
);
const stats = await stat(filePath);
assert.ok(stats.isFile());
assert.ok(stats.size > 0);
} finally {
await rm(filePath, {force: true});
}
});
});
it('with unwritable filePath', async () => {
if (process.platform === 'win32') {
const filePath = join(
tmpdir(),
'readonly-file-for-screenshot-test.png',
);
// Create the file and make it read-only.
await writeFile(filePath, '');
await chmod(filePath, 0o400);
try {
await withBrowser(async (response, context) => {
const fixture = screenshots.basic;
const page = context.getSelectedPage();
await page.setContent(fixture.html);
await assert.rejects(
screenshot.handler(
{params: {format: 'png', filePath}},
response,
context,
),
);
});
} finally {
// Make the file writable again so it can be deleted.
await chmod(filePath, 0o600);
await rm(filePath, {force: true});
}
} else {
const dir = join(tmpdir(), 'readonly-dir-for-screenshot-test');
await mkdir(dir, {recursive: true});
await chmod(dir, 0o500);
const filePath = join(dir, 'test-screenshot.png');
try {
await withBrowser(async (response, context) => {
const fixture = screenshots.basic;
const page = context.getSelectedPage();
await page.setContent(fixture.html);
await assert.rejects(
screenshot.handler(
{params: {format: 'png', filePath}},
response,
context,
),
);
});
} finally {
await chmod(dir, 0o700);
await rm(dir, {recursive: true, force: true});
}
}
});
it('with malformed filePath', async () => {
await withBrowser(async (response, context) => {
// Use a platform-specific invalid character.
// On Windows, characters like '<', '>', ':', '"', '/', '\', '|', '?', '*' are invalid.
// On POSIX, the null byte is invalid.
const invalidChar = process.platform === 'win32' ? '>' : '\0';
const filePath = `malformed${invalidChar}path.png`;
const fixture = screenshots.basic;
const page = context.getSelectedPage();
await page.setContent(fixture.html);
await assert.rejects(
screenshot.handler(
{params: {format: 'png', filePath}},
response,
context,
),
);
});
});
});
});