Skip to content

Commit d45f964

Browse files
authored
feat(browser): introduce toMatchScreenshot for Visual Regression Testing (#8041)
1 parent e71a5d0 commit d45f964

35 files changed

+2955
-207
lines changed

docs/.vitepress/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ export default ({ mode }: { mode: string }) => {
291291
link: '/guide/browser/multiple-setups',
292292
docFooterText: 'Multiple Setups | Browser Mode',
293293
},
294+
{
295+
text: 'Visual Regression Testing',
296+
link: '/guide/browser/visual-regression-testing',
297+
docFooterText: 'Visual Regression Testing | Browser Mode',
298+
},
294299
],
295300
},
296301
{

docs/guide/browser/assertion-api.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,3 +1067,216 @@ await expect.element(queryByTestId('parent')).toHaveSelection('ected text')
10671067
await expect.element(queryByTestId('prev')).not.toHaveSelection()
10681068
await expect.element(queryByTestId('next')).toHaveSelection('ne')
10691069
```
1070+
1071+
## toMatchScreenshot <Badge type="warning">experimental</Badge>
1072+
1073+
```ts
1074+
function toMatchScreenshot(
1075+
options?: ScreenshotMatcherOptions,
1076+
): Promise<void>
1077+
function toMatchScreenshot(
1078+
name?: string,
1079+
options?: ScreenshotMatcherOptions,
1080+
): Promise<void>
1081+
```
1082+
1083+
::: tip
1084+
The `toMatchScreenshot` assertion can be configured globally in your
1085+
[Vitest config](/guide/browser/config#browser-expect-tomatchscreenshot).
1086+
:::
1087+
1088+
This assertion allows you to perform visual regression testing by comparing
1089+
screenshots of elements or pages against stored reference images.
1090+
1091+
When differences are detected beyond the configured threshold, the test fails.
1092+
To help identify the changes, the assertion generates:
1093+
1094+
- The actual screenshot captured during the test
1095+
- The expected reference screenshot
1096+
- A diff image highlighting the differences (when possible)
1097+
1098+
::: warning Screenshots Stability
1099+
The assertion automatically retries taking screenshots until two consecutive
1100+
captures yield the same result. This helps reduce flakiness caused by
1101+
animations, loading states, or other dynamic content. You can control this
1102+
behavior with the `timeout` option.
1103+
1104+
However, browser rendering can vary across:
1105+
1106+
- Different browsers and browser versions
1107+
- Operating systems (Windows, macOS, Linux)
1108+
- Screen resolutions and pixel densities
1109+
- GPU drivers and hardware acceleration
1110+
- Font rendering and system fonts
1111+
1112+
It is recommended to read the
1113+
[Visual Regression Testing guide](/guide/browser/visual-regression-testing) to
1114+
implement this testing strategy efficiently.
1115+
:::
1116+
1117+
::: tip
1118+
When a screenshot comparison fails due to **intentional changes**, you can
1119+
update the reference screenshot by pressing the `u` key in watch mode, or by
1120+
running tests with the `-u` or `--update` flags.
1121+
:::
1122+
1123+
```html
1124+
<button data-testid="button">Fancy Button</button>
1125+
```
1126+
1127+
```ts
1128+
// basic usage, auto-generates screenshot name
1129+
await expect.element(getByTestId('button')).toMatchScreenshot()
1130+
1131+
// with custom name
1132+
await expect.element(getByTestId('button')).toMatchScreenshot('fancy-button')
1133+
1134+
// with options
1135+
await expect.element(getByTestId('button')).toMatchScreenshot({
1136+
comparatorName: 'pixelmatch',
1137+
comparatorOptions: {
1138+
allowedMismatchedPixelRatio: 0.01,
1139+
},
1140+
})
1141+
1142+
// with both name and options
1143+
await expect.element(getByTestId('button')).toMatchScreenshot('fancy-button', {
1144+
comparatorName: 'pixelmatch',
1145+
comparatorOptions: {
1146+
allowedMismatchedPixelRatio: 0.01,
1147+
},
1148+
})
1149+
```
1150+
1151+
### Options
1152+
1153+
- `comparatorName: "pixelmatch" = "pixelmatch"`
1154+
1155+
The name of the algorithm/library used for comparing images.
1156+
1157+
Currently, [`"pixelmatch"`](https://github.com/mapbox/pixelmatch) is the only
1158+
supported comparator.
1159+
1160+
- `comparatorOptions: object`
1161+
1162+
These options allow changing the behavior of the comparator. What properties
1163+
can be set depends on the chosen comparator algorithm.
1164+
1165+
Vitest has set default values out of the box, but they can be overridden.
1166+
1167+
- [`"pixelmatch"` options](#pixelmatch-comparator-options)
1168+
1169+
::: warning
1170+
**Always explicitly set `comparatorName` to get proper type inference for
1171+
`comparatorOptions`**.
1172+
1173+
Without it, TypeScript won't know which options are valid:
1174+
1175+
```ts
1176+
// ❌ TypeScript can't infer the correct options
1177+
await expect.element(button).toMatchScreenshot({
1178+
comparatorOptions: {
1179+
// might error when new comparators are added
1180+
allowedMismatchedPixelRatio: 0.01,
1181+
},
1182+
})
1183+
1184+
// ✅ TypeScript knows these are pixelmatch options
1185+
await expect.element(button).toMatchScreenshot({
1186+
comparatorName: 'pixelmatch',
1187+
comparatorOptions: {
1188+
allowedMismatchedPixelRatio: 0.01,
1189+
},
1190+
})
1191+
```
1192+
:::
1193+
1194+
- `screenshotOptions: object`
1195+
1196+
The same options allowed by
1197+
[`locator.screenshot()`](/guide/browser/locators.html#screenshot), except for:
1198+
1199+
- `'base64'`
1200+
- `'path'`
1201+
- `'save'`
1202+
- `'type'`
1203+
1204+
- `timeout: number = 5_000`
1205+
1206+
Time to wait until a stable screenshot is found.
1207+
1208+
Setting this value to `0` disables the timeout, but if a stable screenshot
1209+
can't be determined the process will not end.
1210+
1211+
#### `"pixelmatch"` comparator options
1212+
1213+
The following options are available when using the `"pixelmatch"` comparator:
1214+
1215+
- `allowedMismatchedPixelRatio: number | undefined = undefined`
1216+
1217+
The maximum allowed ratio of differing pixels between the captured screenshot
1218+
and the reference image.
1219+
1220+
Must be a value between `0` and `1`.
1221+
1222+
For example, `allowedMismatchedPixelRatio: 0.02` means the test will pass
1223+
if up to 2% of pixels differ, but fail if more than 2% differ.
1224+
1225+
- `allowedMismatchedPixels: number | undefined = undefined`
1226+
1227+
The maximum number of pixels that are allowed to differ between the captured
1228+
screenshot and the stored reference image.
1229+
1230+
If set to `undefined`, any non-zero difference will cause the test to fail.
1231+
1232+
For example, `allowedMismatchedPixels: 10` means the test will pass if 10 or
1233+
fewer pixels differ, but fail if 11 or more differ.
1234+
1235+
- `threshold: number = 0.1`
1236+
1237+
Acceptable perceived color difference between the same pixel in two images.
1238+
1239+
Value ranges from `0` (strict) to `1` (very lenient). Lower values mean small
1240+
differences will be detected.
1241+
1242+
The comparison uses the [YIQ color space](https://en.wikipedia.org/wiki/YIQ).
1243+
1244+
- `includeAA: boolean = false`
1245+
1246+
If `true`, disables detection and ignoring of anti-aliased pixels.
1247+
1248+
- `alpha: number = 0.1`
1249+
1250+
Blending level of unchanged pixels in the diff image.
1251+
1252+
Ranges from `0` (white) to `1` (original brightness).
1253+
1254+
- `aaColor: [r: number, g: number, b: number] = [255, 255, 0]`
1255+
1256+
Color used for anti-aliased pixels in the diff image.
1257+
1258+
- `diffColor: [r: number, g: number, b: number] = [255, 0, 0]`
1259+
1260+
Color used for differing pixels in the diff image.
1261+
1262+
- `diffColorAlt: [r: number, g: number, b: number] | undefined = undefined`
1263+
1264+
Optional alternative color for dark-on-light differences, to help show what's
1265+
added vs. removed.
1266+
1267+
If not set, `diffColor` is used for all differences.
1268+
1269+
- `diffMask: boolean = false`
1270+
1271+
If `true`, shows only the diff as a mask on a transparent background, instead
1272+
of overlaying it on the original image.
1273+
1274+
Anti-aliased pixels won't be shown (if detected).
1275+
1276+
::: warning
1277+
When both `allowedMismatchedPixels` and `allowedMismatchedPixelRatio` are set,
1278+
the more restrictive value is used.
1279+
1280+
For example, if you allow 100 pixels or 2% ratio, and your image has 10,000
1281+
pixels, the effective limit would be 100 pixels instead of 200.
1282+
:::

docs/guide/browser/config.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,152 @@ The timeout in milliseconds. If connection to the browser takes longer, the test
325325
::: info
326326
This is the time it should take for the browser to establish the WebSocket connection with the Vitest server. In normal circumstances, this timeout should never be reached.
327327
:::
328+
329+
## browser.expect
330+
331+
- **Type:** `ExpectOptions`
332+
333+
### browser.expect.toMatchScreenshot
334+
335+
Default options for the
336+
[`toMatchScreenshot` assertion](/guide/browser/assertion-api.html#tomatchscreenshot).
337+
These options will be applied to all screenshot assertions.
338+
339+
::: tip
340+
Setting global defaults for screenshot assertions helps maintain consistency
341+
across your test suite and reduces repetition in individual tests. You can still
342+
override these defaults at the assertion level when needed for specific test cases.
343+
:::
344+
345+
```ts
346+
import { defineConfig } from 'vitest/config'
347+
348+
export default defineConfig({
349+
test: {
350+
browser: {
351+
enabled: true,
352+
expect: {
353+
toMatchScreenshot: {
354+
comparatorName: 'pixelmatch',
355+
comparatorOptions: {
356+
threshold: 0.2,
357+
allowedMismatchedPixels: 100,
358+
},
359+
resolveScreenshotPath: ({ arg, browserName, ext, testFileName }) =>
360+
`custom-screenshots/${testFileName}/${arg}-${browserName}${ext}`,
361+
},
362+
},
363+
},
364+
},
365+
})
366+
```
367+
368+
[All options available in the `toMatchScreenshot` assertion](/guide/browser/assertion-api#options)
369+
can be configured here. Additionally, two path resolution functions are
370+
available: `resolveScreenshotPath` and `resolveDiffPath`.
371+
372+
#### browser.expect.toMatchScreenshot.resolveScreenshotPath
373+
374+
- **Type:** `(data: PathResolveData) => string`
375+
- **Default output:** `` `${root}/${testFileDirectory}/${screenshotDirectory}/${testFileName}/${arg}-${browserName}-${platform}${ext}` ``
376+
377+
A function to customize where reference screenshots are stored. The function
378+
receives an object with the following properties:
379+
380+
- `arg: string`
381+
382+
Path **without** extension, sanitized and relative to the test file.
383+
384+
This comes from the arguments passed to `toMatchScreenshot`; if called
385+
without arguments this will be the auto-generated name.
386+
387+
```ts
388+
test('calls `onClick`', () => {
389+
expect(locator).toMatchScreenshot()
390+
// arg = "calls-onclick-1"
391+
})
392+
393+
expect(locator).toMatchScreenshot('foo/bar/baz.png')
394+
// arg = "foo/bar/baz"
395+
396+
expect(locator).toMatchScreenshot('../foo/bar/baz.png')
397+
// arg = "foo/bar/baz"
398+
```
399+
400+
- `ext: string`
401+
402+
Screenshot extension, with leading dot.
403+
404+
This can be set through the arguments passed to `toMatchScreenshot`, but
405+
the value will fall back to `'.png'` if an unsupported extension is used.
406+
407+
- `browserName: string`
408+
409+
The instance's browser name.
410+
411+
- `platform: NodeJS.Platform`
412+
413+
The value of
414+
[`process.platform`](https://nodejs.org/docs/v22.16.0/api/process.html#processplatform).
415+
416+
- `screenshotDirectory: string`
417+
418+
The value provided to
419+
[`browser.screenshotDirectory`](/guide/browser/config#browser-screenshotdirectory),
420+
if none is provided, its default value.
421+
422+
- `root: string`
423+
424+
Absolute path to the project's [`root`](/config/#root).
425+
426+
- `testFileDirectory: string`
427+
428+
Path to the test file, relative to the project's [`root`](/config/#root).
429+
430+
- `testFileName: string`
431+
432+
The test's filename.
433+
434+
- `testName: string`
435+
436+
The [`test`](/api/#test)'s name, including parent
437+
[`describe`](/api/#describe), sanitized.
438+
439+
- `attachmentsDir: string`
440+
441+
The value provided to [`attachmentsDir`](/config/#attachmentsdir), if none is
442+
provided, its default value.
443+
444+
For example, to group screenshots by browser:
445+
446+
```ts
447+
resolveScreenshotPath: ({ arg, browserName, ext, root, testFileName }) =>
448+
`${root}/screenshots/${browserName}/${testFileName}/${arg}${ext}`
449+
```
450+
451+
#### browser.expect.toMatchScreenshot.resolveDiffPath
452+
453+
- **Type:** `(data: PathResolveData) => string`
454+
- **Default output:** `` `${root}/${attachmentsDir}/${testFileDirectory}/${testFileName}/${arg}-${browserName}-${platform}${ext}` ``
455+
456+
A function to customize where diff images are stored when screenshot comparisons
457+
fail. Receives the same data object as
458+
[`resolveScreenshotPath`](#browser-expect-tomatchscreenshot-resolvescreenshotpath).
459+
460+
For example, to store diffs in a subdirectory of attachments:
461+
462+
```ts
463+
resolveDiffPath: ({ arg, attachmentsDir, browserName, ext, root, testFileName }) =>
464+
`${root}/${attachmentsDir}/screenshot-diffs/${testFileName}/${arg}-${browserName}${ext}`
465+
```
466+
467+
::: tip
468+
To have a better type safety when using built-in providers, you should reference
469+
one of these types (for provider that you are using) in your
470+
[config file](/config/):
471+
472+
```ts
473+
/// <reference types="@vitest/browser/providers/playwright" />
474+
/// <reference types="@vitest/browser/providers/webdriverio" />
475+
```
476+
:::

0 commit comments

Comments
 (0)