From a9989852d548ddbff99c2b7c734cf074e4c07784 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 29 Mar 2022 18:48:13 -0600 Subject: [PATCH] feat(vrt): new option `"caret"` for taking screenshots (#13164) This has two values: - `"hide"` to hide input caret for taking screenshot - `"initial"` to keep caret behavior unchanged Defaults to `"hide"`. Fixes #12643 --- docs/src/api/class-pageassertions.md | 2 ++ docs/src/api/params.md | 6 ++++ .../playwright-core/src/protocol/channels.ts | 6 ++++ .../playwright-core/src/protocol/protocol.yml | 5 +++ .../playwright-core/src/protocol/validator.ts | 3 ++ .../src/server/screenshotter.ts | 25 ++++++++------- packages/playwright-core/types/types.d.ts | 18 +++++++++++ packages/playwright-test/types/test.d.ts | 5 +++ tests/page/page-screenshot.spec.ts | 31 ++++++++++++++++++- utils/generate_types/overrides-test.d.ts | 5 +++ 10 files changed, 94 insertions(+), 12 deletions(-) diff --git a/docs/src/api/class-pageassertions.md b/docs/src/api/class-pageassertions.md index 953c284724..375d2c2a8d 100644 --- a/docs/src/api/class-pageassertions.md +++ b/docs/src/api/class-pageassertions.md @@ -140,6 +140,8 @@ await expect(page).toHaveScreenshot(); ### option: PageAssertions.toHaveScreenshot.mask = %%-screenshot-option-mask-%% +### option: PageAssertions.toHaveScreenshot.caret = %%-screenshot-option-caret-%% + ### option: PageAssertions.toHaveScreenshot.maxDiffPixels = %%-assertions-max-diff-pixels-%% ### option: PageAssertions.toHaveScreenshot.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%% diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 9e8e861c69..28ee059d79 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -964,6 +964,11 @@ When set to `"css"`, screenshot will have a single pixel per each css pixel on t 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-option-caret +- `caret` <[ScreenshotCaret]<"hide"|"initial">> + +When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, text caret behavior will not be changed. Defaults to `"hide"`. + ## screenshot-options-common-list - %%-screenshot-option-animations-%% - %%-screenshot-option-omit-background-%% @@ -971,6 +976,7 @@ When set to `"ready"`, screenshot will wait for [`document.fonts.ready`](https:/ - %%-screenshot-option-path-%% - %%-screenshot-option-size-%% - %%-screenshot-option-fonts-%% +- %%-screenshot-option-caret-%% - %%-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 87341a81c7..9cf89a9a56 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -1517,6 +1517,7 @@ export type PageExpectScreenshotParams = { fullPage?: boolean, clip?: Rect, omitBackground?: boolean, + caret?: 'hide' | 'initial', animations?: 'disabled' | 'allow', size?: 'css' | 'device', fonts?: 'ready' | 'nowait', @@ -1542,6 +1543,7 @@ export type PageExpectScreenshotOptions = { fullPage?: boolean, clip?: Rect, omitBackground?: boolean, + caret?: 'hide' | 'initial', animations?: 'disabled' | 'allow', size?: 'css' | 'device', fonts?: 'ready' | 'nowait', @@ -1565,6 +1567,7 @@ export type PageScreenshotParams = { fullPage?: boolean, clip?: Rect, omitBackground?: boolean, + caret?: 'hide' | 'initial', animations?: 'disabled' | 'allow', size?: 'css' | 'device', fonts?: 'ready' | 'nowait', @@ -1580,6 +1583,7 @@ export type PageScreenshotOptions = { fullPage?: boolean, clip?: Rect, omitBackground?: boolean, + caret?: 'hide' | 'initial', animations?: 'disabled' | 'allow', size?: 'css' | 'device', fonts?: 'ready' | 'nowait', @@ -2899,6 +2903,7 @@ export type ElementHandleScreenshotParams = { type?: 'png' | 'jpeg', quality?: number, omitBackground?: boolean, + caret?: 'hide' | 'initial', animations?: 'disabled' | 'allow', size?: 'css' | 'device', fonts?: 'ready' | 'nowait', @@ -2912,6 +2917,7 @@ export type ElementHandleScreenshotOptions = { type?: 'png' | 'jpeg', quality?: number, omitBackground?: boolean, + caret?: 'hide' | 'initial', animations?: 'disabled' | 'allow', size?: 'css' | 'device', fonts?: 'ready' | 'nowait', diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index 98adfb568e..494a248e4b 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -313,6 +313,11 @@ CommonScreenshotOptions: type: mixin properties: omitBackground: boolean? + caret: + type: enum? + literals: + - hide + - initial animations: type: enum? literals: diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index c8d4b5f8bb..902eb04d5e 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -561,6 +561,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { fullPage: tOptional(tBoolean), clip: tOptional(tType('Rect')), omitBackground: tOptional(tBoolean), + caret: tOptional(tEnum(['hide', 'initial'])), animations: tOptional(tEnum(['disabled', 'allow'])), size: tOptional(tEnum(['css', 'device'])), fonts: tOptional(tEnum(['ready', 'nowait'])), @@ -577,6 +578,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { fullPage: tOptional(tBoolean), clip: tOptional(tType('Rect')), omitBackground: tOptional(tBoolean), + caret: tOptional(tEnum(['hide', 'initial'])), animations: tOptional(tEnum(['disabled', 'allow'])), size: tOptional(tEnum(['css', 'device'])), fonts: tOptional(tEnum(['ready', 'nowait'])), @@ -1081,6 +1083,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { type: tOptional(tEnum(['png', 'jpeg'])), quality: tOptional(tNumber), omitBackground: tOptional(tBoolean), + caret: tOptional(tEnum(['hide', 'initial'])), animations: tOptional(tEnum(['disabled', 'allow'])), size: tOptional(tEnum(['css', 'device'])), fonts: tOptional(tEnum(['ready', 'nowait'])), diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index 3c8fc5c37e..b711181326 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -42,6 +42,7 @@ export type ScreenshotOptions = { clip?: Rect, size?: 'css' | 'device', fonts?: 'ready' | 'nowait', + caret?: 'hide' | 'initial', }; export class Screenshotter { @@ -86,7 +87,7 @@ export class Screenshotter { return this._queue.postTask(async () => { progress.log('taking page screenshot'); const { viewportSize } = await this._originalViewportSize(progress); - await this._preparePageForScreenshot(progress, options.animations === 'disabled', options.fonts === 'ready'); + await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled', options.fonts === 'ready'); progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup. if (options.fullPage) { @@ -115,7 +116,7 @@ export class Screenshotter { progress.log('taking element screenshot'); const { viewportSize } = await this._originalViewportSize(progress); - await this._preparePageForScreenshot(progress, options.animations === 'disabled', options.fonts === 'ready'); + await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled', options.fonts === 'ready'); progress.throwIfAborted(); // Do not do extra work. await handle._waitAndScrollIntoViewIfNeeded(progress); @@ -139,20 +140,22 @@ export class Screenshotter { }); } - async _preparePageForScreenshot(progress: Progress, disableAnimations: boolean, waitForFonts: boolean) { + async _preparePageForScreenshot(progress: Progress, hideCaret: boolean, disableAnimations: boolean, waitForFonts: boolean) { if (disableAnimations) progress.log(' disabled all CSS animations'); if (waitForFonts) progress.log(' waiting for fonts to load...'); await Promise.all(this._page.frames().map(async frame => { - await frame.nonStallingEvaluateInExistingContext('(' + (async function(disableAnimations: boolean, waitForFonts: boolean) { + await frame.nonStallingEvaluateInExistingContext('(' + (async function(hideCaret: boolean, disableAnimations: boolean, waitForFonts: boolean) { const styleTag = document.createElement('style'); - styleTag.textContent = ` - *:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) { - caret-color: transparent !important; - } - `; - document.documentElement.append(styleTag); + if (hideCaret) { + styleTag.textContent = ` + *:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) { + caret-color: transparent !important; + } + `; + document.documentElement.append(styleTag); + } const infiniteAnimationsToResume: Set = new Set(); const cleanupCallbacks: (() => void)[] = []; @@ -222,7 +225,7 @@ export class Screenshotter { if (waitForFonts) await document.fonts.ready; - }).toString() + `)(${disableAnimations}, ${waitForFonts})`, false, 'utility').catch(() => {}); + }).toString() + `)(${hideCaret}, ${disableAnimations}, ${waitForFonts})`, false, 'utility').catch(() => {}); })); if (waitForFonts) progress.log(' fonts in all frames are loaded'); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 9716fc0275..cbcfc9c53d 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8146,6 +8146,12 @@ export interface ElementHandle extends JSHandle { */ animations?: "disabled"|"allow"; + /** + * When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, text caret behavior will not be changed. + * Defaults to `"hide"`. + */ + caret?: "hide"|"initial"; + /** * 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 @@ -15731,6 +15737,12 @@ export interface LocatorScreenshotOptions { */ animations?: "disabled"|"allow"; + /** + * When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, text caret behavior will not be changed. + * Defaults to `"hide"`. + */ + caret?: "hide"|"initial"; + /** * 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 @@ -15884,6 +15896,12 @@ export interface PageScreenshotOptions { */ animations?: "disabled"|"allow"; + /** + * When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, text caret behavior will not be changed. + * Defaults to `"hide"`. + */ + caret?: "hide"|"initial"; + /** * An object which specifies clipping of the resulting image. Should have the following fields: */ diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index cbccd8b95f..39854de6e4 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -76,6 +76,11 @@ type ExpectSettings = { * high-dpi devices will be twice as large or even larger. Defaults to `"css"`. */ size?: 'css'|'device', + /** + * When set to `"hide"`, screenshot will hide text caret. + * When set to `"initial"`, text caret behavior will not be changed. Defaults to `"hide"`. + */ + caret?: 'hide'|'initia', } toMatchSnapshot?: { /** An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax). Defaults to `0.2`. diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index a3415deb9c..83de3e8e20 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -33,7 +33,7 @@ it.describe('page screenshot', () => { expect(screenshot).toMatchSnapshot('screenshot-sanity.png'); }); - it('should not capture blinking caret', async ({ page, server }) => { + it('should not capture blinking caret by default', async ({ page, server }) => { await page.setContent(` + + + +
+ `); + const div = page.locator('div'); + await div.type('foo bar'); + const screenshot = await div.screenshot(); + let hasDifferentScreenshots = false; + for (let i = 0; !hasDifferentScreenshots && i < 10; ++i) { + // Caret blinking time is set to 500ms. + // Try to capture variety of screenshots to make + // sure we capture blinking caret. + await new Promise(x => setTimeout(x, 150)); + const newScreenshot = await div.screenshot({ caret: 'initial' }); + hasDifferentScreenshots = !newScreenshot.equals(screenshot); + } + expect(hasDifferentScreenshots).toBe(true); + }); + it('should clip rect', async ({ page, server }) => { await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/grid.html'); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 5bd0072bc7..fe73a37ff6 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -75,6 +75,11 @@ type ExpectSettings = { * high-dpi devices will be twice as large or even larger. Defaults to `"css"`. */ size?: 'css'|'device', + /** + * When set to `"hide"`, screenshot will hide text caret. + * When set to `"initial"`, text caret behavior will not be changed. Defaults to `"hide"`. + */ + caret?: 'hide'|'initia', } toMatchSnapshot?: { /** An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax). Defaults to `0.2`.