Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -1295,7 +1295,7 @@ relative path, then it is resolved relative to the current working directory. If
saved to the disk.

## screenshot-option-type
- `type` <[ScreenshotType]<"png"|"jpeg">>
- `type` <[ScreenshotType]<"png"|"jpeg"|"webp">>

Specify screenshot type, defaults to `png`.

Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12503,7 +12503,7 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}): Promise<Buffer>;

/**
Expand Down Expand Up @@ -23863,7 +23863,7 @@ export interface LocatorScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

interface ElementHandleWaitForSelectorOptions {
Expand Down Expand Up @@ -24536,7 +24536,7 @@ export interface PageScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

type Devices = {
Expand Down
8 changes: 4 additions & 4 deletions packages/playwright-core/src/client/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3397,7 +3397,7 @@ export type ElementHandleQuerySelectorAllResult = {
};
export type ElementHandleScreenshotParams = {
timeout: number,
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
omitBackground?: boolean,
caret?: 'hide' | 'initial',
Expand All @@ -3411,7 +3411,7 @@ export type ElementHandleScreenshotParams = {
style?: string,
};
export type ElementHandleScreenshotOptions = {
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
omitBackground?: boolean,
caret?: 'hide' | 'initial',
Expand Down Expand Up @@ -4362,7 +4362,7 @@ export type PageExpectScreenshotErrorDetails = {
};
export type PageScreenshotParams = {
timeout: number,
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
fullPage?: boolean,
clip?: Rect,
Expand All @@ -4378,7 +4378,7 @@ export type PageScreenshotParams = {
style?: string,
};
export type PageScreenshotOptions = {
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
fullPage?: boolean,
clip?: Rect,
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/client/elementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,15 @@ export async function convertInputFiles(files: string | FilePayload | string[] |
return { payloads };
}

export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' | 'webp' }): 'png' | 'jpeg' | 'webp' | undefined {
if (options.path) {
const mimeType = getMimeTypeForPath(options.path);
if (mimeType === 'image/png')
return 'png';
else if (mimeType === 'image/jpeg')
return 'jpeg';
else if (mimeType === 'image/webp')
return 'webp';
throw new Error(`path: unsupported mime type "${mimeType}"`);
}
return options.type;
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1884,7 +1884,7 @@ scheme.ElementHandleQuerySelectorAllResult = tObject({
});
scheme.ElementHandleScreenshotParams = tObject({
timeout: tFloat,
type: tOptional(tEnum(['png', 'jpeg'])),
type: tOptional(tEnum(['png', 'jpeg', 'webp'])),
quality: tOptional(tInt),
omitBackground: tOptional(tBoolean),
caret: tOptional(tEnum(['hide', 'initial'])),
Expand Down Expand Up @@ -2448,7 +2448,7 @@ scheme.PageExpectScreenshotErrorDetails = tObject({
});
scheme.PageScreenshotParams = tObject({
timeout: tFloat,
type: tOptional(tEnum(['png', 'jpeg'])),
type: tOptional(tEnum(['png', 'jpeg', 'webp'])),
quality: tOptional(tInt),
fullPage: tOptional(tBoolean),
clip: tOptional(tType('Rect')),
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/bidi/bidiPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,8 @@ export class BidiPage implements PageDelegate {
}

async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
if (format === 'webp')
throw new Error('webp screenshots are not supported via WebDriver BiDi');
const rect = (documentRect || viewportRect)!;
const { data } = await progress.race(this._session.send('browsingContext.captureScreenshot', {
context: this._session.sessionId,
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export class CRPage implements PageDelegate {
await this._mainFrameSession._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
}

async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
async takeScreenshot(progress: Progress, format: 'png' | 'jpeg' | 'webp', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
const { visualViewport, contentSize, cssContentSize } = await progress.race(this._mainFrameSession._client.send('Page.getLayoutMetrics'));
if (!documentRect) {
documentRect = {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/firefox/ffPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ export class FFPage implements PageDelegate {
throw new Error('Not implemented');
}

async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
async takeScreenshot(progress: Progress, format: 'png' | 'jpeg' | 'webp', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
if (!documentRect) {
const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY }));
documentRect = {
Expand All @@ -470,7 +470,7 @@ export class FFPage implements PageDelegate {
};
}
const { data } = await progress.race(this._session.send('Page.screenshot', {
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg' | 'image/webp'),
clip: documentRect,
quality,
omitDeviceScaleFactor: scale === 'css',
Expand Down
14 changes: 7 additions & 7 deletions packages/playwright-core/src/server/screenshotter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ declare global {
}

export type ScreenshotOptions = {
type?: 'png' | 'jpeg';
type?: 'png' | 'jpeg' | 'webp';
quality?: number;
omitBackground?: boolean;
animations?: 'disabled' | 'allow';
Expand Down Expand Up @@ -297,7 +297,7 @@ export class Screenshotter {
}
}

private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean, options: ScreenshotOptions): Promise<Buffer> {
private async _screenshot(progress: Progress, format: 'png' | 'jpeg' | 'webp', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean, options: ScreenshotOptions): Promise<Buffer> {
if ((options as any).__testHookBeforeScreenshot)
await progress.race((options as any).__testHookBeforeScreenshot());

Expand All @@ -307,7 +307,7 @@ export class Screenshotter {
const cleanupHighlight = await this._maskElements(progress, options);

try {
const quality = format === 'jpeg' ? options.quality ?? 80 : undefined;
const quality = format === 'jpeg' ? options.quality ?? 80 : format === 'webp' ? options.quality : undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No default quality for webp?

const buffer = await this._page.delegate.takeScreenshot(progress, format, documentRect, viewportRect, quality, fitsViewport, options.scale || 'device');
await progress.race(cleanupHighlight());
if (shouldSetDefaultBackground)
Expand Down Expand Up @@ -353,20 +353,20 @@ function trimClipToSize(clip: types.Rect, size: types.Size): types.Rect {
return result;
}

export function validateScreenshotOptions(options: ScreenshotOptions): 'png' | 'jpeg' {
let format: 'png' | 'jpeg' | null = null;
export function validateScreenshotOptions(options: ScreenshotOptions): 'png' | 'jpeg' | 'webp' {
let format: 'png' | 'jpeg' | 'webp' | null = null;
// options.type takes precedence over inferring the type from options.path
// because it may be a 0-length file with no extension created beforehand (i.e. as a temp file).
if (options.type) {
assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
assert(options.type === 'png' || options.type === 'jpeg' || options.type === 'webp', 'Unknown options.type value: ' + options.type);
format = options.type;
}

if (!format)
format = 'png';

if (options.quality !== undefined) {
assert(format === 'jpeg', 'options.quality is unsupported for the ' + format + ' screenshots');
assert(format !== 'png', 'options.quality is unsupported for the ' + format + ' screenshots');
assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality));
assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality);
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,11 +867,13 @@ export class WKPage implements PageDelegate {
}

async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
if (format === 'webp' && process.platform === 'darwin')
throw new Error('webp screenshots are not supported in WebKit on macOS');
const rect = (documentRect || viewportRect)!;
const omitDeviceScaleFactor = scale === 'css';
this.validateScreenshotDimension(rect.width, omitDeviceScaleFactor);
this.validateScreenshotDimension(rect.height, omitDeviceScaleFactor);
const result = await progress.race(this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor, format: format as 'png' | 'jpeg', quality }));
const result = await progress.race(this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor, format: format as 'png' | 'jpeg' | 'webp', quality }));
// Strip the 'data:image/<format>;base64,' prefix.
return Buffer.from(result.dataURL.substring(result.dataURL.indexOf(',') + 1), 'base64');
}
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/tools/backend/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class Response {
readonly toolName: string;
readonly toolArgs: Record<string, any>;
private _clientWorkspace: string;
private _imageResults: { data: Buffer, imageType: 'png' | 'jpeg' }[] = [];
private _imageResults: { data: Buffer, imageType: 'png' | 'jpeg' | 'webp' }[] = [];
private _raw: boolean;
private _json: boolean;
private _writtenFiles = new Set<string>();
Expand Down Expand Up @@ -127,7 +127,7 @@ export class Response {
this.addTextResult(`- [${title}](${relativeName})`);
}

async registerImageResult(data: Buffer, imageType: 'png' | 'jpeg') {
async registerImageResult(data: Buffer, imageType: 'png' | 'jpeg' | 'webp') {
this._imageResults.push({ data, imageType });
}

Expand Down Expand Up @@ -209,7 +209,7 @@ export class Response {
if (this._context.config.imageResponses !== 'omit') {
for (const imageResult of this._imageResults) {
const scaledData = scaleImageToFitMessage(imageResult.data, imageResult.imageType);
content.push({ type: 'image', data: scaledData.toString('base64'), mimeType: imageResult.imageType === 'png' ? 'image/png' : 'image/jpeg' });
content.push({ type: 'image', data: scaledData.toString('base64'), mimeType: `image/${imageResult.imageType}` });
}
}

Expand Down
30 changes: 25 additions & 5 deletions packages/playwright-core/src/tools/backend/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import path from 'path';

import jpegjs from 'jpeg-js';
import { PNG } from 'pngjs';
import * as z from 'zod';
Expand All @@ -25,13 +27,27 @@ import { optionalElementSchema } from './snapshot';

import type * as playwright from '../../..';

type ImageFormat = 'png' | 'jpeg' | 'webp';

const screenshotSchema = optionalElementSchema.extend({
type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory.'),
type: z.enum(['png', 'jpeg', 'webp']).optional().describe('Image format for the screenshot. If unset, inferred from the filename extension, otherwise png.'),
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg|webp}` if not specified. Prefer relative file names to stay within the output directory.'),
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.'),
scale: z.enum(['css', 'device']).default('css').describe('Image resolution scale. "css" produces a screenshot sized in CSS pixels (smaller, consistent across devices). "device" produces a high-resolution screenshot using device pixels (larger, accounts for the device pixel ratio). Default is css.'),
});

function inferTypeFromFilename(filename: string | undefined): ImageFormat | undefined {
if (!filename)
return undefined;
switch (path.extname(filename).toLowerCase()) {
case '.png': return 'png';
case '.jpg':
case '.jpeg': return 'jpeg';
case '.webp': return 'webp';
}
return undefined;
}

const screenshot = defineTabTool({
capability: 'core',
schema: {
Expand All @@ -46,10 +62,10 @@ const screenshot = defineTabTool({
if (params.fullPage && params.target)
throw new Error('fullPage cannot be used with element screenshots.');

const fileType = params.type || 'png';
const fileType: ImageFormat = params.type ?? inferTypeFromFilename(params.filename) ?? 'png';
const options: playwright.PageScreenshotOptions = {
type: fileType,
quality: fileType === 'png' ? undefined : 90,
quality: fileType === 'jpeg' ? 90 : undefined,
scale: params.scale,
...tab.actionTimeoutOptions,
...(params.fullPage !== undefined && { fullPage: params.fullPage })
Expand All @@ -73,10 +89,14 @@ const screenshot = defineTabTool({
}
});

export function scaleImageToFitMessage(buffer: Buffer, imageType: 'png' | 'jpeg'): Buffer {
export function scaleImageToFitMessage(buffer: Buffer, imageType: 'png' | 'jpeg' | 'webp'): Buffer {
// https://docs.claude.com/en/docs/build-with-claude/vision#evaluate-image-size
// Not more than 1.15 megapixel, linear size not more than 1568.

// No Node-side webp decoder is bundled, so we can't scale webp screenshots.
if (imageType === 'webp')
return buffer;

const image = imageType === 'png' ? PNG.sync.read(buffer) : jpegjs.decode(buffer, { maxMemoryUsageInMB: 512 });
const pixels = image.width * image.height;

Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/tools/cli-daemon/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,12 +789,13 @@ const screenshot = declareCommand({
target: z.string().optional().describe(elementTargetDescription),
}),
options: z.object({
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg|webp}` if not specified.'),
type: z.enum(['png', 'jpeg', 'webp']).optional().describe('Image format. If unset, inferred from the filename extension, otherwise png.'),
['full-page']: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport.'),
hires: z.boolean().optional().describe('When true, captures a high-resolution screenshot using device pixels (accounts for the device pixel ratio), instead of CSS pixels.'),
}),
toolName: 'browser_take_screenshot',
toolParams: ({ target, filename, ['full-page']: fullPage, hires }) => ({ filename, target, fullPage, scale: hires ? 'device' : undefined }),
toolParams: ({ target, filename, type, ['full-page']: fullPage, hires }) => ({ filename, target, type, fullPage, scale: hires ? 'device' : undefined }),
});

const pdfSave = declareCommand({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ function generateCommandHelp(command: AnyCommandSchema) {
const optionsShape = (command.options as zodType.ZodObject<any>).shape;
for (const [name, schema] of Object.entries(optionsShape)) {
const zodSchema = schema as zodType.ZodTypeAny;
const description = (zodSchema.description ?? '').toLowerCase();
let description = (zodSchema.description ?? '').toLowerCase();
const unwrapped = unwrapZodType(zodSchema);
if (unwrapped instanceof z.ZodEnum)
description = `${description} (one of: ${unwrapped.options.join(', ')})`.trim();
lines.push(formatWithGap(` --${name}`, description));
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12503,7 +12503,7 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}): Promise<Buffer>;

/**
Expand Down Expand Up @@ -23863,7 +23863,7 @@ export interface LocatorScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

interface ElementHandleWaitForSelectorOptions {
Expand Down Expand Up @@ -24536,7 +24536,7 @@ export interface PageScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

type Devices = {
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/spec/handles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ ElementHandle:
literals:
- png
- jpeg
- webp
quality: int?
$mixin: CommonScreenshotOptions
returns:
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/spec/page.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Page:
literals:
- png
- jpeg
- webp
quality: int?
fullPage: boolean?
clip: Rect?
Expand Down
11 changes: 11 additions & 0 deletions tests/mcp/cli-save-as.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ test('screenshot --filename', async ({ cli, server, mcpBrowser }) => {
expect(attachments[0].data).toEqual(expect.any(Buffer));
});

test('screenshot --filename infers webp from extension', async ({ cli, server, mcpBrowser }) => {
test.skip(mcpBrowser === 'webkit' && process.platform === 'darwin', 'CG on macOS does not include a webp encoder UTI');

await cli('open', server.HELLO_WORLD);
const { output, attachments } = await cli('screenshot', '--filename=screenshot.webp');
expect(output).toContain('[Screenshot of viewport](./screenshot.webp)');
const buffer = attachments[0].data as Buffer;
expect(buffer.subarray(0, 4).toString('latin1')).toBe('RIFF');
expect(buffer.subarray(8, 12).toString('latin1')).toBe('WEBP');
});

test('pdf', async ({ cli, server, mcpBrowser }) => {
test.skip(mcpBrowser !== 'chromium' && mcpBrowser !== 'chrome', 'PDF is only supported in Chromium and Chrome');
await cli('open', server.HELLO_WORLD);
Expand Down
Loading
Loading