feat(vrt): allow providing screenshot style (#28229)

This commit is contained in:
Pavel Feldman 2023-11-30 17:42:45 -08:00 коммит произвёл GitHub
Родитель da6a36062e
Коммит 0a7a10d0f6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
25 изменённых файлов: 228 добавлений и 59 удалений

Просмотреть файл

@ -749,6 +749,9 @@ Returns the buffer with the captured screenshot.
### option: ElementHandle.screenshot.maskColor = %%-screenshot-option-mask-color-%%
* since: v1.34
### option: ElementHandle.screenshot.style = %%-screenshot-option-style-%%
* since: v1.41
## async method: ElementHandle.scrollIntoViewIfNeeded
* since: v1.8

Просмотреть файл

@ -1913,6 +1913,9 @@ Returns the buffer with the captured screenshot.
### option: Locator.screenshot.maskColor = %%-screenshot-option-mask-color-%%
* since: v1.34
### option: Locator.screenshot.style = %%-screenshot-option-style-%%
* since: v1.41
## async method: Locator.scrollIntoViewIfNeeded
* since: v1.14

Просмотреть файл

@ -1541,6 +1541,9 @@ Snapshot name.
### option: LocatorAssertions.toHaveScreenshot#1.maskColor = %%-screenshot-option-mask-color-%%
* since: v1.35
### option: LocatorAssertions.toHaveScreenshot#1.style = %%-screenshot-option-style-%%
* since: v1.41
### option: LocatorAssertions.toHaveScreenshot#1.omitBackground = %%-screenshot-option-omit-background-%%
* since: v1.23
@ -1587,6 +1590,9 @@ Note that screenshot assertions only work with Playwright test runner.
### option: LocatorAssertions.toHaveScreenshot#2.maskColor = %%-screenshot-option-mask-color-%%
* since: v1.35
### option: LocatorAssertions.toHaveScreenshot#2.style = %%-screenshot-option-style-%%
* since: v1.41
### option: LocatorAssertions.toHaveScreenshot#2.omitBackground = %%-screenshot-option-omit-background-%%
* since: v1.23

Просмотреть файл

@ -3399,6 +3399,9 @@ Returns the buffer with the captured screenshot.
### option: Page.screenshot.maskColor = %%-screenshot-option-mask-color-%%
* since: v1.34
### option: Page.screenshot.style = %%-screenshot-option-style-%%
* since: v1.41
## async method: Page.selectOption
* since: v1.8
* discouraged: Use locator-based [`method: Locator.selectOption`] instead. Read more about [locators](../locators.md).

Просмотреть файл

@ -161,6 +161,9 @@ Snapshot name.
### option: PageAssertions.toHaveScreenshot#1.maskColor = %%-screenshot-option-mask-color-%%
* since: v1.35
### option: PageAssertions.toHaveScreenshot#1.style = %%-screenshot-option-style-%%
* since: v1.41
### option: PageAssertions.toHaveScreenshot#1.omitBackground = %%-screenshot-option-omit-background-%%
* since: v1.23
@ -212,6 +215,9 @@ Note that screenshot assertions only work with Playwright test runner.
### option: PageAssertions.toHaveScreenshot#2.maskColor = %%-screenshot-option-mask-color-%%
* since: v1.35
### option: PageAssertions.toHaveScreenshot#2.style = %%-screenshot-option-style-%%
* since: v1.41
### option: PageAssertions.toHaveScreenshot#2.omitBackground = %%-screenshot-option-omit-background-%%
* since: v1.23

Просмотреть файл

@ -1132,6 +1132,13 @@ Defaults to `"css"`.
When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, text caret behavior will not be changed. Defaults to `"hide"`.
## screenshot-option-style
- `style` <string>
Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements invisible
or change their properties to help you creating repeatable screenshots. This stylesheet pierces the Shadow DOM and applies
to the inner frames.
## screenshot-options-common-list-v1.8
- %%-screenshot-option-animations-%%
- %%-screenshot-option-omit-background-%%

Просмотреть файл

@ -47,6 +47,7 @@ export default defineConfig({
- `animations` ?<[ScreenshotAnimations]<"allow"|"disabled">> See [`option: animations`] in [`method: Page.screenshot`]. Defaults to `"disabled"`.
- `caret` ?<[ScreenshotCaret]<"hide"|"initial">> See [`option: caret`] in [`method: Page.screenshot`]. Defaults to `"hide"`.
- `scale` ?<[ScreenshotScale]<"css"|"device">> See [`option: scale`] in [`method: Page.screenshot`]. Defaults to `"css"`.
- `style` ?<[string]> See [`option: style`] in [`method: Page.screenshot`].
- `toMatchSnapshot` ?<[Object]> Configuration for the [`method: SnapshotAssertions.toMatchSnapshot#1`] method.
- `threshold` ?<[float]> an acceptable perceived color difference between the same pixel in compared images, ranging from `0` (strict) and `1` (lax). `"pixelmatch"` comparator computes color difference in [YIQ color space](https://en.wikipedia.org/wiki/YIQ) and defaults `threshold` value to `0.2`.
- `maxDiffPixels` ?<[int]> an acceptable amount of pixels that could be different, unset by default.

Просмотреть файл

@ -1071,6 +1071,7 @@ scheme.PageExpectScreenshotParams = tObject({
selector: tString,
}))),
maskColor: tOptional(tString),
style: tOptional(tString),
})),
});
scheme.PageExpectScreenshotResult = tObject({
@ -1095,6 +1096,7 @@ scheme.PageScreenshotParams = tObject({
selector: tString,
}))),
maskColor: tOptional(tString),
style: tOptional(tString),
});
scheme.PageScreenshotResult = tObject({
binary: tBinary,
@ -1896,6 +1898,7 @@ scheme.ElementHandleScreenshotParams = tObject({
selector: tString,
}))),
maskColor: tOptional(tString),
style: tOptional(tString),
});
scheme.ElementHandleScreenshotResult = tObject({
binary: tBinary,

Просмотреть файл

@ -847,7 +847,7 @@ export class Frame extends SdkObject {
return result;
}
async maskSelectors(selectors: ParsedSelector[], color?: string): Promise<void> {
async maskSelectors(selectors: ParsedSelector[], color: string): Promise<void> {
const context = await this._utilityContext();
const injectedScript = await context.injectedScript();
await injectedScript.evaluate((injected, { parsed, color }) => {

Просмотреть файл

@ -118,8 +118,8 @@ export class Highlight {
this._innerUpdateHighlight(elements, options);
}
maskElements(elements: Element[], color?: string) {
this._innerUpdateHighlight(elements, { color: color ? color : '#F0F' });
maskElements(elements: Element[], color: string) {
this._innerUpdateHighlight(elements, { color: color });
}
private _innerUpdateHighlight(elements: Element[], options: HighlightOptions) {

Просмотреть файл

@ -1125,7 +1125,7 @@ export class InjectedScript {
return error;
}
maskSelectors(selectors: ParsedSelector[], color?: string) {
maskSelectors(selectors: ParsedSelector[], color: string) {
if (this._highlight)
this.hideHighlight();
this._highlight = new Highlight(this);

Просмотреть файл

@ -28,24 +28,25 @@ import { MultiMap } from '../utils/multimap';
declare global {
interface Window {
__cleanupScreenshot?: () => void;
__pwCleanupScreenshot?: () => void;
}
}
export type ScreenshotOptions = {
type?: 'png' | 'jpeg',
quality?: number,
omitBackground?: boolean,
animations?: 'disabled' | 'allow',
mask?: { frame: Frame, selector: string}[],
maskColor?: string,
fullPage?: boolean,
clip?: Rect,
scale?: 'css' | 'device',
caret?: 'hide' | 'initial',
type?: 'png' | 'jpeg';
quality?: number;
omitBackground?: boolean;
animations?: 'disabled' | 'allow';
mask?: { frame: Frame, selector: string}[];
maskColor?: string;
fullPage?: boolean;
clip?: Rect;
scale?: 'css' | 'device';
caret?: 'hide' | 'initial';
style?: string;
};
function inPagePrepareForScreenshots(hideCaret: boolean, disableAnimations: boolean) {
function inPagePrepareForScreenshots(screenshotStyle: string, disableAnimations: boolean) {
const collectRoots = (root: Document | ShadowRoot, roots: (Document|ShadowRoot)[] = []): (Document|ShadowRoot)[] => {
roots.push(root);
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
@ -58,29 +59,23 @@ function inPagePrepareForScreenshots(hideCaret: boolean, disableAnimations: bool
return roots;
};
let documentRoots: (Document|ShadowRoot)[] | undefined;
const memoizedRoots = () => documentRoots ??= collectRoots(document);
const styleTags: Element[] = [];
if (hideCaret) {
for (const root of memoizedRoots()) {
const styleTag = document.createElement('style');
styleTag.textContent = `
*:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) {
caret-color: transparent !important;
}
`;
if (root === document)
document.documentElement.append(styleTag);
else
root.append(styleTag);
styleTags.push(styleTag);
}
}
const infiniteAnimationsToResume: Set<Animation> = new Set();
const roots = collectRoots(document);
const cleanupCallbacks: (() => void)[] = [];
for (const root of roots) {
const styleTag = document.createElement('style');
styleTag.textContent = screenshotStyle;
if (root === document)
document.documentElement.append(styleTag);
else
root.append(styleTag);
cleanupCallbacks.push(() => {
styleTag.remove();
});
}
if (disableAnimations) {
const infiniteAnimationsToResume: Set<Animation> = new Set();
const handleAnimations = (root: Document|ShadowRoot): void => {
for (const animation of root.getAnimations()) {
if (!animation.effect || animation.playbackRate === 0 || infiniteAnimationsToResume.has(animation))
@ -106,7 +101,7 @@ function inPagePrepareForScreenshots(hideCaret: boolean, disableAnimations: bool
}
}
};
for (const root of memoizedRoots()) {
for (const root of roots) {
const handleRootAnimations: (() => void) = handleAnimations.bind(null, root);
handleRootAnimations();
root.addEventListener('transitionrun', handleRootAnimations);
@ -116,23 +111,22 @@ function inPagePrepareForScreenshots(hideCaret: boolean, disableAnimations: bool
root.removeEventListener('animationstart', handleRootAnimations);
});
}
cleanupCallbacks.push(() => {
for (const animation of infiniteAnimationsToResume) {
try {
animation.play();
} catch (e) {
// animation.play() should never throw, but
// we'd like to be on the safe side.
}
}
});
}
window.__cleanupScreenshot = () => {
for (const styleTag of styleTags)
styleTag.remove();
for (const animation of infiniteAnimationsToResume) {
try {
animation.play();
} catch (e) {
// animation.play() should never throw, but
// we'd like to be on the safe side.
}
}
window.__pwCleanupScreenshot = () => {
for (const cleanupCallback of cleanupCallbacks)
cleanupCallback();
delete window.__cleanupScreenshot;
delete window.__pwCleanupScreenshot;
};
}
@ -178,7 +172,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.caret !== 'initial', options.animations === 'disabled');
await this._preparePageForScreenshot(progress, screenshotStyle(options), options.animations === 'disabled');
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
if (options.fullPage) {
@ -207,7 +201,7 @@ export class Screenshotter {
progress.log('taking element screenshot');
const { viewportSize } = await this._originalViewportSize(progress);
await this._preparePageForScreenshot(progress, options.caret !== 'initial', options.animations === 'disabled');
await this._preparePageForScreenshot(progress, screenshotStyle(options), options.animations === 'disabled');
progress.throwIfAborted(); // Do not do extra work.
await handle._waitAndScrollIntoViewIfNeeded(progress, true /* waitForVisible */);
@ -231,14 +225,11 @@ export class Screenshotter {
});
}
async _preparePageForScreenshot(progress: Progress, hideCaret: boolean, disableAnimations: boolean) {
if (!hideCaret && !disableAnimations)
return;
async _preparePageForScreenshot(progress: Progress, screenshotStyle: string, disableAnimations: boolean) {
if (disableAnimations)
progress.log(' disabled all CSS animations');
await Promise.all(this._page.frames().map(async frame => {
await frame.nonStallingEvaluateInExistingContext('(' + inPagePrepareForScreenshots.toString() + `)(${hideCaret}, ${disableAnimations})`, false, 'utility').catch(() => {});
await frame.nonStallingEvaluateInExistingContext('(' + inPagePrepareForScreenshots.toString() + `)(${JSON.stringify(screenshotStyle)}, ${disableAnimations})`, false, 'utility').catch(() => {});
}));
if (!process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY) {
progress.log('waiting for fonts to load...');
@ -252,7 +243,7 @@ export class Screenshotter {
async _restorePageAfterScreenshot() {
await Promise.all(this._page.frames().map(async frame => {
frame.nonStallingEvaluateInExistingContext('window.__cleanupScreenshot && window.__cleanupScreenshot()', false, 'utility').catch(() => {});
frame.nonStallingEvaluateInExistingContext('window.__pwCleanupScreenshot && window.__pwCleanupScreenshot()', false, 'utility').catch(() => {});
}));
}
@ -276,7 +267,7 @@ export class Screenshotter {
progress.throwIfAborted(); // Avoid extra work.
await Promise.all([...framesToParsedSelectors.keys()].map(async frame => {
await frame.maskSelectors(framesToParsedSelectors.get(frame), options.maskColor);
await frame.maskSelectors(framesToParsedSelectors.get(frame), options.maskColor || '#F0F');
}));
progress.cleanupWhenAborted(cleanup);
return cleanup;
@ -368,3 +359,16 @@ export function validateScreenshotOptions(options: ScreenshotOptions): 'png' | '
}
return format;
}
function screenshotStyle(options: ScreenshotOptions): string {
const parts: string[] = [];
if (options.caret !== 'initial') {
parts.push(`
*:not(#playwright-aaaaaaaaaa.playwright-bbbbbbbbbbb.playwright-cccccccccc.playwright-dddddddddd.playwright-eeeeeeeee) {
caret-color: transparent !important;
}`);
}
if (options.style)
parts.push(options.style);
return parts.join('\n');
}

21
packages/playwright-core/types/types.d.ts поставляемый
Просмотреть файл

@ -9967,6 +9967,13 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
*/
scale?: "css"|"device";
/**
* Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
* invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces the
* Shadow DOM and applies to the inner frames.
*/
style?: string;
/**
* Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout`
* option in the config, or by using the
@ -20037,6 +20044,13 @@ export interface LocatorScreenshotOptions {
*/
scale?: "css"|"device";
/**
* Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
* invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces the
* Shadow DOM and applies to the inner frames.
*/
style?: string;
/**
* Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout`
* option in the config, or by using the
@ -20230,6 +20244,13 @@ export interface PageScreenshotOptions {
*/
scale?: "css"|"device";
/**
* Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
* invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces the
* Shadow DOM and applies to the inner frames.
*/
style?: string;
/**
* Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout`
* option in the config, or by using the

Просмотреть файл

@ -356,6 +356,7 @@ export async function toHaveScreenshot(
animations: config?.animations ?? 'disabled',
scale: config?.scale ?? 'css',
caret: config?.caret ?? 'hide',
style: config?.style ?? '',
...helper.allOptions,
mask: (helper.allOptions.mask || []) as LocatorEx[],
maskColor: helper.allOptions.maskColor,

33
packages/playwright/types/test.d.ts поставляемый
Просмотреть файл

@ -662,6 +662,11 @@ interface TestConfig {
* to `"css"`.
*/
scale?: "css"|"device";
/**
* See `style` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot).
*/
style?: string;
};
/**
@ -5951,6 +5956,13 @@ interface LocatorAssertions {
*/
scale?: "css"|"device";
/**
* Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
* invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces the
* Shadow DOM and applies to the inner frames.
*/
style?: string;
/**
* An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the
* same pixel in compared images, between zero (strict) and one (lax), default is configurable with
@ -6034,6 +6046,13 @@ interface LocatorAssertions {
*/
scale?: "css"|"device";
/**
* Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
* invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces the
* Shadow DOM and applies to the inner frames.
*/
style?: string;
/**
* An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the
* same pixel in compared images, between zero (strict) and one (lax), default is configurable with
@ -6298,6 +6317,13 @@ interface PageAssertions {
*/
scale?: "css"|"device";
/**
* Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
* invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces the
* Shadow DOM and applies to the inner frames.
*/
style?: string;
/**
* An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the
* same pixel in compared images, between zero (strict) and one (lax), default is configurable with
@ -6411,6 +6437,13 @@ interface PageAssertions {
*/
scale?: "css"|"device";
/**
* Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements
* invisible or change their properties to help you creating repeatable screenshots. This stylesheet pierces the
* Shadow DOM and applies to the inner frames.
*/
style?: string;
/**
* An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the
* same pixel in compared images, between zero (strict) and one (lax), default is configurable with

Просмотреть файл

@ -1944,6 +1944,7 @@ export type PageExpectScreenshotParams = {
selector: string,
}[],
maskColor?: string,
style?: string,
},
};
export type PageExpectScreenshotOptions = {
@ -1971,6 +1972,7 @@ export type PageExpectScreenshotOptions = {
selector: string,
}[],
maskColor?: string,
style?: string,
},
};
export type PageExpectScreenshotResult = {
@ -1995,6 +1997,7 @@ export type PageScreenshotParams = {
selector: string,
}[],
maskColor?: string,
style?: string,
};
export type PageScreenshotOptions = {
timeout?: number,
@ -2011,6 +2014,7 @@ export type PageScreenshotOptions = {
selector: string,
}[],
maskColor?: string,
style?: string,
};
export type PageScreenshotResult = {
binary: Binary,
@ -3355,6 +3359,7 @@ export type ElementHandleScreenshotParams = {
selector: string,
}[],
maskColor?: string,
style?: string,
};
export type ElementHandleScreenshotOptions = {
timeout?: number,
@ -3369,6 +3374,7 @@ export type ElementHandleScreenshotOptions = {
selector: string,
}[],
maskColor?: string,
style?: string,
};
export type ElementHandleScreenshotResult = {
binary: Binary,

Просмотреть файл

@ -379,6 +379,7 @@ CommonScreenshotOptions:
frame: Frame
selector: string
maskColor: string?
style: string?
LaunchOptions:
type: mixin

Просмотреть файл

@ -543,6 +543,36 @@ it.describe('page screenshot', () => {
maskColor: '#00FF00',
})).toMatchSnapshot('mask-color-should-work.png');
});
it('should hide elements based on attr', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
await page.locator('div').nth(5).evaluate(element => {
element.setAttribute('data-test-screenshot', 'hide');
});
expect(await page.screenshot({
style: `[data-test-screenshot="hide"] {
visibility: hidden;
}`
})).toMatchSnapshot('hide-should-work.png');
const visibility = await page.locator('div').nth(5).evaluate(element => element.style.visibility);
expect(visibility).toBe('');
});
it('should remove elements based on attr', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
await page.locator('div').nth(5).evaluate(element => {
element.setAttribute('data-test-screenshot', 'remove');
});
expect(await page.screenshot({
style: `[data-test-screenshot="remove"] {
display: none;
}`
})).toMatchSnapshot('remove-should-work.png');
const display = await page.locator('div').nth(5).evaluate(element => element.style.display);
expect(display).toBe('');
});
});
});

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 35 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 45 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 39 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 35 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 45 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 39 KiB

Просмотреть файл

@ -1216,6 +1216,47 @@ test('should support maskColor option', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
});
test('should support style option', async ({ runInlineTest }) => {
const result = await runInlineTest({
...playwrightConfig({
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
}),
'__screenshots__/a.spec.js/snapshot.png': createImage(IMG_WIDTH, IMG_HEIGHT, 0, 255, 0),
'a.spec.js': `
const { test, expect } = require('@playwright/test');
test('png', async ({ page }) => {
await page.setContent('<style> html,body { padding: 0; margin: 0; }</style>');
await expect(page).toHaveScreenshot('snapshot.png', {
style: 'body { background: #00FF00; }',
});
});
`,
});
expect(result.exitCode).toBe(0);
});
test('should support style option in config', async ({ runInlineTest }) => {
const result = await runInlineTest({
...playwrightConfig({
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
expect: {
toHaveScreenshot: {
style: 'body { background: #00FF00; }',
},
},
}),
'__screenshots__/a.spec.js/snapshot.png': createImage(IMG_WIDTH, IMG_HEIGHT, 0, 255, 0),
'a.spec.js': `
const { test, expect } = require('@playwright/test');
test('png', async ({ page }) => {
await page.setContent('<style> html,body { padding: 0; margin: 0; }</style>');
await expect(page).toHaveScreenshot('snapshot.png');
});
`,
});
expect(result.exitCode).toBe(0);
});
function playwrightConfig(obj: any) {
return {
'playwright.config.js': `