Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -1313,7 +1313,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
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const ignores = [
"examples",
"packages/*/lib/",
"packages/playwright-core/bundles/utils/src/third_party/",
"packages/utils/webp/webp_codec.js",
"packages/playwright-core/src/generated/*",
"packages/playwright-core/src/third_party/",
"packages/playwright-core/types/*",
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 @@ -13218,7 +13218,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 @@ -25150,7 +25150,7 @@ export interface LocatorScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

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

type Devices = {
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
!lib/**/*.ttf
!lib/**/*.json
!lib/**/*.md
# Include the WebP codec WASM binary (see @utils/webp/webp).
!lib/**/*.wasm
!lib/xdg-open
!lib/**/manifest.webmanifest
# Exclude injected files. A preprocessed version of these is included via lib/generated.
Expand Down
10 changes: 6 additions & 4 deletions packages/playwright-core/src/client/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3334,7 +3334,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 @@ -3348,7 +3348,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 @@ -4194,6 +4194,7 @@ export type PageExpectScreenshotParams = {
threshold?: number,
fullPage?: boolean,
clip?: Rect,
type?: 'png' | 'jpeg' | 'webp',
omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow',
Expand All @@ -4217,6 +4218,7 @@ export type PageExpectScreenshotOptions = {
threshold?: number,
fullPage?: boolean,
clip?: Rect,
type?: 'png' | 'jpeg' | 'webp',
omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow',
Expand All @@ -4241,7 +4243,7 @@ export type PageExpectScreenshotErrorDetails = {
};
export type PageScreenshotParams = {
timeout: number,
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
fullPage?: boolean,
clip?: Rect,
Expand All @@ -4257,7 +4259,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
5 changes: 3 additions & 2 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1898,7 +1898,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 @@ -2438,6 +2438,7 @@ scheme.PageExpectScreenshotParams = tObject({
threshold: tOptional(tFloat),
fullPage: tOptional(tBoolean),
clip: tOptional(tType('Rect')),
type: tOptional(tEnum(['png', 'jpeg', 'webp'])),
omitBackground: tOptional(tBoolean),
caret: tOptional(tEnum(['hide', 'initial'])),
animations: tOptional(tEnum(['disabled', 'allow'])),
Expand All @@ -2462,7 +2463,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
10 changes: 6 additions & 4 deletions packages/playwright-core/src/server/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3337,7 +3337,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 @@ -3351,7 +3351,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 @@ -4197,6 +4197,7 @@ export type PageExpectScreenshotParams = {
threshold?: number,
fullPage?: boolean,
clip?: Rect,
type?: 'png' | 'jpeg' | 'webp',
omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow',
Expand All @@ -4220,6 +4221,7 @@ export type PageExpectScreenshotOptions = {
threshold?: number,
fullPage?: boolean,
clip?: Rect,
type?: 'png' | 'jpeg' | 'webp',
omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow',
Expand All @@ -4244,7 +4246,7 @@ export type PageExpectScreenshotErrorDetails = {
};
export type PageScreenshotParams = {
timeout: number,
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
fullPage?: boolean,
clip?: Rect,
Expand All @@ -4260,7 +4262,7 @@ export type PageScreenshotParams = {
style?: string,
};
export type PageScreenshotOptions = {
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
fullPage?: boolean,
clip?: Rect,
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
6 changes: 2 additions & 4 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,8 @@ export class Page extends SdkObject<PageEventMap> {
return await this.screenshotter.screenshotPage(progress, options || {});
};

const comparator = getComparator('image/png');
const format = validateScreenshotOptions(options || {});
const comparator = getComparator(`image/${format}`);
let intermediateResult: {
actual?: Buffer,
previous?: Buffer,
Expand All @@ -738,9 +739,6 @@ export class Page extends SdkObject<PageEventMap> {
try {
if (!options.expected && options.isNot)
throw new Error('"not" matcher requires expected result');
const format = validateScreenshotOptions(options || {});
if (format !== 'png')
throw new Error('Only PNG screenshots are supported');
let actual: Buffer | undefined;
let previous: Buffer | undefined;
const pollIntervals = [0, 100, 250, 500];
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;
Comment thread
yury-s marked this conversation as resolved.
Outdated
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
3 changes: 3 additions & 0 deletions packages/playwright-core/src/server/webkit/webview/wvPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ManualPromise } from '@isomorphic/manualPromise';
import { splitErrorMessage } from '@utils/stackTrace';
import { debugLogger } from '@utils/debugLogger';
import { eventsHelper } from '@utils/eventsHelper';
import { encodeWebp } from '@utils/webp/webp';
import * as dialog from '../../dialog';
import * as dom from '../../dom';
import { TargetClosedError } from '../../errors';
Expand Down Expand Up @@ -769,6 +770,8 @@ export class WVPage implements PageDelegate {
let buffer: Buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
if (format === 'jpeg')
buffer = jpegjs.encode(PNG.sync.read(buffer), quality).data;
else if (format === 'webp')
buffer = encodeWebp(PNG.sync.read(buffer), { quality: quality ?? 80 });
return buffer;
}

Expand Down
14 changes: 12 additions & 2 deletions packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
* limitations under the License.
*/

import { PNG } from 'pngjs';

import { headersArrayToObject, headersObjectToArray } from '@isomorphic/headers';
import { splitErrorMessage } from '@utils/stackTrace';
import { eventsHelper } from '@utils/eventsHelper';
import { hostPlatform } from '@utils/hostPlatform';
import { encodeWebp } from '@utils/webp/webp';
import { assert } from '@isomorphic/assert';
import * as dialog from '../dialog';
import * as dom from '../dom';
Expand Down Expand Up @@ -871,9 +874,16 @@ export class WKPage implements PageDelegate {
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 }));
// WebKit on macOS has no built-in WebP encoder, so capture a PNG and re-encode it.
const recodePngToWebp = format === 'webp' && process.platform === 'darwin';
const result = await progress.race(this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor, format: (recodePngToWebp ? 'png' : format) as 'png' | 'jpeg' | 'webp', quality: recodePngToWebp ? undefined : quality }));
// Strip the 'data:image/<format>;base64,' prefix.
return Buffer.from(result.dataURL.substring(result.dataURL.indexOf(',') + 1), 'base64');
const buffer = Buffer.from(result.dataURL.substring(result.dataURL.indexOf(',') + 1), 'base64');
if (recodePngToWebp) {
const png = PNG.sync.read(buffer);
return encodeWebp({ width: png.width, height: png.height, data: png.data }, { quality: quality ?? 80 });
}
return buffer;
}

async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
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
Loading
Loading