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.caret = %%-screenshot-option-caret-%%
### option: PageAssertions.toHaveScreenshot.maxDiffPixels = %%-assertions-max-diff-pixels-%%
### 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"`.
## 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-%%

View File

@ -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',

View File

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

View File

@ -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'])),

View File

@ -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<Animation> = 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');

View File

@ -8146,6 +8146,12 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
*/
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:
*/

View File

@ -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`.

View File

@ -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(`
<!-- Refer to stylesheet from other origin. Accessing this
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 }) => {
await page.setViewportSize({ width: 500, height: 500 });
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"`.
*/
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`.