From 49e66c7f08c192f8b79664f8ccb032eeaf665b84 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Thu, 10 Mar 2022 17:54:36 -0700 Subject: [PATCH] feat(screenshot): introduce new "fonts" option for screenshots (#12661) This option will wait for webfonts to load before taking screenshots. --- docs/src/api/params.md | 8 +++- .../playwright-core/src/protocol/channels.ts | 4 ++ .../playwright-core/src/protocol/protocol.yml | 10 +++++ .../playwright-core/src/protocol/validator.ts | 2 + .../src/server/screenshotter.ts | 14 ++++--- packages/playwright-core/types/types.d.ts | 39 ++++++++++++++---- tests/assets/webfont/README.md | 3 ++ tests/assets/webfont/iconfont.svg | 14 +++++++ tests/assets/webfont/iconfont.woff2 | Bin 0 -> 2656 bytes tests/assets/webfont/webfont.html | 18 ++++++++ tests/page/page-screenshot.spec.ts | 22 ++++++++++ .../screenshot-web-font-chromium.png | Bin 0 -> 2898 bytes .../screenshot-web-font-firefox.png | Bin 0 -> 6277 bytes .../screenshot-web-font-webkit.png | Bin 0 -> 6410 bytes 14 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 tests/assets/webfont/README.md create mode 100644 tests/assets/webfont/iconfont.svg create mode 100644 tests/assets/webfont/iconfont.woff2 create mode 100644 tests/assets/webfont/webfont.html create mode 100644 tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-chromium.png create mode 100644 tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-firefox.png create mode 100644 tests/page/page-screenshot.spec.ts-snapshots/screenshot-web-font-webkit.png 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 0000000000000000000000000000000000000000..ceba03549a59bd18dea3d36df96a2438540c9c50 GIT binary patch literal 2656 zcmV-m3ZM0NPew8T0RR910199L4*&oF02hz|0162J0RR9100000000000000000000 z0000SR0d!Gg9Zo=37iZO2nvJ%gb51>00A}vBm)ctAO(d@2R00W92eRJdEd>ugcKqIOq} z9QL2)O8eg=DUndb!lN*CZf>%(-9O18f+~SW1xS*>wMGE>6S7LP^*x4L9>c#?8co@( zWFJvKBm#J8Wf&D*v|Mz#U<$-c`@ZLo86I)#t$7 z;g^@cX%htsOB0X#n(2j;d`*CmUN=9>7X2pA0g+%uU_m2DCSG=kImYk==V-jK7{WH* zNKga_&CkQops~i|Oo7!*O=DJ`<}{5KPK&#@o= zN^~yM_*OO=H@>30{&CBYwAc2#Z1P6k+tw1zb$FyJk*TDoVBO?7Ac~|9I&8;FkT1Y;+uraB5sS$`;4T3UDuLQENQM* z6i1cScgL=ZBuGv+R?~Cd3^h9csSsOhzhkPmsl0LZT~3yL7LkX9MAszxIIb3O0S_-s z^t3U#-?+!32`pArYlF0rgsU0wKw5|_u74Y832qaSl=-n`uIY`um^>50MLh3~hJ4+P z$j{ipy(JnD<3)?npEcBU49l%iiL*?kwvx~;^yk|jOXj-K)NTNJrIe*g^G?2Hj!Ey) zo8+DOeBKAL(Nxb7lX)J<0g@!L`3!E8$hMftnZrGDvIDsoyESrC{Wj7b{h3@Ajk|Ox zxnvKMhvy$sGo5)pNoLo!0{KgMZ`1SlPPFrmmA`ZW8Z)zxp}gYuNRH&j3`BWs*%Uc9 zM@kdg-=rqOf*~p6ZQW5$j88a+oJ5_uN2Aj=||1}UoiDp}Yr)7$oJi!oN73LYBCtzal}CoZ4Jfg&`D3xVRH z5GoOhN`X?e@Lbd`MRL&7zlDE7TLe6hbGqB ze@D&1HnL+C^N{?*QP8bwsDa`IDEv^=LQw}rJroU4G(yn?MKd%v4E4rJE!7JpNi&=E zX6>0HiEwuE#^WRBI_cB$TXp>Y+b_JEz!vIdSi+m&c*HY*g1bU9bh z@2H!;i*q}n8(OD#VH=EcbhzCkQ4htsE#-Fvh(55}tuqwWJSyE#ua)jeK>UURJ?IYf zfnK?6glpON+J~m3Jf%)6u!!=f-Tnds`MDOYkmsy|XS2~*j!-gbtUOOy}X18la48Ur>amti)R*Q`g0 z6ijsl!dVykO0>&i3UXku8CH>mRV7(}d-cSjSiGXp64V~KdJ~4-k^~z0hVYm=_2wOF zg^^C-h8u1B+_0y2TLLZEYe&~7%9y!Fj6|y8gZ=>vPTIJ^_AQCP)HUEZFDYhvy@)+okd#f4+195ab)xEZG?3JsA&5l-OS{O^&HT|m5 z%PJWb%dL#FzNuGdw|D&=UxeoE^9ZFm{}ZbYtg54~!uIX$w)0&j-e(*k5EhujHSQ9O<{t^BMNCH>v5q-H#}gUa zAx_vsIB5_0rzRMs(-1^(Msw+`BXlm2;a%apJ&X(XFfQ7|xTJwDJ3?0+;j1ul1lQ~l zT(?JX!yds+4Rp&9y6s5ro#4B^JGE6(cKN$Tjqe}x*Ob@9KtkD&P&@|)3rPpu23SMvS#n*g3=H7Wj#3xr(@p`x1-tcQU z>*B`WX5HER$E-`6evdhLX=3iIFc*2|Hmq~&sC(A+;@kDI_I0LtS(&+IxtWh24=!5u z`q|mxiT7G*VfKhtaEGEpQ#+kiJTqzPiL=}C%4ON^_{N$ z#om}ZI?r8Pzke41^H24cx)EP(WC^&%ja)=4kJ6rtUa@gwyJsvBBT9=(;&Hh=aXd6C z0e)bvwUreMB0b#=^%bp^t$u&L-=F8RD*}-oTNGG|87!9=u}l)tTH=W41X}dkKyy)H zTe!I^(CJhcHWf9M7P{?{00Id7|6(z*GkfqKceb7b_-jq=J478V%`r3&1I9lk;26gYnBsb1I~&?7CtsC zvT?I>k(U6;d;7@eF&Z%*V3wD9i-a7>UZlv9YS!R{*nl&%$f8JAvJK9}A}^cd$UZ*m z<(Gf*oif(s{OR3|Pe$t(WliL5(CUeK^@L>*^cR%}o#0SEOPSFYZe8s5rn8zyjwjoX z9-V%!F?^9Q_+2#m8J=#3dThoxz(G!Nm>H7n<|q!0jM0QcJFQu9I}YO{-Qj@3ooCoj zg1tB-IL1+SGx-SN2;phu{S2P6Bo-ea%tSHrJ{GUDl9L>>cA4O?pyzqsG*-L!W>|;s z&_x{UrU*L OX|-JQJ!{e?8U+Ay83p73 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..40aa6d483cf9c98500b16abc78c82856acff0e26 GIT binary patch literal 2898 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 literal 0 HcmV?d00001