diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index ccca23455d..abe90782b9 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -1541,7 +1541,7 @@ Snapshot name. ### option: LocatorAssertions.toHaveScreenshot#1.maskColor = %%-screenshot-option-mask-color-%% * since: v1.35 -### option: LocatorAssertions.toHaveScreenshot#1.style = %%-screenshot-option-style-%% +### option: LocatorAssertions.toHaveScreenshot#1.stylePath = %%-screenshot-option-style-path-%% * since: v1.41 ### option: LocatorAssertions.toHaveScreenshot#1.omitBackground = %%-screenshot-option-omit-background-%% @@ -1590,7 +1590,7 @@ 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-%% +### option: LocatorAssertions.toHaveScreenshot#2.stylePath = %%-screenshot-option-style-path-%% * since: v1.41 ### option: LocatorAssertions.toHaveScreenshot#2.omitBackground = %%-screenshot-option-omit-background-%% diff --git a/docs/src/api/class-pageassertions.md b/docs/src/api/class-pageassertions.md index c79ddb50eb..75a3ec5e80 100644 --- a/docs/src/api/class-pageassertions.md +++ b/docs/src/api/class-pageassertions.md @@ -161,7 +161,7 @@ Snapshot name. ### option: PageAssertions.toHaveScreenshot#1.maskColor = %%-screenshot-option-mask-color-%% * since: v1.35 -### option: PageAssertions.toHaveScreenshot#1.style = %%-screenshot-option-style-%% +### option: PageAssertions.toHaveScreenshot#1.stylePath = %%-screenshot-option-style-path-%% * since: v1.41 ### option: PageAssertions.toHaveScreenshot#1.omitBackground = %%-screenshot-option-omit-background-%% @@ -215,7 +215,7 @@ 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-%% +### option: PageAssertions.toHaveScreenshot#2.stylePath = %%-screenshot-option-style-path-%% * since: v1.41 ### option: PageAssertions.toHaveScreenshot#2.omitBackground = %%-screenshot-option-omit-background-%% diff --git a/docs/src/api/params.md b/docs/src/api/params.md index c2c9e33c0d..8de2a2a18f 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -1137,10 +1137,15 @@ When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, ## screenshot-option-style - `style` -Stylesheet to apply while making the screenshot. This is where you can hide dynamic elements, make elements invisible +Text of the 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-option-style-path +- `stylePath` <[string]|[Array]<[string]>> + +File name containing the 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-%% diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index dc558c0e31..46fb7ac38f 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -64,7 +64,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`]. + - `stylePath` ?<[string]|[Array]<[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. diff --git a/docs/src/test-api/class-testproject.md b/docs/src/test-api/class-testproject.md index 179a4815d8..338910db71 100644 --- a/docs/src/test-api/class-testproject.md +++ b/docs/src/test-api/class-testproject.md @@ -97,6 +97,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"`. + - `stylePath` ?<[string]|[Array]<[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. diff --git a/docs/src/test-snapshots-js.md b/docs/src/test-snapshots-js.md index 83f309b929..41f855dde3 100644 --- a/docs/src/test-snapshots-js.md +++ b/docs/src/test-snapshots-js.md @@ -16,6 +16,8 @@ test('example test', async ({ page }) => { }); ``` +## Generating screenshots + When you run above for the first time, test runner will say: ```txt @@ -42,13 +44,7 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts: - `chromium-darwin` - the browser name and the platform. Screenshots differ between browsers and platforms due to different rendering, fonts and more, so you will need different snapshots for them. If you use multiple projects in your [configuration file](./test-configuration.md), project name will be used instead of `chromium`. -If you are not on the same operating system as your CI system, you can use Docker to generate/update the screenshots: - -```bash -docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v%%VERSION%%-jammy /bin/bash -npm install -npx playwright test --update-snapshots -``` +## Updating screenshots Sometimes you need to update the reference screenshot, for example when the page has changed. Do this with the `--update-snapshots` flag. @@ -59,6 +55,10 @@ npx playwright test --update-snapshots > Note that `snapshotName` also accepts an array of path segments to the snapshot file such as `expect().toHaveScreenshot(['relative', 'path', 'to', 'snapshot.png'])`. > However, this path must stay within the snapshots directory for each test file (i.e. `a.spec.js-snapshots`), otherwise it will throw. +## Options + +### maxDiffPixels + Playwright Test uses the [pixelmatch](https://github.com/mapbox/pixelmatch) library. You can [pass various options](./api/class-pageassertions.md#page-assertions-to-have-screenshot-1) to modify its behavior: ```js title="example.spec.ts" @@ -81,6 +81,42 @@ export default defineConfig({ }); ``` +### stylePath + +You can apply a custom stylesheet to your page while taking screenshot. This +allows filtering out dynamic or volatile elements, hence improving the screenshot +determinism. + +```css title="screenshot.css" +iframe { + visibility: hidden; +} +``` + +```js title="example.spec.ts" +import { test, expect } from '@playwright/test'; + +test('example test', async ({ page }) => { + await page.goto('https://playwright.dev'); + await expect(page).toHaveScreenshot({ styleFile: path.join(__dirname, 'screenshot.css') }); +}); +``` + +If you'd like to share the default value among all the tests in the project, you can specify it in the playwright config, either globally or per project: + +```js title="playwright.config.ts" +import { defineConfig } from '@playwright/test'; +export default defineConfig({ + expect: { + toHaveScreenshot: { + styleFile: './screenshot.css' + }, + }, +}); +``` + +## Non-image snapshots + Apart from screenshots, you can use `expect(value).toMatchSnapshot(snapshotName)` to compare text or arbitrary binary data. Playwright Test auto-detects the content type and uses the appropriate comparison algorithm. Here we compare text content against the reference. diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 4413fdc276..d37498c23b 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10134,9 +10134,9 @@ export interface ElementHandle extends JSHandle { 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. + * Text of the 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; @@ -20256,9 +20256,9 @@ 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. + * Text of the 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; @@ -20456,9 +20456,9 @@ 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. + * Text of the 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; diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 541e05cb36..dda5fb7cbf 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -189,6 +189,10 @@ export class FullProjectInternal { }; this.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel, config.fullyParallel, undefined); this.expect = takeFirst(projectConfig.expect, config.expect, {}); + if (this.expect.toHaveScreenshot?.stylePath) { + const stylePaths = Array.isArray(this.expect.toHaveScreenshot.stylePath) ? this.expect.toHaveScreenshot.stylePath : [this.expect.toHaveScreenshot.stylePath]; + this.expect.toHaveScreenshot.stylePath = stylePaths.map(stylePath => path.resolve(configDir, stylePath)); + } this.respectGitIgnore = !projectConfig.testDir && !config.testDir; } } diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts index 0140abf899..511a42395e 100644 --- a/packages/playwright/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchSnapshot.ts @@ -19,7 +19,7 @@ import type { Page as PageEx } from 'playwright-core/lib/client/page'; import type { Locator as LocatorEx } from 'playwright-core/lib/client/locator'; import { currentTestInfo, currentExpectTimeout } from '../common/globals'; import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils'; -import { getComparator, sanitizeForFilePath } from 'playwright-core/lib/utils'; +import { getComparator, sanitizeForFilePath, zones } from 'playwright-core/lib/utils'; import type { PageScreenshotOptions } from 'playwright-core/types/types'; import { addSuffixToFilePath, serializeError, @@ -308,7 +308,7 @@ export function toMatchSnapshot( return helper.handleDifferent(received, expected, undefined, result.diff, result.errorMessage, undefined); } -type HaveScreenshotOptions = ImageComparatorOptions & Omit; +type HaveScreenshotOptions = ImageComparatorOptions & Omit & { stylePath?: string | string[] }; export function toHaveScreenshotStepTitle( nameOrOptions: NameOrSegments | { name?: NameOrSegments } & HaveScreenshotOptions = {}, @@ -351,12 +351,25 @@ export async function toHaveScreenshot( if (!helper.snapshotPath.toLowerCase().endsWith('.png')) throw new Error(`Screenshot name "${path.basename(helper.snapshotPath)}" must have '.png' extension`); expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot'); + return await zones.preserve(async () => { + // Loading from filesystem resets zones. + const style = await loadScreenshotStyles(optOptions.stylePath || config?.stylePath); + return toHaveScreenshotContinuation.call(this, helper, page, locator, config, style); + }); +} - const screenshotOptions = { +async function toHaveScreenshotContinuation( + this: ExpectMatcherContext, + helper: SnapshotHelper, + page: PageEx, + locator: LocatorEx | undefined, + config?: HaveScreenshotOptions, + style?: string) { + const screenshotOptions: any = { animations: config?.animations ?? 'disabled', scale: config?.scale ?? 'css', caret: config?.caret ?? 'hide', - style: config?.style ?? '', + style, ...helper.allOptions, mask: (helper.allOptions.mask || []) as LocatorEx[], maskColor: helper.allOptions.maskColor, @@ -462,3 +475,15 @@ function determineFileExtension(file: string | Buffer): string { function compareMagicBytes(file: Buffer, magicBytes: number[]): boolean { return Buffer.compare(Buffer.from(magicBytes), file.slice(0, magicBytes.length)) === 0; } + +async function loadScreenshotStyles(stylePath?: string | string[]): Promise { + if (!stylePath) + return; + + const stylePaths = Array.isArray(stylePath) ? stylePath : [stylePath]; + const styles = await Promise.all(stylePaths.map(async stylePath => { + const text = await fs.promises.readFile(stylePath, 'utf8'); + return text.trim(); + })); + return styles.join('\n').trim() || undefined; +} diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index e0713271cc..b14d3edd71 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -685,7 +685,7 @@ interface TestConfig { /** * See `style` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot). */ - style?: string; + stylePath?: string|Array; }; /** @@ -5993,11 +5993,11 @@ 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. + * File name containing the 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; + stylePath?: string|Array; /** * An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the @@ -6083,11 +6083,11 @@ 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. + * File name containing the 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; + stylePath?: string|Array; /** * An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the @@ -6354,11 +6354,11 @@ 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. + * File name containing the 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; + stylePath?: string|Array; /** * An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the @@ -6474,11 +6474,11 @@ 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. + * File name containing the 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; + stylePath?: string|Array; /** * An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the @@ -6830,6 +6830,11 @@ interface TestProject { * to `"css"`. */ scale?: "css"|"device"; + + /** + * See `style` in [page.screenshot([options])](https://playwright.dev/docs/api/class-page#page-screenshot). + */ + stylePath?: string|Array; }; /** diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 92942780b3..1a7774c840 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -1216,18 +1216,19 @@ test('should support maskColor option', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); }); -test('should support style option', async ({ runInlineTest }) => { +test('should support stylePath 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': ` + '__screenshots__/tests/a.spec.js/snapshot.png': createImage(IMG_WIDTH, IMG_HEIGHT, 0, 255, 0), + 'screenshot.css': 'body { background: #00FF00; }', + 'tests/a.spec.js': ` const { test, expect } = require('@playwright/test'); test('png', async ({ page }) => { await page.setContent(''); await expect(page).toHaveScreenshot('snapshot.png', { - style: 'body { background: #00FF00; }', + stylePath: './screenshot.css', }); }); `, @@ -1235,16 +1236,17 @@ test('should support style option', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); }); -test('should support style option in config', async ({ runInlineTest }) => { +test('should support stylePath option in config', async ({ runInlineTest }) => { const result = await runInlineTest({ ...playwrightConfig({ snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}', expect: { toHaveScreenshot: { - style: 'body { background: #00FF00; }', + stylePath: './screenshot.css', }, }, }), + 'screenshot.css': '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'); diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index 5dccfa65f0..7bc4dccee6 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -181,6 +181,7 @@ async function run() { const allowedCodeLangs = new Set([ 'csharp', 'java', + 'css', 'js', 'ts', 'python',