···33outline: [2, 3]
44---
5566+<script setup>
77+import MoonPhase from '../../.vitepress/components/MoonPhase.vue'
88+</script>
99+610# Visual Regression Testing
71188-Vitest can run visual regression tests out of the box. It captures screenshots
99-of your UI components and pages, then compares them against reference images to
1010-detect unintended visual changes.
1212+Vitest can run visual regression tests out of the box. It captures screenshots of your UI components and pages, then compares them against reference images to detect unintended visual changes.
11131212-Unlike functional tests that verify behavior, visual tests catch styling issues,
1313-layout shifts, and rendering problems that might otherwise go unnoticed without
1414-thorough manual testing.
1414+Unlike functional tests that verify behavior, visual tests catch styling issues, layout shifts, and rendering problems that might otherwise go unnoticed without thorough manual testing.
15151616-## Why Visual Regression Testing?
1616+## Why visual regression testing?
17171818-Visual bugs don’t throw errors, they just look wrong. That’s where visual
1919-testing comes in.
1818+Visual bugs don’t throw errors, they just look wrong. That’s where visual testing comes in.
20192120- That button still submits the form... but why is it hot pink now?
2221- The text fits perfectly... until someone views it on mobile
2323-- Everything works great... except those two containers are out of viewport
2222+- Everything works great... except those two containers are outside the viewport
2423- That careful CSS refactor works... but broke the layout on a page no one tests
25242626-Visual regression testing acts as a safety net for your UI, automatically
2727-catching these visual changes before they reach production.
2525+Visual regression testing acts as a safety net for your UI, automatically catching these visual changes before they reach production.
28262929-## Getting Started
2727+## Example
30283131-::: warning Browser Rendering Differences
3232-Visual regression tests are **inherently unstable across different
3333-environments**. Screenshots will look different on different machines because
3434-of:
3535-3636-- Font rendering (the big one. Windows, macOS, Linux, they all render text
3737-differently)
3838-- GPU drivers and hardware acceleration
3939-- Whether you're running headless or not
4040-- Browser settings and versions
4141-- ...and honestly, sometimes just the phase of the moon
4242-4343-That's why Vitest includes the browser and platform in screenshot names (like
4444-`button-chromium-darwin.png`).
4545-4646-For stable tests, use the same environment everywhere. We **strongly recommend**
4747-cloud services like
4848-[Azure App Testing](https://azure.microsoft.com/en-us/products/app-testing/)
4949-or [Docker containers](https://playwright.dev/docs/docker).
5050-:::
5151-5252-Visual regression testing in Vitest can be done through the
5353-[`toMatchScreenshot` assertion](/api/browser/assertions.html#tomatchscreenshot):
2929+Visual regression testing in Vitest can be done through the [`toMatchScreenshot` assertion](/api/browser/assertions#tomatchscreenshot):
54305531```ts
5632import { expect, test } from 'vitest'
5733import { page } from 'vitest/browser'
58345959-test('hero section looks correct', async () => {
6060- // ...the rest of the test
3535+test('button renders in default state', async () => {
3636+ // render your component
61376238 // capture and compare screenshot
6363- await expect(page.getByTestId('hero')).toMatchScreenshot('hero-section')
3939+ await expect(page.getByRole('button')).toMatchScreenshot()
6440})
6541```
66426767-### Creating References
4343+## Getting started
68446969-When you run a visual test for the first time, Vitest creates a reference (also
7070-called baseline) screenshot and fails the test with the following error message:
4545+### Environmental stability
4646+4747+Visual regression tests are **sensitive to environmental differences** because rendering is not perfectly deterministic across environments and depends on multiple factors:
4848+4949+- GPU, drivers, and hardware acceleration
5050+- Operating System
5151+- Font rendering pipelines
5252+- Browser, browser versions, and settings
5353+- Whether the browser is running headless or headed
5454+- Screen scaling, color profiles, and display settings
5555+- ...and occasionally what feels like the phase of the moon <MoonPhase />
5656+5757+In practice, even seemingly identical environments can occasionally produce subtle rendering differences. For this reason, **visual regression tests are most reliable when run in a standardized and tightly controlled environment**. This is also why [Docker containers](https://playwright.dev/docs/docker), [CI-only visual testing workflows, or cloud services](#visual-testing-for-teams) are strongly recommended.
5858+5959+### Not a replacement for behavior testing
6060+6161+When a visual test fails alongside behavior tests, it's harder to tell what's actually broken or why. Visual failures are also expected during intentional UI work, but a failing unit test usually is not. Keeping them separate means each suite can fail loudly for the right reasons.
6262+6363+It's worth calling out that **`toMatchScreenshot` is not a substitute for proper assertions**.
6464+6565+A test that renders a button and just takes a screenshot is just documenting the current state. There's no way to tell from a screenshot whether users can interact with the button. **Visual tests work best as a complementary layer on top of behavior tests, not a replacement for them**.
6666+6767+Put another way, **visual testing doesn't tell you why something renders the way it does**. It just tells you that something rendered a certain way, or a different way than it did last time.
6868+6969+For example, take a business requirement to sort recent purchases in a table by purchase date. If you're looking only at the visual regression tests, you might notice that the same items from the last test are in a different order. This could be because you just introduced the sorting or because the sorting is broken. Either way, you don't know why the order is different just by looking at the UI. Someone could dismiss the visual diff as noise because the table "looks the same", even though the ordering logic is now broken. Now you have a broken business requirement in production.
7070+7171+### Project structure
7272+7373+Separating your visual suite from other tests gives you cleaner failure signals and a more deliberate update workflow. The recommended setup uses [projects](/guide/projects) with a `[name].vrt.test.[ext]` naming convention to keep them distinct, and runs them in headless mode for consistency. As the browser instance might have a different default size, it also sets a specific viewport size.
7474+7575+```ts [vitest.config.ts]
7676+import { defaultExclude, defineConfig } from 'vitest/config'
7777+7878+const vrtPattern = '**/*.vrt.test.[tj]s?(x)'
7979+8080+export default defineConfig({
8181+ test: {
8282+ // ...other configurations
8383+ projects: [
8484+ {
8585+ extends: true,
8686+ test: {
8787+ name: 'unit',
8888+ exclude: [vrtPattern, ...defaultExclude],
8989+ },
9090+ },
9191+ {
9292+ extends: true,
9393+ test: {
9494+ name: 'vrt',
9595+ browser: {
9696+ headless: true,
9797+ instances: [
9898+ {
9999+ browser: '[browser-name]',
100100+ viewport: { width: 1280, height: 720 },
101101+ },
102102+ ],
103103+ },
104104+ include: [vrtPattern],
105105+ },
106106+ },
107107+ ],
108108+ },
109109+})
110110+```
111111+112112+With this configuration in place, add scripts to launch each project separately:
113113+114114+```json [package.json]
115115+{
116116+ "scripts": {
117117+ "test:unit": "vitest --project unit",
118118+ "test:visual": "vitest --project vrt"
119119+ }
120120+}
121121+```
122122+123123+### Creating references
124124+125125+When you run a visual test for the first time, Vitest creates a reference (also called baseline) screenshot and fails the test with the following error message:
7112672127```
73128expect(element).toMatchScreenshot()
···75130No existing reference screenshot found; a new one was created. Review it before running tests again.
7613177132Reference screenshot:
7878- tests/__screenshots__/hero.test.ts/hero-section-chromium-darwin.png
133133+ tests/__screenshots__/button.vrt.test.ts/button-default-state-chromium-darwin.png
79134```
801358181-This is normal. Check that the screenshot looks right, then run the test again.
8282-Vitest will now compare future runs against this baseline.
136136+This is normal. Check that the screenshot looks right, then run the test again. Vitest will now compare future runs against this baseline.
8313784138::: tip
8585-Reference screenshots live in `__screenshots__` folders next to your tests.
8686-**Don't forget to commit them!**
139139+Reference screenshots live in `__screenshots__` folders next to your tests. **Commit them to your repository.**
87140:::
881418989-### Screenshot Organization
142142+### Screenshot organization
9014391144By default, screenshots are organized as:
9214593146```
94147.
95148├── __screenshots__
9696-│ └── test-file.test.ts
149149+│ └── test-file.vrt.test.ts
97150│ ├── test-name-chromium-darwin.png
98151│ ├── test-name-firefox-linux.png
99152│ └── test-name-webkit-win32.png
100100-└── test-file.test.ts
153153+└── test-file.vrt.test.ts
101154```
102155103156The naming convention includes:
104104-- **Test name**: either the first argument of the `toMatchScreenshot()` call,
105105-or automatically generated from the test's name.
106106-- **Browser name**: `chrome`, `chromium`, `firefox` or `webkit`.
107107-- **Platform**: `aix`, `darwin`, `freebsd`, `linux`, `openbsd`, `sunos`, or
108108-`win32`.
157157+- **Test name**: either the first argument of the `toMatchScreenshot()` call, or automatically generated from the test's name.
158158+- **Browser name**: depends on the configured browser provider, for example `chrome`, `chromium`, `firefox` or `webkit`.
159159+- **Platform**: `aix`, `darwin`, `freebsd`, `linux`, `openbsd`, `sunos`, or `win32`.
109160110161This ensures screenshots from different environments don't overwrite each other.
111162112112-### Updating References
163163+### Updating references
113164114114-When you intentionally change your UI, you'll need to update the reference
115115-screenshots:
165165+When you intentionally change your UI, you'll need to update the reference screenshots just as you would update snapshots:
116166117167```bash
118118-$ vitest --update
168168+$ vitest --project vrt --update
119169```
120170121121-Review updated screenshots before committing to make sure changes are
122122-intentional.
171171+Review updated screenshots before committing to make sure changes are intentional.
123172124124-## How Visual Tests Work
173173+::: warning Stale screenshots
174174+Note that **screenshots for deleted or renamed tests aren't removed automatically**. Clean up the `__screenshots__` folder manually when you remove or rename tests, otherwise stale references will accumulate over time.
175175+:::
125176126126-Visual regression tests need stable screenshots to compare against. But pages aren't instantly stable as images load, animations finish, fonts render, and layouts settle.
177177+### Debugging failed tests
127178128128-Vitest handles this automatically through "Stable Screenshot Detection":
179179+When a visual test fails, Vitest provides three images to help debug:
129180130130-1. Vitest takes a first screenshot (or uses the reference screenshot if available) as baseline
131131-1. It takes another screenshot and compares it with the baseline
132132- - If the screenshots match, the page is stable and testing continues
133133- - If they differ, Vitest uses the newest screenshot as the baseline and repeats
134134-1. This continues until stability is achieved or the timeout is reached
181181+1. **Reference screenshot**: the expected baseline image
182182+1. **Actual screenshot**: what was captured during the test
183183+1. **Diff image**: highlights the differences; only generated when the screenshots have the same dimensions (behavior may vary with custom matchers)
135184136136-This ensures that transient visual changes (like loading spinners or animations) don't cause false failures. If something never stops animating though, you'll hit the timeout, so consider [disabling animations during testing](#disable-animations).
185185+You'll see something like this in the CLI output:
137186138138-If a stable screenshot is captured after retries (one or more) and a reference screenshot exists, Vitest performs a final comparison with the reference using `createDiff: true`. This will generate a diff image if they don't match.
187187+```
188188+expect(element).toMatchScreenshot()
139189140140-During stability detection, Vitest calls comparators with `createDiff: false` since it only needs to know if screenshots match. This keeps the detection process fast.
190190+Screenshot does not match the stored reference.
191191+245 pixels (ratio 0.03) differ.
141192142142-## Configuring Visual Tests
193193+Reference screenshot:
194194+ tests/__screenshots__/button.vrt.test.ts/button-chromium-darwin.png
143195144144-### Global Configuration
196196+Actual screenshot:
197197+ tests/.vitest/attachments/button.vrt.test.ts/button-chromium-darwin-actual.png
145198146146-Configure visual regression testing defaults in your
147147-[Vitest config](/config/browser/expect#tomatchscreenshot):
199199+Diff image:
200200+ tests/.vitest/attachments/button.vrt.test.ts/button-chromium-darwin-diff.png
201201+```
148202149149-```ts [vitest.config.ts]
203203+While in UI mode, Vitest shows a tabbed diff view with an A/B slider as shown below.
204204+205205+<center>
206206+ <img alt="Animated demo of the visual regression diff view, switching tabs and using the slider to reveal differences" img-light src="/visual-regression/diff-view-light.avif">
207207+ <img alt="Animated demo of the visual regression diff view, switching tabs and using the slider to reveal differences" img-dark src="/visual-regression/diff-view-dark.avif">
208208+209209+ <sup>An example of the visual regression diff UI, showing the "Diff", "Reference", "Actual", and "Slider" tabs, and how the slider reveals unexpected visual changes in a component.</sup>
210210+</center>
211211+212212+#### Understanding the diff image
213213+214214+- **Red pixels** are areas that differ between reference and actual
215215+- **Yellow pixels** are anti-aliasing differences (when anti-alias is not ignored)
216216+- **Transparent/original** are unchanged areas
217217+218218+:::tip
219219+If the diff is mostly red, something's really wrong. If it's speckled with a few red pixels around text, you probably just need to bump your threshold.
220220+:::
221221+222222+## Configuring the `toMatchScreenshot` assertion
223223+224224+It's possible to configure the `toMatchScreenshot` assertion either globally, by changing its default options, or on a per-test basis.
225225+226226+To change the defaults, you have to change the [Vitest config](/config/browser/expect#tomatchscreenshot):
227227+228228+```ts{6-16} [vitest.config.ts]
150229import { defineConfig } from 'vitest/config'
151230152231export default defineConfig({
···168247})
169248```
170249171171-### Per-Test Configuration
250250+For more fine-grained control, override global settings in specific tests by passing options directly to the assertion:
172251173173-Override global settings for specific tests:
174174-175175-```ts
176176-await expect(element).toMatchScreenshot('button-hover', {
252252+```ts{2-6}
253253+await expect(element).toMatchScreenshot('button', {
177254 comparatorName: 'pixelmatch',
178255 comparatorOptions: {
179256 // more lax comparison for text-heavy elements
···182259})
183260```
184261185185-## Best Practices
262262+## Third-party comparators
186263187187-### Test Specific Elements
264264+Vitest ships with `pixelmatch` as its built-in comparator. It's fast, compares images pixel-by-pixel, has no native dependencies, and handles the majority of cases well. Perceptual comparators aren't included by default because they bring heavier dependencies and there's no clear single "best one" to pick as different algorithms make different trade-offs, but the comparator API exists precisely to let you plug in whatever fits your needs. This decision may change as the ecosystem matures, though.
188265189189-Unless you explicitly want to test the whole page, prefer capturing specific
190190-components to reduce false positives:
266266+For use cases where pixel-level diffing produces excessive noise, a perceptual or structural similarity comparator may be a better fit. These compare images more like a human would, tolerating minor rendering differences while still detecting meaningful visual changes.
267267+268268+There are many algorithms, so these are a useful starting point:
269269+270270+- [`@blazediff/ssim`](https://blazediff.dev/docs/ssim), [SSIM (Structural Similarity Index)](https://en.wikipedia.org/wiki/Structural_similarity_index_measure) implementations for perceptual image quality assessment. It offers standard SSIM, MS-SSIM (Multi-Scale SSIM), and Hitchhiker’s SSIM for various use cases
271271+- [`@blazediff/gmsd`](https://blazediff.dev/docs/gmsd), a single-threaded GMSD (Gradient Magnitude Similarity Deviation) metric for perceptual image quality assessment, good for CI environments
272272+273273+To use one, install and register it:
274274+275275+```ts{5-11,18-46} [vitest.config.ts]
276276+import ssim from '@blazediff/ssim/ssim'
277277+import type { SsimOptionsExtended } from '@blazediff/ssim/ssim'
278278+import { defineConfig } from 'vitest/config'
279279+280280+declare module 'vitest/browser' {
281281+ interface ScreenshotComparatorRegistry {
282282+ 'standard-ssim': SsimOptionsExtended & {
283283+ threshold?: number
284284+ }
285285+ }
286286+}
287287+288288+export default defineConfig({
289289+ test: {
290290+ browser: {
291291+ expect: {
292292+ toMatchScreenshot: {
293293+ comparators: {
294294+ // naive implementation, always check the library's docs
295295+ 'standard-ssim': (
296296+ reference,
297297+ actual,
298298+ { createDiff, ...options }
299299+ ) => {
300300+ const diffBuffer = createDiff
301301+ ? new Uint8Array(reference.data.length)
302302+ : undefined
303303+304304+ const output = ssim(
305305+ reference.data,
306306+ actual.data,
307307+ diffBuffer,
308308+ reference.metadata.width,
309309+ reference.metadata.height,
310310+ options,
311311+ )
312312+313313+ const pass = output >= (options.threshold ?? 0.95)
314314+315315+ return {
316316+ pass,
317317+ diff: diffBuffer ?? null,
318318+ message: pass ? null : `SSIM score: ${output}.`,
319319+ }
320320+ },
321321+ },
322322+ },
323323+ },
324324+ },
325325+ },
326326+})
327327+```
328328+329329+Once registered, the comparator can be referenced by name in your config or on a per-test basis:
330330+331331+:::code-group
332332+333333+```ts{8} [vitest.config.ts]
334334+import { defineConfig } from 'vitest/config'
335335+336336+export default defineConfig({
337337+ test: {
338338+ browser: {
339339+ expect: {
340340+ toMatchScreenshot: {
341341+ comparatorName: 'standard-ssim',
342342+ },
343343+ },
344344+ },
345345+ },
346346+})
347347+```
348348+349349+```ts{2} [button.vrt.test.tsx]
350350+await expect(button).toMatchScreenshot('button', {
351351+ comparatorName: 'standard-ssim',
352352+})
353353+```
354354+355355+:::
356356+357357+## Best practices
358358+359359+### Test specific elements
360360+361361+Unless you explicitly want to test the whole page, prefer capturing specific components to reduce false positives:
191362192363```ts
193364// ❌ Captures entire page; prone to unrelated changes
194365await expect(page).toMatchScreenshot()
195366196367// ✅ Captures only the component under test
197197-await expect(page.getByTestId('product-card')).toMatchScreenshot()
368368+await expect(
369369+ page.getByRole('article', { name: 'Tote bag' })
370370+).toMatchScreenshot()
198371```
199372200200-### Handle Dynamic Content
373373+### Handle dynamic content
201374202202-Dynamic content like timestamps, user data, or random values will cause tests
203203-to fail. You can either mock the sources of dynamic content or mask them when
204204-using the Playwright provider by using the
205205-[`mask` option](https://playwright.dev/docs/api/class-page#page-screenshot-option-mask)
206206-in `screenshotOptions`.
375375+Dynamic content like timestamps, user data, or random values will cause tests to fail. Either mock the underlying data sources or mask them using the [`mask` option](https://playwright.dev/docs/api/class-page#page-screenshot-option-mask) in `screenshotOptions` when using the Playwright provider.
207376208208-```ts
209209-await expect(page.getByTestId('profile')).toMatchScreenshot({
377377+```ts{8}
378378+const profile = page.getByRole(
379379+ 'article',
380380+ { name: 'Gracie\'s profile' },
381381+)
382382+383383+await expect(profile).toMatchScreenshot({
210384 screenshotOptions: {
211211- mask: [page.getByTestId('last-seen')],
385385+ mask: [profile.getByRole('status')],
212386 },
213387})
214388```
215389216216-### Disable Animations
217217-218218-Animations can cause flaky tests. Disable them during testing by injecting
219219-a custom CSS snippet:
220220-221221-```css
222222-*, *::before, *::after {
223223- animation-duration: 0s !important;
224224- animation-delay: 0s !important;
225225- transition-duration: 0s !important;
226226- transition-delay: 0s !important;
227227-}
228228-```
390390+### Disable animations
229391230392::: tip
231231-When using the Playwright provider, animations are automatically disabled
232232-when using the assertion: the `animations` option's value in `screenshotOptions`
233233-is set to `"disabled"` by default.
393393+When using the Playwright provider, animations are automatically disabled when using the built-in assertion: the `animations` option's value in `screenshotOptions` is set to `"disabled"` by default.
394394+395395+If you prefer to disable all animations to save some execution time, continue reading.
234396:::
235397236236-### Set Appropriate Thresholds
237237-238238-Tuning thresholds is tricky. It depends on the content, test environment,
239239-what's acceptable for your app, and might also change based on the test.
240240-241241-Vitest does not set a default for the mismatching pixels, that's up for the
242242-user to decide based on their needs. The recommendation is to use
243243-`allowedMismatchedPixelRatio`, so that the threshold is computed on the size
244244-of the screenshot and not a fixed number.
245245-246246-When setting both `allowedMismatchedPixelRatio` and
247247-`allowedMismatchedPixels`, Vitest uses whichever limit is stricter.
248248-249249-### Set consistent viewport sizes
250250-251251-As the browser instance might have a different default size, it's best to
252252-set a specific viewport size, either on the test or the instance
253253-configuration:
398398+Animations can cause flaky tests. Disable them during testing by injecting a custom CSS snippet using [`setupFiles`](/config/setupfiles) or directly in your tests:
254399255400```ts
256256-await page.viewport(1280, 720)
401401+const stylesheet = document.createElement('style')
402402+403403+stylesheet.textContent = /* css */`
404404+ *, *::before, *::after {
405405+ animation-duration: 0s !important;
406406+ animation-delay: 0s !important;
407407+ transition-duration: 0s !important;
408408+ transition-delay: 0s !important;
409409+ }
410410+`
411411+412412+document.head.appendChild(stylesheet)
257413```
258414259259-```ts [vitest.config.ts]
260260-import { playwright } from '@vitest/browser-playwright'
261261-import { defineConfig } from 'vitest/config'
415415+Alternatively, you can declare the CSS in a custom HTML template by using [`browser.testerHtmlPath`](/config/browser/testerhtmlpath).
262416263263-export default defineConfig({
264264- test: {
265265- browser: {
266266- enabled: true,
267267- provider: playwright(),
268268- instances: [
269269- {
270270- browser: 'chromium',
271271- viewport: { width: 1280, height: 720 },
272272- },
273273- ],
274274- },
275275- },
276276-})
277277-```
417417+### Set appropriate thresholds
418418+419419+Tuning thresholds is tricky. It depends on the content, test environment, what's acceptable for your app, and might also change based on the test.
420420+421421+Vitest does not define a default tolerance for mismatched pixels. The appropriate value depends on your application and environment. The recommendation is to use `allowedMismatchedPixelRatio`, so that the threshold is computed on the size of the screenshot and not a fixed number.
422422+423423+When setting both `allowedMismatchedPixelRatio` and `allowedMismatchedPixels`, Vitest uses whichever limit is stricter.
278424279425### Use Git LFS
280426281281-Store reference screenshots in
282282-[Git LFS](https://github.com/git-lfs/git-lfs?tab=readme-ov-file) if you plan to
283283-have a large test suite.
427427+Store reference screenshots in [Git LFS](https://github.com/git-lfs/git-lfs?tab=readme-ov-file) if you plan to have a large test suite.
284428285285-## Debugging Failed Tests
429429+## Common issues and solutions
286430287287-When a visual test fails, Vitest provides three images to help debug:
431431+### False positives from font rendering
288432289289-1. **Reference screenshot**: the expected baseline image
290290-1. **Actual screenshot**: what was captured during the test
291291-1. **Diff image**: highlights the differences, but this might not get generated
292292-293293-You'll see something like:
294294-295295-```
296296-expect(element).toMatchScreenshot()
297297-298298-Screenshot does not match the stored reference.
299299-245 pixels (ratio 0.03) differ.
300300-301301-Reference screenshot:
302302- tests/__screenshots__/button.test.ts/button-chromium-darwin.png
303303-304304-Actual screenshot:
305305- tests/.vitest/attachments/button.test.ts/button-chromium-darwin-actual.png
306306-307307-Diff image:
308308- tests/.vitest/attachments/button.test.ts/button-chromium-darwin-diff.png
309309-```
310310-311311-### Understanding the diff image
312312-313313-- **Red pixels** are areas that differ between reference and actual
314314-- **Yellow pixels** are anti-aliasing differences (when anti-alias is not ignored)
315315-- **Transparent/original** are unchanged areas
316316-317317-:::tip
318318-If the diff is mostly red, something's really wrong. If it's speckled with a
319319-few red pixels around text, you probably just need to bump your threshold.
320320-:::
321321-322322-## Common Issues and Solutions
323323-324324-### False Positives from Font Rendering
325325-326326-Font availability and rendering varies significantly between systems. Some
327327-possible solutions might be to:
433433+Font availability and rendering varies significantly between systems. Some possible solutions might be to:
328434329435- Use web fonts and wait for them to load:
330436···337443338444- Increase comparison threshold for text-heavy areas:
339445340340- ```ts
341341- await expect(page.getByTestId('article-summary')).toMatchScreenshot({
446446+ ```ts{6-7}
447447+ await expect(
448448+ page.getByRole('article', { name: 'How to grow tomatoes' })
449449+ ).toMatchScreenshot({
342450 comparatorName: 'pixelmatch',
343451 comparatorOptions: {
344452 // 10% of the pixels are allowed to change
···347455 })
348456 ```
349457350350-- Use a cloud service or containerized environment for consistent font rendering.
458458+- [Consider a shared environment setup](#visual-testing-for-teams) for consistent font rendering.
351459352352-### Flaky Tests or Different Screenshot Sizes
460460+### Flaky tests or different screenshot sizes
353461354354-If tests pass and fail randomly, or if screenshots have different dimensions
355355-between runs:
462462+If tests pass and fail randomly, or if screenshots have different dimensions between runs:
356463357464- Wait for everything to load, including loading indicators
358465- Set explicit viewport sizes: `await page.viewport(1920, 1080)`
359466- Check for responsive behavior at viewport boundaries
360467- Check for unintended animations or transitions
361468- Increase test timeout for large screenshots
362362-- Use a cloud service or containerized environment
469469+- [Consider a shared environment setup](#visual-testing-for-teams)
363470364364-## Visual Regression Testing for Teams
471471+## Visual testing for teams
365472366366-Remember when we mentioned visual tests need a stable environment? Well, here's
367367-the thing: your local machine isn't it.
473473+Even with a controlled local setup, references generated on one machine will often fail on another. This matters as soon as more than one person is running the suite.
368474369369-For teams, you've basically got three options:
475475+Running the visual regression suite in a shared environment solves this problem. There are three ways to do this:
370476371371-1. **Self-hosted runners**, complex to set up, painful to maintain
372372-1. **GitHub Actions**, free (for open source), works with any provider
373373-1. **Cloud services**, like
374374-[Azure App Testing](https://azure.microsoft.com/en-us/products/app-testing/),
375375-built for this exact problem
477477+1. **Self-hosted runners** (e.g., Docker images), complex to set up and maintain
478478+1. **Generate references in CI**, which requires some setup
479479+1. **Cloud services**, like [Azure App Testing](https://azure.microsoft.com/en-us/products/app-testing/), built to solve this exact problem, but usually restricted to specific providers and browsers
376480377377-We'll focus on options 2 and 3 since they're the quickest to get running.
481481+Options 2 and 3 are the quickest to get running, so those are covered below.
378482379379-To be upfront, the main trade-offs for each are:
483483+:::: tabs key:shared-environment-vrt
484484+=== GitHub Actions (CI)
380485381381-- **GitHub Actions**: visual tests only run in CI (developers can't run them
382382-locally)
383383-- **Microsoft's service**: works everywhere but costs money and only works
384384-with Playwright
385385-386386-:::: tabs key:vrt-for-teams
387387-=== GitHub Actions
388388-389389-The trick here is keeping visual tests separate from your regular tests,
390390-otherwise, you'll waste hours checking failing logs of screenshot mismatches.
391391-392392-### Organizing Your Tests
393393-394394-First, isolate your visual tests. Stick them in a `visual` folder (or whatever
395395-makes sense for your project):
396396-397397-```json [package.json]
398398-{
399399- "scripts": {
400400- "test:unit": "vitest --exclude tests/visual/*.test.ts",
401401- "test:visual": "vitest tests/visual/*.test.ts"
402402- }
403403-}
404404-```
405405-406406-Now developers can run `npm run test:unit` locally without visual tests getting
407407-in the way. Visual tests stay in CI where the environment is consistent.
408408-409409-::: tip Alternative
410410-Not a fan of glob patterns? You could also use separate
411411-[Test Projects](/guide/projects) instead and run them using:
412412-413413-- `vitest --project unit`
414414-- `vitest --project visual`
415415-:::
416416-417417-### CI Setup
418418-419419-Your CI needs browsers installed. How you do this depends on your provider:
486486+GitHub runners don't have browsers preinstalled. Install them before running tests, using the steps for your provider:
420487421488::: tabs key:provider
422489== Playwright
423490424424-[Playwright](https://npmx.dev/package/playwright) makes this easy. Just pin
425425-your version and add this before running tests:
491491+[Playwright](https://npmx.dev/package/playwright) makes this easy. Just pin your version and add this step before running tests:
426492427493```yaml [.github/workflows/ci.yml]
428494# ...the rest of the workflow
···432498433499== WebdriverIO
434500435435-[WebdriverIO](https://npmx.dev/package/webdriverio) expects you to bring
436436-your own browsers. The folks at
437437-[@browser-actions](https://github.com/browser-actions) have your back:
501501+[WebdriverIO](https://npmx.dev/package/webdriverio) installs browsers automatically if none can be found when a test run starts, but it's recommended to decouple the installation process. To help with this, the folks at [@browser-actions](https://github.com/browser-actions) have packaged scripts to install [Chrome](https://github.com/browser-actions/setup-chrome), [Edge](https://github.com/browser-actions/setup-edge), and [Firefox](https://github.com/browser-actions/setup-firefox) in convenient reusable actions:
438502439503```yaml [.github/workflows/ci.yml]
440504# ...the rest of the workflow
···445509446510:::
447511448448-Then run your visual tests:
512512+Then in your existing workflow run the visual tests:
449513450514```yaml [.github/workflows/ci.yml]
451515# ...the rest of the workflow
···454518 run: npm run test:visual
455519```
456520457457-### The Update Workflow
521521+### The update workflow
458522459459-Here's where it gets interesting. You don't want to update screenshots on every
460460-PR automatically <small>*(chaos!)*</small>. Instead, create a
461461-manually-triggered workflow that developers can run when they intentionally
462462-change the UI.
523523+Running `vitest --update` locally would generate screenshots on your machine, defeating the whole point of a controlled environment. Instead, you need a way to trigger the update in CI where the environment matches the one that runs the tests.
524524+525525+You don't want this to happen automatically on every PR <small>*(chaos!)*</small>. Instead, create a manually-triggered workflow that runs when there are intentional changes to the UI.
463526464527The workflow below:
465528- Only runs on feature branches (never on main)
···477540 <img alt="Action summary after no updates" img-dark src="/vrt-gha-summary-no-update-dark.png">
478541479542::: tip
480480-This is just one approach. Some teams prefer PR comments (`/update-screenshots`),
481481-others use labels. Adjust it to fit your workflow!
543543+This is just one approach. Some prefer PR comments (`/update-screenshots`), others use labels. Adjust it to fit your workflow.
482544483483-The important part is having a controlled way to update baselines.
545545+The important part is having a controlled way to update reference screenshots.
484546:::
485547486548```yaml [.github/workflows/update-screenshots.yml]
···537599 - name: Install Playwright Browsers
538600 run: npx --no playwright install --with-deps --only-shell
539601540540- # the magic happens below 🪄
541602 - name: Update Visual Regression Screenshots
542603 run: npm run test:visual --update
543604···594655 fi
595656```
596657597597-=== Azure App Testing
658658+=== Azure App Testing (Cloud service)
598659599599-Your tests stay local, only the browsers run in the cloud. It's Playwright's
600600-remote browser feature, but Microsoft handles all the infrastructure.
660660+With this method, your tests stay local but the browsers run in the cloud. This is built on top of Playwright's remote browser feature and Azure handles all the infrastructure.
601661602602-### Organizing Your Tests
662662+Everyone uses the same cloud browsers, so references are consistent regardless of who runs them. Tests work locally, you pay only for what you use, and there's nothing to maintain.
603663604604-Keep visual tests separate to control costs. Only tests that actually take
605605-screenshots should use the service.
664664+### Configuration
606665607607-The cleanest approach is using [Test Projects](/guide/projects):
666666+To have Playwright connect to the browsers spawned within the service, you have to update the provider configuration.
608667609609-```ts [vitest.config.ts]
668668+```ts{14-28} [vitest.config.ts]
610669import { env } from 'node:process'
611670import { defineConfig } from 'vitest/config'
612671import { playwright } from '@vitest/browser-playwright'
613672614673export default defineConfig({
615615- // ...global Vite config
616616- tests: {
617617- // ...global Vitest config
674674+ test: {
675675+ // ...other configurations
618676 projects: [
619677 {
620678 extends: true,
621679 test: {
622622- name: 'unit',
623623- include: ['tests/**/*.test.ts'],
624624- // regular config, can use local browsers
625625- },
626626- },
627627- {
628628- extends: true,
629629- test: {
630630- name: 'visual',
631631- // or you could use a different suffix, e.g.,: `tests/**/*.visual.ts?(x)`
632632- include: ['visual-regression-tests/**/*.test.ts?(x)'],
680680+ name: 'vrt',
633681 browser: {
634634- enabled: true,
635682 provider: playwright({
636683 connectOptions: {
637684 wsEndpoint: `${env.PLAYWRIGHT_SERVICE_URL}?${new URLSearchParams({
···650697 headless: true,
651698 instances: [
652699 {
653653- browser: 'chromium',
654654- viewport: { width: 2560, height: 1440 },
700700+ browser: '[browser-name]',
701701+ viewport: { width: 1280, height: 720 },
655702 },
656703 ],
657704 },
705705+ include: [vrtPattern],
658706 },
659707 },
708708+ // ...other projects
660709 ],
661710 },
662711})
663712```
664713665665-Follow the [official guide to create a Playwright Workspace](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/quickstart-run-end-to-end-tests?tabs=playwrightcli&pivots=playwright-test-runner#create-a-workspace).
714714+To create a Playwright Workspace follow the [official guide](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/quickstart-run-end-to-end-tests?tabs=playwrightcli&pivots=playwright-test-runner#create-a-workspace).
666715667716Once your workspace is created, configure Vitest to use it:
6687176697181. **Set the endpoint URL**: following the [official guide](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/quickstart-run-end-to-end-tests?tabs=playwrightcli&pivots=playwright-test-runner#configure-the-browser-endpoint), retrieve the URL and set it as the `PLAYWRIGHT_SERVICE_URL` environment variable.
6707191. **Enable token authentication**: [enable access tokens](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/how-to-manage-authentication?pivots=playwright-test-runner#enable-authentication-using-access-tokens) for your workspace, then [generate a token](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/how-to-manage-access-tokens#generate-a-workspace-access-token) and set it as the `PLAYWRIGHT_SERVICE_ACCESS_TOKEN` environment variable.
671720672672-::: danger Keep that Token Secret!
673673-Never commit `PLAYWRIGHT_SERVICE_ACCESS_TOKEN` to your repository. Anyone with
674674-the token can rack up your bill. Use environment variables locally and secrets
675675-in CI.
721721+::: danger Keep that token secret!
722722+Never commit `PLAYWRIGHT_SERVICE_ACCESS_TOKEN` to your repository. Anyone with the token can rack up your bill. Use environment variables locally and secrets in CI.
676723:::
677724678678-Then split your `test` script like this:
679679-680680-```json [package.json]
681681-{
682682- "scripts": {
683683- "test:visual": "vitest --project visual",
684684- "test:unit": "vitest --project unit"
685685- }
686686-}
687687-```
688688-689689-### Running Tests
725725+### Running tests
690726691727```bash
692728# Local development
693693-npm run test:unit # free, runs locally
729729+npm run test:unit # runs locally using your browsers
694730npm run test:visual # uses cloud browsers
695731696732# Update screenshots
697733npm run test:visual -- --update
698734```
699735700700-The best part of this approach is that it just works:
736736+### CI setup
701737702702-- **Consistent screenshots**, everyone uses the same cloud browsers
703703-- **Works locally**, developers can run and update visual tests on their machines
704704-- **Pay for what you use**, only visual tests consume service minutes
705705-- **No Docker or workflow setups needed**, nothing to manage or maintain
706706-707707-### CI Setup
708708-709709-In your CI, add the secrets:
738738+Add the secrets to your CI configuration:
710739711740```yaml
712741env:
···714743 PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }}
715744```
716745717717-Then run your tests like normal. The service handles the rest.
746746+Then run your tests like normal. The service handles the browser infrastructure.
718747719748::::
720749721721-### So Which One?
750750+### Picking the right option
722751723723-Both approaches work. The real question is what pain points matter most to your
724724-team.
752752+All approaches work. The real question is what pain points matter most to you and your team.
725753726726-If you're already deep in the GitHub ecosystem, GitHub Actions is hard to beat.
727727-Free for open source, works with any browser provider, and you control
728728-everything.
754754+If you're comfortable with containerization, a self-hosted Docker setup gives you a controlled environment without any external dependencies or costs. The downside is maintenance as you own the setup, the browser versions, and any breakage.
729755730730-The downside? That "works on my machine" conversation when someone generates
731731-screenshots locally and they don't match CI expectations anymore.
756756+CI runs work with any browser provider and give you full control, but screenshots can only be generated in CI. If someone runs `vitest --update` locally and commits the result, those references will likely fail on the next CI run. This is preventable by guarding the command behind a CI environment check.
732757733733-A cloud service makes sense if developers need to run visual tests locally.
758758+A cloud service makes sense if you want developers to be able to run and update visual tests locally without risking mismatched references. It becomes even more useful when designers are involved in reviewing changes, or when the push-wait-check-fix-push cycle becomes a real bottleneck.
734759735735-Some teams have designers checking their work or developers who prefer catching
736736-issues before pushing. It allows skipping the push-wait-check-fix-push cycle.
760760+Still on the fence? Start with the CI workflow. You can always move to a container or cloud service later if it becomes a pain point.
737761738738-Still on the fence? Start with GitHub Actions. You can always add a cloud
739739-service later if local testing becomes a pain point.
762762+## Going deeper
763763+764764+### How Vitest ensures screenshot stability
765765+766766+Visual regression tests rely on screenshots remaining stable across runs. In practice, pages are not instantly stable: images load asynchronously, animations finish at different times, fonts render, and layouts settle. To mitigate this, Vitest uses a "Stable Screenshot Detection" strategy:
767767+768768+1. It takes an initial screenshot (or uses the reference screenshot if available) as baseline
769769+1. It takes another screenshot and compares it with the baseline
770770+ - If the screenshots match, the page is stable and testing continues
771771+ - If they differ, Vitest uses the newest screenshot as the baseline and repeats
772772+1. This continues until stability is achieved or the timeout is reached
773773+774774+This ensures that transient visual changes (like loading spinners or animations) don't cause false positives. If something never stops animating, though, you'll hit the timeout, so consider [disabling animations during testing](#disable-animations).
775775+776776+If a stable screenshot is captured after one or more retries and a reference screenshot exists, Vitest performs a final comparison with the reference using `createDiff: true`. This will generate a diff image if they don't match.
777777+778778+During stability detection, Vitest calls comparators with `createDiff: false` since it only needs to know if screenshots match. This keeps the detection process fast.