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
This commit is contained in:
Andrey Lushnikov 2022-03-29 18:48:13 -06:00 committed by GitHub
parent 5e17ed137b
commit a9989852d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 94 additions and 12 deletions

View file

@ -140,6 +140,8 @@ await expect(page).toHaveScreenshot();
### option: PageAssertions.toHaveScreenshot.mask = %%-screenshot-option-mask-%% ### 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.maxDiffPixels = %%-assertions-max-diff-pixels-%%
### option: PageAssertions.toHaveScreenshot.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%% ### option: PageAssertions.toHaveScreenshot.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%%

View file

@ -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"`. 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-options-common-list
- %%-screenshot-option-animations-%% - %%-screenshot-option-animations-%%
- %%-screenshot-option-omit-background-%% - %%-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-path-%%
- %%-screenshot-option-size-%% - %%-screenshot-option-size-%%
- %%-screenshot-option-fonts-%% - %%-screenshot-option-fonts-%%
- %%-screenshot-option-caret-%%
- %%-screenshot-option-type-%% - %%-screenshot-option-type-%%
- %%-screenshot-option-mask-%% - %%-screenshot-option-mask-%%
- %%-input-timeout-%% - %%-input-timeout-%%

View file

@ -1517,6 +1517,7 @@ export type PageExpectScreenshotParams = {
fullPage?: boolean, fullPage?: boolean,
clip?: Rect, clip?: Rect,
omitBackground?: boolean, omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow', animations?: 'disabled' | 'allow',
size?: 'css' | 'device', size?: 'css' | 'device',
fonts?: 'ready' | 'nowait', fonts?: 'ready' | 'nowait',
@ -1542,6 +1543,7 @@ export type PageExpectScreenshotOptions = {
fullPage?: boolean, fullPage?: boolean,
clip?: Rect, clip?: Rect,
omitBackground?: boolean, omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow', animations?: 'disabled' | 'allow',
size?: 'css' | 'device', size?: 'css' | 'device',
fonts?: 'ready' | 'nowait', fonts?: 'ready' | 'nowait',
@ -1565,6 +1567,7 @@ export type PageScreenshotParams = {
fullPage?: boolean, fullPage?: boolean,
clip?: Rect, clip?: Rect,
omitBackground?: boolean, omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow', animations?: 'disabled' | 'allow',
size?: 'css' | 'device', size?: 'css' | 'device',
fonts?: 'ready' | 'nowait', fonts?: 'ready' | 'nowait',
@ -1580,6 +1583,7 @@ export type PageScreenshotOptions = {
fullPage?: boolean, fullPage?: boolean,
clip?: Rect, clip?: Rect,
omitBackground?: boolean, omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow', animations?: 'disabled' | 'allow',
size?: 'css' | 'device', size?: 'css' | 'device',
fonts?: 'ready' | 'nowait', fonts?: 'ready' | 'nowait',
@ -2899,6 +2903,7 @@ export type ElementHandleScreenshotParams = {
type?: 'png' | 'jpeg', type?: 'png' | 'jpeg',
quality?: number, quality?: number,
omitBackground?: boolean, omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow', animations?: 'disabled' | 'allow',
size?: 'css' | 'device', size?: 'css' | 'device',
fonts?: 'ready' | 'nowait', fonts?: 'ready' | 'nowait',
@ -2912,6 +2917,7 @@ export type ElementHandleScreenshotOptions = {
type?: 'png' | 'jpeg', type?: 'png' | 'jpeg',
quality?: number, quality?: number,
omitBackground?: boolean, omitBackground?: boolean,
caret?: 'hide' | 'initial',
animations?: 'disabled' | 'allow', animations?: 'disabled' | 'allow',
size?: 'css' | 'device', size?: 'css' | 'device',
fonts?: 'ready' | 'nowait', fonts?: 'ready' | 'nowait',

View file

@ -313,6 +313,11 @@ CommonScreenshotOptions:
type: mixin type: mixin
properties: properties:
omitBackground: boolean? omitBackground: boolean?
caret:
type: enum?
literals:
- hide
- initial
animations: animations:
type: enum? type: enum?
literals: literals:

View file

@ -561,6 +561,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
fullPage: tOptional(tBoolean), fullPage: tOptional(tBoolean),
clip: tOptional(tType('Rect')), clip: tOptional(tType('Rect')),
omitBackground: tOptional(tBoolean), omitBackground: tOptional(tBoolean),
caret: tOptional(tEnum(['hide', 'initial'])),
animations: tOptional(tEnum(['disabled', 'allow'])), animations: tOptional(tEnum(['disabled', 'allow'])),
size: tOptional(tEnum(['css', 'device'])), size: tOptional(tEnum(['css', 'device'])),
fonts: tOptional(tEnum(['ready', 'nowait'])), fonts: tOptional(tEnum(['ready', 'nowait'])),
@ -577,6 +578,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
fullPage: tOptional(tBoolean), fullPage: tOptional(tBoolean),
clip: tOptional(tType('Rect')), clip: tOptional(tType('Rect')),
omitBackground: tOptional(tBoolean), omitBackground: tOptional(tBoolean),
caret: tOptional(tEnum(['hide', 'initial'])),
animations: tOptional(tEnum(['disabled', 'allow'])), animations: tOptional(tEnum(['disabled', 'allow'])),
size: tOptional(tEnum(['css', 'device'])), size: tOptional(tEnum(['css', 'device'])),
fonts: tOptional(tEnum(['ready', 'nowait'])), fonts: tOptional(tEnum(['ready', 'nowait'])),
@ -1081,6 +1083,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
type: tOptional(tEnum(['png', 'jpeg'])), type: tOptional(tEnum(['png', 'jpeg'])),
quality: tOptional(tNumber), quality: tOptional(tNumber),
omitBackground: tOptional(tBoolean), omitBackground: tOptional(tBoolean),
caret: tOptional(tEnum(['hide', 'initial'])),
animations: tOptional(tEnum(['disabled', 'allow'])), animations: tOptional(tEnum(['disabled', 'allow'])),
size: tOptional(tEnum(['css', 'device'])), size: tOptional(tEnum(['css', 'device'])),
fonts: tOptional(tEnum(['ready', 'nowait'])), fonts: tOptional(tEnum(['ready', 'nowait'])),

View file

@ -42,6 +42,7 @@ export type ScreenshotOptions = {
clip?: Rect, clip?: Rect,
size?: 'css' | 'device', size?: 'css' | 'device',
fonts?: 'ready' | 'nowait', fonts?: 'ready' | 'nowait',
caret?: 'hide' | 'initial',
}; };
export class Screenshotter { export class Screenshotter {
@ -86,7 +87,7 @@ export class Screenshotter {
return this._queue.postTask(async () => { return this._queue.postTask(async () => {
progress.log('taking page screenshot'); progress.log('taking page screenshot');
const { viewportSize } = await this._originalViewportSize(progress); 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. progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
if (options.fullPage) { if (options.fullPage) {
@ -115,7 +116,7 @@ export class Screenshotter {
progress.log('taking element screenshot'); progress.log('taking element screenshot');
const { viewportSize } = await this._originalViewportSize(progress); 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. progress.throwIfAborted(); // Do not do extra work.
await handle._waitAndScrollIntoViewIfNeeded(progress); 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) if (disableAnimations)
progress.log(' disabled all CSS animations'); progress.log(' disabled all CSS animations');
if (waitForFonts) if (waitForFonts)
progress.log(' waiting for fonts to load...'); progress.log(' waiting for fonts to load...');
await Promise.all(this._page.frames().map(async frame => { 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'); const styleTag = document.createElement('style');
styleTag.textContent = ` if (hideCaret) {
*:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) { styleTag.textContent = `
caret-color: transparent !important; *:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) {
} caret-color: transparent !important;
`; }
document.documentElement.append(styleTag); `;
document.documentElement.append(styleTag);
}
const infiniteAnimationsToResume: Set<Animation> = new Set(); const infiniteAnimationsToResume: Set<Animation> = new Set();
const cleanupCallbacks: (() => void)[] = []; const cleanupCallbacks: (() => void)[] = [];
@ -222,7 +225,7 @@ export class Screenshotter {
if (waitForFonts) if (waitForFonts)
await document.fonts.ready; await document.fonts.ready;
}).toString() + `)(${disableAnimations}, ${waitForFonts})`, false, 'utility').catch(() => {}); }).toString() + `)(${hideCaret}, ${disableAnimations}, ${waitForFonts})`, false, 'utility').catch(() => {});
})); }));
if (waitForFonts) if (waitForFonts)
progress.log(' fonts in all frames are loaded'); progress.log(' fonts in all frames are loaded');

View file

@ -8146,6 +8146,12 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
*/ */
animations?: "disabled"|"allow"; 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 * 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 * [`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"; 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 * 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 * [`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"; 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: * An object which specifies clipping of the resulting image. Should have the following fields:
*/ */

View file

@ -76,6 +76,11 @@ type ExpectSettings = {
* high-dpi devices will be twice as large or even larger. Defaults to `"css"`. * high-dpi devices will be twice as large or even larger. Defaults to `"css"`.
*/ */
size?: 'css'|'device', 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?: { 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`. /** 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`.

View file

@ -33,7 +33,7 @@ it.describe('page screenshot', () => {
expect(screenshot).toMatchSnapshot('screenshot-sanity.png'); 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(` await page.setContent(`
<!-- Refer to stylesheet from other origin. Accessing this <!-- Refer to stylesheet from other origin. Accessing this
stylesheet rules will throw. stylesheet rules will throw.
@ -60,6 +60,35 @@ it.describe('page screenshot', () => {
} }
}); });
it('should capture blinking caret if explicitly asked for', async ({ page, server }) => {
await page.setContent(`
<!-- Refer to stylesheet from other origin. Accessing this
stylesheet rules will throw.
-->
<link rel=stylesheet href="${server.CROSS_PROCESS_PREFIX + '/injectedstyle.css'}">
<!-- make life harder: define caret color in stylesheet -->
<style>
div {
caret-color: #000 !important;
}
</style>
<div contenteditable="true"></div>
`);
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 }) => { it('should clip rect', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 }); await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html'); await page.goto(server.PREFIX + '/grid.html');

View file

@ -75,6 +75,11 @@ type ExpectSettings = {
* high-dpi devices will be twice as large or even larger. Defaults to `"css"`. * high-dpi devices will be twice as large or even larger. Defaults to `"css"`.
*/ */
size?: 'css'|'device', 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?: { 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`. /** 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`.