Vitest visual testing plugin
npm install vitest-plugin-vis[![NPM version][npm_image]][npm_url]
[![NPM downloads][downloads_image]][npm_url]
Vitest visual testing plugin allowing you to capture and compare image snapshots automatically and manually.
It requires [Vitest Browser Mode][vitest-browser-mode] to work.
This plugin is inspired by [jest-image-snapshot][jest-image-snapshot],
and extracted from [storybook-addon-vis][storybook-addon-vis] to use directly in Vitest.
``sh
npm install --save-dev vitest-plugin-vis
pnpm add --save-dev vitest-plugin-vis
yarn add --save-dev vitest-plugin-vis
`
The [vitest-plugin-vis][vitest-plugin-vis] plugin can be used without customization.
`ts
// vitest.config.ts
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
import { vis } from 'vitest-plugin-vis/config'
export default defineConfig({
plugins: [vis()],
test: {
// vitest v2
browser: {
enabled: true,
provider: 'playwright',
name: 'chromium',
},
// vitest v3
browser: {
enabled: true,
provider: 'playwright',
instances: [
{ browser: 'chromium' }
]
},
// vitest v4
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' }
]
}
}
})
`
This default configuration will:
- Use the auto preset, taking image snapshot at the end on each rendering test.pixelmatch
- Use as the image comparison method.0 pixels
- Set config to compare image snapshot with a failure threshold of .30000 ms
- Timeout for image comparison is set to .
- Save image snapshots using the default directory structure.
The preset option set up typical visual testing scenarios.
`ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { vis } from 'vitest-plugin-vis/config'
export default defineConfig({
plugins: [
vis({
preset: 'auto' // or 'manual' or 'none'
})
],
})
`
- auto (default): Automatically take a snapshot at the end of each rendering test.manual
- : You control which test(s) should take a snapshot automatically with the setAutoSnapshotOptions() function.none
- : Without preset. Set up your visual testing strategy in vitest.setup.ts.
When using the auto or manual preset,expect().toMatchImageSnapshot()
manual snapshots are enabled. You can take manual snapshot using the matcher,page.toMatchImageSnapshot()
or the for full page snapshot.
If you want to customize the snapshot behavior,
you can set the preset to none and configure your own snapshot strategy in vitest.setup.ts:
`ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { vis } from 'vitest-plugin-vis/config'
export default defineConfig({
plugins: [
vis({ preset: 'none' })
],
test: {
browser: {/ ... /},
setupFiles: ['vitest.setup.ts']
}
})
// vitest.setup.ts
import { vis } from 'vitest-plugin-vis/setup'
vis.setup({
auto: true,
auto: async ({ meta }) => meta['darkOnly'],
auto: {
async light() { document.body.classList.remove('dark') },
async dark() { document.body.classList.add('dark') },
}
})
`
As seen in the example above,
you can configure the auto snapshot strategy to:
- Enable/disable auto snapshot for all tests with auto: true/false,false
- Perform some actions before the snapshot is taken,
- Skip certain snapshots for specific tests by returning in the function,
- Take snapshots for different themes or scenarios by providing an object.
Let's say you have this test:
`ts
// src/components/MyComponent.spec.tsx
describe('MyComponent', () => {
describe('className', () => {
it('can customize className', () => {
// ...
})
})
})
`
By default, when you run the test locally, the image snapshot will be saved in the following path:
`sh`
__vis__/local/__baselines__/components/MyComponent.spec.tsx/MyComponent/className/can-customize-className-auto.png
This path can be broken down into a few parts:
> __vis__/local: snapshotRootDir
This is the snapshotRootDir where the image snapshots folders are placed.snapshotRootDir
When running on CI, the is default to __vis__/.
> __baselines__: baseline folder
This is the folder where the baseline images are saved and used for comparison.
There is also a __results__ folder where the current test run images are saved,__diffs__
and a folder where the diff images are saved if the comparison fails.
> components/MyComponent.spec.tsx: snapshotSubpath
This is part of the path based on the path of the test file relative to the project root.
By default, the plugin will trim the common folder such as src or test from the path to reduce the path length.
If you place your test files in multiple folders,
such as in both tests and src folders,snapshotSubpath
and they might have files with the same name and create conflicting snapshots,
you can use to customize the snapshot sub-path to avoid conflicts.
`ts
// vitest.config.ts
import { storybookVis } from 'storybook-addon-vis/vitest-plugin'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [
storybookVis({
// keep the folder structure
snapshotSubpath: (subpath) => subpath
})
],
// ...
})
`
With the above configuration, the snapshot folder structure will look like this:
`ini`
v __vis__
> # ...
v local # snapshot generated on local machine
> __baselines__
v examples
v button.stories.tsx
snapshot-1.png
snapshot-2.png
v src
v button.stories.tsx
snapshot-1.png
snapshot-2.png
v tests
v button.stories.tsx
snapshot-1.png
snapshot-2.png
v examples
button.stories.tsx
v src
button.stories.tsx
v tests
button.stories.tsx
> MyComponent/className/can-customize-className: snapshotId
This is the ID of the snapshot based on the test name and scope.
This is not customizable.
> auto: snapshotKey
This is the key of the snapshot.
In this case, it is auto because the snapshot is taken automatically at the end of the test.0
If you take a manual snapshot, the key will be , 1, etc.
If you customize it when taking the snapshot,
the global customization will not be used.
You can customize the snapshotRootDir, snapshotSubpath, and snapshotKey with corresponding options:
`ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { vis, trimCommonFolder } from 'vitest-plugin-vis/config'
export default defineConfig({
plugins: [
vis({
snapshotRootDir: ({
ci, // true if running on CI
platform, // process.platform
providerName, // 'playwright' or 'webdriverio'
browserName, // 'chromium', 'firefox', etc.
screenshotFailures, // from browser configbrowser
screenshotDirectory, // from config__vis__/${ci ? platform : 'local'}
}) => ,`
snapshotSubpath: ({ subpath }) => trimCommonFolder(subpath),
// Alphanumeric characters, and underscore are allowed. Dash is not allowed.
snapshotKey: 'auto',
})
]
})
By default, auto snapshots are taken from the document.body element.
You can customize this globally by specifying your selector in the subject option:
`ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { vis } from 'vitest-plugin-vis/config'
export default defineConfig({
plugins: [
vis({
subject: '[data-testid="subject"]'
})
]
})
`
You can also customize the subject per test using the setAutoSnapshotOptions function:
`ts
// some.test.ts
import { page } from 'vitest/browser'
import { expect, it } from 'vitest'
import { render } from 'vitest-browser-react'
import { setAutoSnapshotOptions } from 'vitest-plugin-vis'
it('set your own subject', async () => {
setAutoSnapshotOptions({ subject: '[data-testid="subject"]' })
render(
$3
You can customize the snapshot comparison options globally in the config:
`ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { vis } from 'vitest-plugin-vis/config'export default defineConfig({
plugins: [
vis({
// set a default subject selector (e.g.
[data-testid="subject"]) to capture image snapshot
subject: undefined,
comparisonMethod: 'pixel', // or 'ssim'
// pixelmatch or ssim.js options, depending on comparisonMethod.
diffOptions: undefined,
timeout: 30000,
failureThresholdType: 'pixel',
failureThreshold: 0,
})
]
})
`$3
The main usage of this add-on is to use the
toMatchImageSnapshot matcher.Since it is exposed under the
expect object of vitest,
you typically do not need to import vitest-plugin-vis directly.Because of this, TypeScript may not recognize the matcher.
To address this, you can add the following to your
tsconfig.json:`json
{
"compilerOptions": {
"types": ["vitest-plugin-vis"]
}
}
`Or use the triple-slash reference.
To do that, create a typing file, e.g.
types/vitest-plugin-vis.d.ts:`ts
///
`Make sure to include this file in your
tsconfig.json:`json
{
"files": ["types/vitest-plugin-vis.d.ts"],
// or
"include": ["src", "types"]
}
`Usage
$3
By default, the plugin will use the
auto preset,
which will take a snapshot at the end of each rendering test.You can control how the auto snapshot is taken using the
setAutoSnapshotOptions function:`ts
import { setAutoSnapshotOptions } from 'vitest-plugin-vis'
import { beforeEach, it } from 'vitest'beforeAll(() => {
// Apply options to all tests in the current suite (file)
setAutoSnapshotOptions(/ options /)
})
beforeEach(() => {
// Apply options to all tests in the current scope
setAutoSnapshotOptions(/ options /)
})
it('disable snapshot per test', async () => {
// Apply options to this test only
setAutoSnapshotOptions(/ options /)
})
describe('nested scope', () => {
beforeEach(() => {
// Apply options to all tests in the current scope
setAutoSnapshotOptions(/ options /)
})
})
`It supports options of
expect(...).toMatchImageSnapshot(options):`ts
setAutoSnapshotOptions({
enable: true,
comparisonMethod: 'pixel',
snapshotKey: 'auto',
diffOptions: { threshold: 0.01 },
failureThreshold: 0.01,
failureThresholdType: 'percent',
timeout: 60000
})
`You can also enable/disable auto snapshot by passing boolean:
`ts
// enable/disable auto snapshot
setAutoSnapshotOptions(true / or false /)
`You can also provide additional options, which you can use during theme to enable/disable snapshot for each theme:
`ts
setAutoSnapshotOptions({
skipDark: true
})// in vitest.setup.ts
vis.setup({
auto: {
async dark(options) {
if (options.skipDark) return false
document.body.classList.add('dark')
},
}
})
`$3
You can take snapshots manually:
`ts
// some.test.ts
import { render } from 'vitest-browser-react'
import { page } from 'vitest/browser'
import { it } from 'vitest'it('manual snapshot', async ({ expect }) => {
render(
hello world)
await expect(document.body).toMatchImageSnapshot(/ options /)
// or
const subject = page.getByTestId('subject')
await expect(subject).toMatchImageSnapshot(/ options /)
})
`You can customize the snapshot comparison options per assertion:
`ts
// some.test.ts
import { render } from 'vitest-browser-react'
import { page } from 'vitest/browser'
import { it } from 'vitest'it('manual snapshot with options', async ({ expect }) => {
render(
hello world)
const subject = page.getByTestId('subject')
await expect(subject).toMatchImageSnapshot({
snapshotKey: 'custom',
failureThreshold: 0.01,
failureThresholdType: 'percent',
diffOptions: {
threshold: 0.1
},
timeout: 60000
})
})
`$3
You can also take a full page snapshot:
`ts
import { page } from 'vitest/browser'
import { it } from 'vitest'it('full page snapshot', async () => {
await page.toMatchImageSnapshot({ fullPage: true })
})
`$3
While less common, you can also check if a snapshot exists:
`ts
import { page } from 'vitest/browser'
import { it } from 'vitest'it('Has Snapshot', async ({ expect }) => {
const hasSnapshot = await page.hasImageSnapshot(/ options /)
if (!hasSnapshot) {
// do something
}
else {
// do something else
}
})
`This is useful when you are performing negative test.
$3
While Vitest Browser Mode supports both
playwright and webdriverio,
webdriverio currently does not work well with visual testing.There are two issues we are aware of:
>
element click intercepted: WebDriverError: element click intercepted: Element is not clickable at pointThis occurs in CI when
--window-size is not set.
To work around this issue, you can set the --window-size flag in your config:`ts
// vitest.config.tsexport default {
test: {
browser: {
instances: [
{
browser: 'chrome',
capabilities: {
'goog:chromeOptions': {
args: ['--window-size=1280,720']
}
}
}
]
}
}
}
`>
fullPage is not workingThis occurs when the browser is in
headless mode.
But even when it is not in headless mode,
the resulting snapshot is still not capturing the full page.For the time being, we recommend using
playwright for visual testing.Git Ignore
The local snapshots, current run results, and diffs should be ignored by git.
Add the following lines to your
.gitignore file:`sh
/__vis__//__diffs__
/__vis__//__results__
**/__vis__/local
`Vitest Browser Mode
Vitest visual testing plugin runs on [Vitest Browser Mode][vitest-browser-mode].
Please follow its guide to set up your environment.
Bonus note, if you want to install [Firefox] on WSL,
you can follow these steps: Install Firefox on Ubuntu 22.04.
Also, you may need to
sudo apt-get install xdg-utils to fix xdg-settings: not found.Running on CI
When running on CI, the plugin will save the image snapshots in the
directory.The image snapshots are taken on the server side using
playwright or webdriverio depending on your browser provider.
It is recommended to run your tests serially to avoid flakiness.Migrating from v2
[
vitest-plugin-vis][vitest-plugin-vis] v3 is a number of breaking changes from v2.If you are using
vitest-plugin-vis v2,
you can follow the migration guide here to use v3.> Preset changes
The
enable and manual options are combined as manual.
The only difference between enable and manual was that manual was not capable to take automatic snapshot even when you use setAutoSnapshotOptions in your test.>
platform option is removedThe
platform option is removed.
It is replaced with snapshotRootDir which takes a function to determine the snapshot root directory.>
customizeSnapshotSubpath is replaced with snapshotSubpathThe main difference is that
customizeSnapshotSubpath receives the subpath as a string,
while snapshotSubpath receives { subpath: string }.This change allows us to expand it by adding more properties such as
viewport in the future.>
customizeSnapshotId is replaced with snapshotKeyIn v3, we need a stable snapshot ID to be able to identify snapshots originated from the same test.
We couldn't to that with
customizeSnapshotId.The
snapshotKey has a reduced responsibility of only customizing the snapshot key,
which is added to the end of the snapshot filename.Let's say you have a test file
src/components/x/x.test.ts.
Within that file, you have a test:`ts
// src/components/x/x.test.tsdescribe('some scope', () => {
it('should do something', () => {
// ...
})
})
`By default, the snapshot will be saved in the following path:
`sh
// auto snapshot
__vis__/local/__baselines__/components/x/x.test.ts/some-scope/should-do-something-auto.png// manual snapshot
__vis__/local/__baselines__/components/x/x.test.ts/some-scope/should-do-something-1.png
// auto snapshot map:
{ light() {...}, dark() {...} }
__vis__/local/__baselines__/components/x/x.test.ts/some-scope/should-do-something-light.png
__vis__/local/__baselines__/components/x/x.test.ts/some-scope/should-do-something-dark.png
`The
snapshotKey defined in your config or is setAutoSnapshotOptions() will affect the auto snapshot`ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'export default defineConfig({
plugins: [
vis({
snapshotKey: 'custom'
})
]
})
// or
// src/components/x/x.test.ts
describe('some scope', () => {
it('should do something', () => {
setAutoSnapshotOptions({
snapshotKey: 'custom'
})
// ...
})
})
``sh
// auto snapshot
__vis__/local/__baselines__/components/x/x.test.ts/some-scope/should-do-something-auto.png
// becomes
__vis__/local/__baselines__/components/x/x.test.ts/some-scope/should-do-something-custom.png
`If you define a
snapshotKey in your manual snapshot,
expectedly it will be used for that snapshot only.`ts
// src/components/x/x.test.tsdescribe('some scope', () => {
it('should do something', () => {
expect(document.body).toMatchImageSnapshot({
snapshotKey: 'custom'
})
page.toMatchImageSnapshot({
snapshotKey: 'custom'
})
})
})
``sh
// manual snapshot
__vis__/local/__baselines__/components/x/x.test.ts/some-scope/should-do-something-1.png
// becomes
__vis__/local/__baselines__/components/x/x.test.ts/some-scope/should-do-something-custom.png
`>
subjectDataTestId is replaced with subjectIf you are using
subjectDataTestId in your config,
you can replace it with subject in your config.`ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import { vis } from 'vitest-plugin-vis/config'export default defineConfig({
plugins: [
vis({
// subjectDataTestId: 'subject'
subject: '[data-testid="subject"]'
})
]
})
`FAQ
> feature X in [
jest-image-snapshot][jest-image-snapshot] is missingSome features in [
jest-image-snapshot][jest-image-snapshot] are not implemented in [vitest-plugin-vis`][vitest-plugin-vis] yet.If you have a good use case for these features, please open an issue or PR.
[downloads_image]: https://img.shields.io/npm/dm/vitest-plugin-vis.svg?style=flat
[firefox]: https://www.mozilla.org/en-US/firefox/
[jest-image-snapshot]: https://github.com/americanexpress/jest-image-snapshot
[npm_image]: https://img.shields.io/npm/v/vitest-plugin-vis.svg?style=flat
[npm_url]: https://npmjs.org/package/vitest-plugin-vis
[storybook-addon-vis]: https://github.com/repobuddy/storybook-addon-vis
[vitest-browser-mode]: https://vitest.dev/guide/browser/
[vitest-plugin-vis]: https://www.npmjs.com/package/vitest-plugin-vis