diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 755e57b567..0996b703b2 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -952,11 +952,17 @@ When true, takes a screenshot of the full scrollable page, instead of the curren An object which specifies clipping of the resulting image. Should have the following fields: +## screenshot-option-size +- `size` <[ScreenshotSize]<"css"|"device">> + +When set to `css`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will keep screenshots small. Using `device` option will produce a single pixel per each device pixel, so screenhots of high-dpi devices will be twice as large or even larger. Defaults to `device`. + ## screenshot-options-common-list - %%-screenshot-option-animations-%% - %%-screenshot-option-omit-background-%% - %%-screenshot-option-quality-%% - %%-screenshot-option-path-%% +- %%-screenshot-option-size-%% - %%-screenshot-option-type-%% - %%-screenshot-option-mask-%% - %%-input-timeout-%% diff --git a/packages/playwright-core/src/protocol/channels.ts b/packages/playwright-core/src/protocol/channels.ts index 80a413ebd5..72bc6131f6 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -1549,6 +1549,7 @@ export type PageScreenshotParams = { fullPage?: boolean, animations?: 'disabled', clip?: Rect, + size?: 'css' | 'device', mask?: { frame: FrameChannel, selector: string, @@ -1562,6 +1563,7 @@ export type PageScreenshotOptions = { fullPage?: boolean, animations?: 'disabled', clip?: Rect, + size?: 'css' | 'device', mask?: { frame: FrameChannel, selector: string, @@ -2861,6 +2863,7 @@ export type ElementHandleScreenshotParams = { quality?: number, omitBackground?: boolean, animations?: 'disabled', + size?: 'css' | 'device', mask?: { frame: FrameChannel, selector: string, @@ -2872,6 +2875,7 @@ export type ElementHandleScreenshotOptions = { quality?: number, omitBackground?: boolean, animations?: 'disabled', + size?: 'css' | 'device', mask?: { frame: FrameChannel, selector: string, diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index c50c36f529..cad670edef 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -1050,6 +1050,11 @@ Page: literals: - disabled clip: Rect? + size: + type: enum? + literals: + - css + - device mask: type: array? items: @@ -2214,6 +2219,11 @@ ElementHandle: type: enum? literals: - disabled + size: + type: enum? + literals: + - css + - device mask: type: array? items: diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 997a5a7e1b..5940749d96 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -572,6 +572,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { fullPage: tOptional(tBoolean), animations: tOptional(tEnum(['disabled'])), clip: tOptional(tType('Rect')), + size: tOptional(tEnum(['css', 'device'])), mask: tOptional(tArray(tObject({ frame: tChannel('Frame'), selector: tString, @@ -1066,6 +1067,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { quality: tOptional(tNumber), omitBackground: tOptional(tBoolean), animations: tOptional(tEnum(['disabled'])), + size: tOptional(tEnum(['css', 'device'])), mask: tOptional(tArray(tObject({ frame: tChannel('Frame'), selector: tString, diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 2309075cf4..301ad2b847 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -248,7 +248,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 | undefined): Promise { + async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, size: 'css' | 'device'): Promise { const { visualViewport } = await this._mainFrameSession._client.send('Page.getLayoutMetrics'); if (!documentRect) { documentRect = { @@ -263,6 +263,10 @@ export class CRPage implements PageDelegate { // When taking screenshots with documentRect (based on the page content, not viewport), // ignore current page scale. const clip = { ...documentRect, scale: viewportRect ? visualViewport.scale : 1 }; + if (size === 'css') { + const deviceScaleFactor = this._browserContext._options.deviceScaleFactor || 1; + clip.scale /= deviceScaleFactor; + } progress.throwIfAborted(); const result = await this._mainFrameSession._client.send('Page.captureScreenshot', { format, quality, clip, captureBeyondViewport: !fitsViewport }); return Buffer.from(result.data, 'base64'); diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 53f4073027..6eba2ebc64 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -408,7 +408,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): Promise { + async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, size: 'css' | 'device'): Promise { if (!documentRect) { const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY })); documentRect = { @@ -424,6 +424,7 @@ export class FFPage implements PageDelegate { const { data } = await this._session.send('Page.screenshot', { mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'), clip: documentRect, + omitDeviceScaleFactor: size === 'css', }); return Buffer.from(data, 'base64'); } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 2f7ded5460..ab65c54b42 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -61,7 +61,7 @@ export interface PageDelegate { bringToFront(): Promise; setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise; - takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean | undefined): Promise; + takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, size: 'css' | 'device'): Promise; isElementHandle(remoteObject: any): boolean; adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise>; diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index 351baf5db4..8508f990f1 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -40,6 +40,7 @@ export type ScreenshotOptions = { mask?: { frame: Frame, selector: string}[], fullPage?: boolean, clip?: Rect, + size?: 'css' | 'device', }; export class Screenshotter { @@ -237,7 +238,7 @@ export class Screenshotter { progress.cleanupWhenAborted(() => this._page.hideHighlight()); } - private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean | undefined, options: ScreenshotOptions): Promise { + private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean, options: ScreenshotOptions): Promise { if ((options as any).__testHookBeforeScreenshot) await (options as any).__testHookBeforeScreenshot(); progress.throwIfAborted(); // Screenshotting is expensive - avoid extra work. @@ -251,7 +252,7 @@ export class Screenshotter { await this._maskElements(progress, options); progress.throwIfAborted(); // Avoid extra work. - const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality, fitsViewport); + const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality, fitsViewport, options.size || 'device'); progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup. await this._page.hideHighlight(); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 242b685fe2..ca2888baa3 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -811,9 +811,9 @@ export class WKPage implements PageDelegate { this._recordingVideoFile = null; } - async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { + async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, size: 'css' | 'device'): Promise { const rect = (documentRect || viewportRect)!; - const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport' }); + const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor: size === 'css' }); const prefix = 'data:image/png;base64,'; let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64'); if (format === 'jpeg') diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index c4669c0d75..b94f61e4d9 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8097,6 +8097,13 @@ export interface ElementHandle extends JSHandle { */ quality?: number; + /** + * When set to `css`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will + * keep screenshots small. Using `device` option will produce a single pixel per each device pixel, so screenhots of + * high-dpi devices will be twice as large or even larger. Defaults to `device`. + */ + size?: "css"|"device"; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the @@ -15611,6 +15618,13 @@ export interface LocatorScreenshotOptions { */ quality?: number; + /** + * When set to `css`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will + * keep screenshots small. Using `device` option will produce a single pixel per each device pixel, so screenhots of + * high-dpi devices will be twice as large or even larger. Defaults to `device`. + */ + size?: "css"|"device"; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the @@ -15779,6 +15793,13 @@ export interface PageScreenshotOptions { */ quality?: number; + /** + * When set to `css`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will + * keep screenshots small. Using `device` option will produce a single pixel per each device pixel, so screenhots of + * high-dpi devices will be twice as large or even larger. Defaults to `device`. + */ + size?: "css"|"device"; + /** * Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by * using the diff --git a/tests/screenshot.spec.ts b/tests/screenshot.spec.ts index ee07521468..64943ed5a2 100644 --- a/tests/screenshot.spec.ts +++ b/tests/screenshot.spec.ts @@ -41,7 +41,6 @@ browserTest.describe('page screenshot', () => { browserTest('should work with a mobile viewport', async ({ browser, server, browserName }) => { browserTest.skip(browserName === 'firefox'); - browserTest.fixme(browserName === 'chromium'); const context = await browser.newContext({ viewport: { width: 320, height: 480 }, isMobile: true }); const page = await context.newPage(); @@ -53,7 +52,6 @@ browserTest.describe('page screenshot', () => { browserTest('should work with a mobile viewport and clip', async ({ browser, server, browserName, channel }) => { browserTest.skip(browserName === 'firefox'); - browserTest.skip(!!channel, 'Different result in stable/beta'); const context = await browser.newContext({ viewport: { width: 320, height: 480 }, isMobile: true }); const page = await context.newPage(); @@ -83,6 +81,33 @@ browserTest.describe('page screenshot', () => { await context.close(); }); + browserTest('should work with device scale factor and clip', async ({ browser, server }) => { + const context = await browser.newContext({ viewport: { width: 500, height: 500 }, deviceScaleFactor: 3 }); + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ clip: { x: 50, y: 100, width: 150, height: 100 } }); + expect(screenshot).toMatchSnapshot('screenshot-device-scale-factor-clip.png'); + await context.close(); + }); + + browserTest('should work with device scale factor and size:css', async ({ browser, server }) => { + const context = await browser.newContext({ viewport: { width: 320, height: 480 }, deviceScaleFactor: 2 }); + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ size: 'css' }); + expect(screenshot).toMatchSnapshot('screenshot-device-scale-factor-css-size.png'); + await context.close(); + }); + + browserTest('should work with device scale factor, clip and size:css', async ({ browser, server }) => { + const context = await browser.newContext({ viewport: { width: 500, height: 500 }, deviceScaleFactor: 3 }); + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ clip: { x: 50, y: 100, width: 150, height: 100 }, size: 'css' }); + expect(screenshot).toMatchSnapshot('screenshot-device-scale-factor-clip-css-size.png'); + await context.close(); + }); + browserTest('should work with large size', async ({ browserName, headless, platform, contextFactory }) => { browserTest.fixme(browserName === 'chromium' && !headless && platform === 'linux', 'Chromium has gpu problems on linux with large screnshots'); browserTest.slow(true, 'Large screenshot is slow'); @@ -345,4 +370,54 @@ browserTest.describe('element screenshot', () => { await page.close(); } }); + + browserTest('should capture full element when larger than viewport with device scale factor', async ({ browser }) => { + const context = await browser.newContext({ viewport: { width: 501, height: 501 }, deviceScaleFactor: 2.5 }); + const page = await context.newPage(); + await page.setContent(` +
oooo
+ +
+ `); + const screenshot = await page.locator('div.to-screenshot').screenshot(); + expect(screenshot).toMatchSnapshot('element-larger-than-viewport-dsf.png'); + await context.close(); + }); + + browserTest('should capture full element when larger than viewport with device scale factor and size:css', async ({ browser }) => { + const context = await browser.newContext({ viewport: { width: 501, height: 501 }, deviceScaleFactor: 2.5 }); + const page = await context.newPage(); + await page.setContent(` +
oooo
+ +
+ `); + const screenshot = await page.locator('div.to-screenshot').screenshot({ size: 'css' }); + expect(screenshot).toMatchSnapshot('element-larger-than-viewport-dsf-css-size.png'); + await context.close(); + }); }); diff --git a/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-chromium.png b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-chromium.png new file mode 100644 index 0000000000..61d43989b8 Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-chromium.png differ diff --git a/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-chromium.png b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-chromium.png new file mode 100644 index 0000000000..4d9757b190 Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-chromium.png differ diff --git a/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-firefox.png b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-firefox.png new file mode 100644 index 0000000000..4e40340a90 Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-firefox.png differ diff --git a/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-webkit.png b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-webkit.png new file mode 100644 index 0000000000..9b2018042d Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-css-size-webkit.png differ diff --git a/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-firefox.png b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-firefox.png new file mode 100644 index 0000000000..382dea1fe0 Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-firefox.png differ diff --git a/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-webkit.png b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-webkit.png new file mode 100644 index 0000000000..fb834069ce Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/element-larger-than-viewport-dsf-webkit.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-chromium.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-chromium.png new file mode 100644 index 0000000000..4810c1e38f Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-chromium.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-chromium.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-chromium.png new file mode 100644 index 0000000000..8c4046910b Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-chromium.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-firefox.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-firefox.png new file mode 100644 index 0000000000..4cf53bde05 Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-firefox.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-webkit.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-webkit.png new file mode 100644 index 0000000000..6d0a713e29 Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-css-size-webkit.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-firefox.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-firefox.png new file mode 100644 index 0000000000..38d7edb27b Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-firefox.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-webkit.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-webkit.png new file mode 100644 index 0000000000..647b28d7cc Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-clip-webkit.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-chromium.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-chromium.png new file mode 100644 index 0000000000..1b9f31e408 Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-chromium.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-firefox.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-firefox.png new file mode 100644 index 0000000000..15cd79d89b Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-firefox.png differ diff --git a/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-webkit.png b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-webkit.png new file mode 100644 index 0000000000..d0107b63be Binary files /dev/null and b/tests/screenshot.spec.ts-snapshots/screenshot-device-scale-factor-css-size-webkit.png differ