vitest-plugin-vis
v4.1.0
Published
Vitest visual testing plugin
Maintainers
Readme
Vitest Visual Testing Plugin
Vitest visual testing plugin allowing you to capture and compare image snapshots automatically and manually.
It requires Vitest Browser Mode to work.
This plugin is inspired by jest-image-snapshot,
and extracted from storybook-addon-vis to use directly in Vitest.
Install
npm install --save-dev vitest-plugin-vis
pnpm add --save-dev vitest-plugin-vis
yarn add --save-dev vitest-plugin-visConfig
The vitest-plugin-vis plugin can be used without customization.
// 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
autopreset, taking image snapshot at the end on each rendering test. - Use
pixelmatchas the image comparison method. - Set config to compare image snapshot with a failure threshold of
0 pixels. - Timeout for image comparison is set to
30000 ms. - Save image snapshots using the default directory structure.
preset
The preset option set up typical visual testing scenarios.
// 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 thesetAutoSnapshotOptions()function.none: Without preset. Set up your visual testing strategy invitest.setup.ts.
When using the auto or manual preset,
manual snapshots are enabled. You can take manual snapshot using the expect().toMatchImageSnapshot() matcher,
or the page.toMatchImageSnapshot() 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:
// 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, - Perform some actions before the snapshot is taken,
- Skip certain snapshots for specific tests by returning
falsein the function, - Take snapshots for different themes or scenarios by providing an object.
Customizing snapshot path
Let's say you have this test:
// 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:
__vis__/local/__baselines__/components/MyComponent.spec.tsx/MyComponent/className/can-customize-className-auto.pngThis path can be broken down into a few parts:
__vis__/local:snapshotRootDir
This is the snapshotRootDir where the image snapshots folders are placed.
When running on CI, the snapshotRootDir is default to __vis__/<process.platform>.
__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,
and a __diffs__ 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,
and they might have files with the same name and create conflicting snapshots,
you can use snapshotSubpath to customize the snapshot sub-path to avoid conflicts.
// 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:
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.
If you take a manual snapshot, the key will be 0, 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:
// 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` config
screenshotDirectory, // from `browser` config
}) => `__vis__/${ci ? platform : 'local'}`,
snapshotSubpath: ({ subpath }) => trimCommonFolder(subpath),
// Alphanumeric characters, and underscore are allowed. Dash is not allowed.
snapshotKey: 'auto',
})
]
})Customizing auto snapshot subject
By default, auto snapshots are taken from the document.body element.
You can customize this globally by specifying your selector in the subject option:
// 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:
// 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(<div data-testid="subject">hello world</div>)
})Customizing snapshot comparison options
You can customize the snapshot comparison options globally in the config:
// 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,
})
]
})TypeScript Configuration
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:
{
"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:
/// <reference types="vitest-plugin-vis" />Make sure to include this file in your tsconfig.json:
{
"files": ["types/vitest-plugin-vis.d.ts"],
// or
"include": ["src", "types"]
}Usage
Automatic snapshots
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:
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):
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:
// 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:
setAutoSnapshotOptions({
skipDark: true
})
// in vitest.setup.ts
vis.setup({
auto: {
async dark(options) {
if (options.skipDark) return false
document.body.classList.add('dark')
},
}
})Manual Snapshots
You can take snapshots manually:
// some.test.ts
import { render } from 'vitest-browser-react'
import { page } from 'vitest/browser'
import { it } from 'vitest'
it('manual snapshot', async ({ expect }) => {
render(<div data-testid="subject">hello world</div>)
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:
// 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(<div data-testid="subject">hello world</div>)
const subject = page.getByTestId('subject')
await expect(subject).toMatchImageSnapshot({
snapshotKey: 'custom',
failureThreshold: 0.01,
failureThresholdType: 'percent',
diffOptions: {
threshold: 0.1
},
timeout: 60000
})
})Full Page Snapshot
You can also take a full page snapshot:
import { page } from 'vitest/browser'
import { it } from 'vitest'
it('full page snapshot', async () => {
await page.toMatchImageSnapshot({ fullPage: true })
})Has Snapshot
While less common, you can also check if a snapshot exists:
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.
webdriverio Support
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 point
This occurs in CI when --window-size is not set.
To work around this issue, you can set the --window-size flag in your config:
// vitest.config.ts
export default {
test: {
browser: {
instances: [
{
browser: 'chrome',
capabilities: {
'goog:chromeOptions': {
args: ['--window-size=1280,720']
}
}
}
]
}
}
}
fullPageis not working
This 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:
**/__vis__/**/__diffs__
**/__vis__/**/__results__
**/__vis__/localVitest Browser Mode
Vitest visual testing plugin runs on 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 <root>/__vis__/<process.platform> 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 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.
platformoption is removed
The platform option is removed.
It is replaced with snapshotRootDir which takes a function to determine the snapshot root directory.
customizeSnapshotSubpathis replaced withsnapshotSubpath
The 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.
customizeSnapshotIdis replaced withsnapshotKey
In 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:
// src/components/x/x.test.ts
describe('some scope', () => {
it('should do something', () => {
// ...
})
})By default, the snapshot will be saved in the following path:
// 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.pngThe snapshotKey defined in your config or is setAutoSnapshotOptions() will affect the auto snapshot
// 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'
})
// ...
})
})// 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.pngIf you define a snapshotKey in your manual snapshot,
expectedly it will be used for that snapshot only.
// src/components/x/x.test.ts
describe('some scope', () => {
it('should do something', () => {
expect(document.body).toMatchImageSnapshot({
snapshotKey: 'custom'
})
page.toMatchImageSnapshot({
snapshotKey: 'custom'
})
})
})// 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
subjectDataTestIdis replaced withsubject
If you are using subjectDataTestId in your config,
you can replace it with subject in your config.
// 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-snapshotis missing
Some features in jest-image-snapshot are not implemented in vitest-plugin-vis yet.
This is because through our own usage, we do not found a good use case for them.
If you have a good use case for these features, please open an issue or PR.
