diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 0996b703b2..40bdadc118 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -955,7 +955,12 @@ An object which specifies clipping of the resulting image. Should have the follo ## 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`. +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-option-fonts +- `fonts` <[ScreenshotFonts]<"ready"|"nowait">> + +When set to `"ready"`, screenshot will wait for [`document.fonts.ready`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) promise to resolve in all frames. Defaults to `"nowait"`. ## screenshot-options-common-list - %%-screenshot-option-animations-%% @@ -963,6 +968,7 @@ When set to `css`, screenshot will have a single pixel per each css pixel on the - %%-screenshot-option-quality-%% - %%-screenshot-option-path-%% - %%-screenshot-option-size-%% +- %%-screenshot-option-fonts-%% - %%-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 72bc6131f6..8aa1283c20 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -1550,6 +1550,7 @@ export type PageScreenshotParams = { animations?: 'disabled', clip?: Rect, size?: 'css' | 'device', + fonts?: 'ready' | 'nowait', mask?: { frame: FrameChannel, selector: string, @@ -1564,6 +1565,7 @@ export type PageScreenshotOptions = { animations?: 'disabled', clip?: Rect, size?: 'css' | 'device', + fonts?: 'ready' | 'nowait', mask?: { frame: FrameChannel, selector: string, @@ -2864,6 +2866,7 @@ export type ElementHandleScreenshotParams = { omitBackground?: boolean, animations?: 'disabled', size?: 'css' | 'device', + fonts?: 'ready' | 'nowait', mask?: { frame: FrameChannel, selector: string, @@ -2876,6 +2879,7 @@ export type ElementHandleScreenshotOptions = { omitBackground?: boolean, animations?: 'disabled', size?: 'css' | 'device', + fonts?: 'ready' | 'nowait', mask?: { frame: FrameChannel, selector: string, diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index cad670edef..b586aa3089 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -1055,6 +1055,11 @@ Page: literals: - css - device + fonts: + type: enum? + literals: + - ready + - nowait mask: type: array? items: @@ -2224,6 +2229,11 @@ ElementHandle: literals: - css - device + fonts: + type: enum? + literals: + - ready + - nowait mask: type: array? items: diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 5940749d96..ce6b49c974 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -573,6 +573,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { animations: tOptional(tEnum(['disabled'])), clip: tOptional(tType('Rect')), size: tOptional(tEnum(['css', 'device'])), + fonts: tOptional(tEnum(['ready', 'nowait'])), mask: tOptional(tArray(tObject({ frame: tChannel('Frame'), selector: tString, @@ -1068,6 +1069,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { omitBackground: tOptional(tBoolean), animations: tOptional(tEnum(['disabled'])), size: tOptional(tEnum(['css', 'device'])), + fonts: tOptional(tEnum(['ready', 'nowait'])), mask: tOptional(tArray(tObject({ frame: tChannel('Frame'), selector: tString, diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index 8508f990f1..40b7f96bd2 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -41,6 +41,7 @@ export type ScreenshotOptions = { fullPage?: boolean, clip?: Rect, size?: 'css' | 'device', + fonts?: 'ready' | 'nowait', }; export class Screenshotter { @@ -84,7 +85,7 @@ export class Screenshotter { const format = validateScreenshotOptions(options); return this._queue.postTask(async () => { const { viewportSize } = await this._originalViewportSize(progress); - await this._preparePageForScreenshot(progress, options.animations === 'disabled'); + await this._preparePageForScreenshot(progress, options.animations === 'disabled', options.fonts === 'ready'); progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup. if (options.fullPage) { @@ -112,7 +113,7 @@ export class Screenshotter { return this._queue.postTask(async () => { const { viewportSize } = await this._originalViewportSize(progress); - await this._preparePageForScreenshot(progress, options.animations === 'disabled'); + await this._preparePageForScreenshot(progress, options.animations === 'disabled', options.fonts === 'ready'); progress.throwIfAborted(); // Do not do extra work. await handle._waitAndScrollIntoViewIfNeeded(progress); @@ -136,9 +137,9 @@ export class Screenshotter { }); } - async _preparePageForScreenshot(progress: Progress, disableAnimations: boolean) { + async _preparePageForScreenshot(progress: Progress, disableAnimations: boolean, waitForFonts: boolean) { await Promise.all(this._page.frames().map(async frame => { - await frame.nonStallingEvaluateInExistingContext('(' + (function(disableAnimations: boolean) { + await frame.nonStallingEvaluateInExistingContext('(' + (async function(disableAnimations: boolean, waitForFonts: boolean) { const styleTag = document.createElement('style'); styleTag.textContent = ` *:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) { @@ -212,7 +213,10 @@ export class Screenshotter { cleanupCallback(); delete window.__cleanupScreenshot; }; - }).toString() + `)(${disableAnimations || false})`, false, 'utility').catch(() => {}); + + if (waitForFonts) + await document.fonts.ready; + }).toString() + `)(${disableAnimations}, ${waitForFonts})`, false, 'utility').catch(() => {}); })); progress.cleanupWhenAborted(() => this._restorePageAfterScreenshot()); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 128c5c37a4..c711f1605c 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8072,6 +8072,13 @@ export interface ElementHandle extends JSHandle { */ animations?: "disabled"; + /** + * When set to `"ready"`, screenshot will wait for + * [`document.fonts.ready()`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) promise to resolve in all + * frames. Defaults to `"nowait"`. + */ + fonts?: "ready"|"nowait"; + /** * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with a pink box * `#FF00FF` that completely covers its bounding box. @@ -8097,9 +8104,9 @@ 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`. + * 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"; @@ -15593,6 +15600,13 @@ export interface LocatorScreenshotOptions { */ animations?: "disabled"; + /** + * When set to `"ready"`, screenshot will wait for + * [`document.fonts.ready()`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) promise to resolve in all + * frames. Defaults to `"nowait"`. + */ + fonts?: "ready"|"nowait"; + /** * Specify locators that should be masked when the screenshot is taken. Masked elements will be overlayed with a pink box * `#FF00FF` that completely covers its bounding box. @@ -15618,9 +15632,9 @@ 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`. + * 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"; @@ -15762,6 +15776,13 @@ export interface PageScreenshotOptions { height: number; }; + /** + * When set to `"ready"`, screenshot will wait for + * [`document.fonts.ready()`](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready) promise to resolve in all + * frames. Defaults to `"nowait"`. + */ + fonts?: "ready"|"nowait"; + /** * When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to * `false`. @@ -15793,9 +15814,9 @@ 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`. + * 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"; diff --git a/tests/assets/webfont/README.md b/tests/assets/webfont/README.md new file mode 100644 index 0000000000..5bcee9ad4d --- /dev/null +++ b/tests/assets/webfont/README.md @@ -0,0 +1,3 @@ +This icon font was generated: +- using SVG icons from https://github.com/primer/octicons +- bundling icons into webfonts using https://github.com/fontello/fontello diff --git a/tests/assets/webfont/iconfont.svg b/tests/assets/webfont/iconfont.svg new file mode 100644 index 0000000000..9d2a4ca9c2 --- /dev/null +++ b/tests/assets/webfont/iconfont.svg @@ -0,0 +1,14 @@ + + + +Copyright (C) 2022 by original authors @ fontello.com + + + + + + + + + + diff --git a/tests/assets/webfont/iconfont.woff2 b/tests/assets/webfont/iconfont.woff2 new file mode 100644 index 0000000000..ceba03549a Binary files /dev/null and b/tests/assets/webfont/iconfont.woff2 differ diff --git a/tests/assets/webfont/webfont.html b/tests/assets/webfont/webfont.html new file mode 100644 index 0000000000..5167594324 --- /dev/null +++ b/tests/assets/webfont/webfont.html @@ -0,0 +1,18 @@ + + + ++- + diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index 9d8d7eca7b..9aefce0dc8 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -710,5 +710,27 @@ it.describe('page screenshot animations', () => { 'onfinish', 'animationend' ]); }); + + it('should respect fonts option', async ({ page, server }) => { + await page.setViewportSize({ width: 500, height: 500 }); + let serverRequest, serverResponse; + // Stall font loading. + server.setRoute('/webfont/iconfont.woff2', (req, res) => { + serverRequest = req; + serverResponse = res; + }); + await page.goto(server.PREFIX + '/webfont/webfont.html', { + waitUntil: 'domcontentloaded', // 'load' will not happen if webfont is pending + }); + // Make sure we can take screenshot. + const noIconsScreenshot = await page.screenshot(); + const [iconsScreenshot] = await Promise.all([ + page.screenshot({ fonts: 'ready' }), + server.serveFile(serverRequest, serverResponse), + ]); + expect(iconsScreenshot).toMatchSnapshot('screenshot-web-font.png'); + expect(noIconsScreenshot).not.toMatchSnapshot('screenshot-web-font.png'); + }); + }); diff --git a/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-chromium.png b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-chromium.png new file mode 100644 index 0000000000..40aa6d483c Binary files /dev/null and b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-chromium.png differ diff --git a/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-firefox.png b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-firefox.png new file mode 100644 index 0000000000..11c8e0f230 Binary files /dev/null and b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-firefox.png differ diff --git a/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-webkit.png b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-webkit.png new file mode 100644 index 0000000000..6b0c93238f Binary files /dev/null and b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-webkit.png differ