Snapshot Testing Vue Components with Vitest and Jest
Snapshot testing Vue components captures the rendered HTML output and alerts you when it changes. It's useful for catching unintended rendering changes — when a refactor modifies the markup structure, font classes, or element order in ways that aren't covered by functional tests.
This guide covers snapshot testing for Vue 3 components using Vitest (recommended) and Jest with Vue Test Utils.
Setup with Vitest
Vitest is the standard test runner for Vue 3 projects. It ships with snapshot support built in.
npm install --save-dev vitest @vue/test-utils jsdom// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true
}
})Setup with Jest
npm install --save-dev jest @vue/test-utils vue-jest babel-jest// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.js$': 'babel-jest'
}
}Writing Vue Component Snapshots
Basic Component Snapshot
<!-- UserCard.vue -->
<template>
<div class="user-card">
<img :src="avatar" :alt="name" />
<h2>{{ name }}</h2>
<p>{{ role }}</p>
</div>
</template>
<script setup>
defineProps({
name: String,
role: String,
avatar: String
})
</script>// UserCard.test.js
import { mount } from '@vue/test-utils'
import UserCard from './UserCard.vue'
test('renders UserCard correctly', () => {
const wrapper = mount(UserCard, {
props: {
name: 'Alice',
role: 'Senior Engineer',
avatar: 'https://example.com/alice.jpg'
}
})
expect(wrapper.html()).toMatchSnapshot()
})The snapshot stores the rendered HTML. On subsequent runs, Jest/Vitest compares the output against the stored snapshot.
Component with Slots
test('renders card with slot content', () => {
const wrapper = mount(Card, {
slots: {
default: '<p>Slot content here</p>',
header: '<h1>Card Title</h1>'
}
})
expect(wrapper.html()).toMatchSnapshot()
})Component States
Snapshot each meaningful state separately:
describe('Button', () => {
test('default state', () => {
const wrapper = mount(Button, { props: { label: 'Submit' } })
expect(wrapper.html()).toMatchSnapshot()
})
test('loading state', () => {
const wrapper = mount(Button, { props: { label: 'Submit', loading: true } })
expect(wrapper.html()).toMatchSnapshot()
})
test('disabled state', () => {
const wrapper = mount(Button, { props: { label: 'Submit', disabled: true } })
expect(wrapper.html()).toMatchSnapshot()
})
test('danger variant', () => {
const wrapper = mount(Button, { props: { label: 'Delete', variant: 'danger' } })
expect(wrapper.html()).toMatchSnapshot()
})
})Async Components
For components that fetch data on mount:
test('renders after data loads', async () => {
// Mock the API call
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: 'Alice', email: 'alice@example.com' })
}))
const wrapper = mount(UserProfile, { props: { userId: 1 } })
// Wait for async operations
await wrapper.vm.$nextTick()
await flushPromises()
expect(wrapper.html()).toMatchSnapshot()
})Shallow vs Deep Rendering
shallowMount replaces child components with stubs. Useful when you want to test a component's own template without caring about child component rendering.
// Deep mount — renders all child components
const wrapper = mount(ParentComponent)
expect(wrapper.html()).toMatchSnapshot()
// Snapshot includes child component rendered HTML
// Shallow mount — stubs child components
const wrapper = shallowMount(ParentComponent)
expect(wrapper.html()).toMatchSnapshot()
// Snapshot includes child component stubs: <childcomponent-stub />When to use shallow:
- Testing a component's own template structure
- When child components are unstable or complex
When to use deep:
- Testing integration between parent and child components
- When the full rendered output is what you care about
Serializers for Vue Wrappers
By default, snapshotting a Vue wrapper gives you the HTML string. For more structured output, use @vue/test-utils's built-in serializer:
// vitest.config.js / jest.config.js
import { config } from '@vue/test-utils'
// This makes snapshots look like the Vue template structure
expect.addSnapshotSerializer({
test: (val) => val && val.__isVueWrapper,
print: (val) => val.html()
})Or install jest-serializer-vue for Jest:
npm install --save-dev jest-serializer-vue// jest.config.js
module.exports = {
snapshotSerializers: ['jest-serializer-vue']
}Filtering Dynamic Data
Timestamps, IDs, and other dynamic values cause snapshot failures even when nothing meaningful changed.
Option 1: Mock dynamic data
test('renders order', () => {
vi.setSystemTime(new Date('2024-01-15'))
const wrapper = mount(OrderCard, {
props: { order: { id: 'fixed-id', total: 100, createdAt: new Date() } }
})
expect(wrapper.html()).toMatchSnapshot()
vi.useRealTimers()
})Option 2: Replace dynamic attributes before snapshotting
test('renders with stable output', () => {
const wrapper = mount(Component)
// Replace dynamic IDs
const html = wrapper.html().replace(/id="[^"]+"/g, 'id="[id]"')
expect(html).toMatchSnapshot()
})Option 3: Snapshot specific elements
Instead of the full component, snapshot only the parts that are stable:
test('renders product list', () => {
const wrapper = mount(ProductList, { props: { products } })
// Snapshot the product cards, not the outer container with dynamic class
expect(wrapper.find('.product-grid').html()).toMatchSnapshot()
})Inline Snapshots for Short Outputs
For small components, inline snapshots are more readable:
test('renders badge', () => {
const wrapper = mount(Badge, { props: { label: 'New', variant: 'success' } })
expect(wrapper.html()).toMatchInlineSnapshot(`
<span class="badge badge--success">New</span>
`)
})When Not to Use Snapshots for Vue
Don't snapshot:
- Complex components with lots of dynamic data (high maintenance)
- Components that change frequently during development
- Generated markup from third-party libraries (their changes break your snapshots)
Do snapshot:
- Design system components (Button, Input, Card) — stable, high test value
- Components where template structure matters for accessibility (role, aria attributes)
- Components that render conditional markup based on props
Reviewing Snapshot Updates
When you update snapshots, always review what changed:
npx vitest --update-snapshot
# or
npx jest --updateSnapshotLook at the diff before committing. A snapshot update that removes an aria-label attribute is a bug, not a normal update. A snapshot update that adds a new CSS class is probably intentional.
Configure your PR template to remind reviewers to check snapshot diffs:
## Snapshot Changes
If this PR updates snapshots (`*.snap` files or inline `toMatchInlineSnapshot`):
- [ ] I reviewed the snapshot diff and it reflects an intentional change
- [ ] The visual output was verified in the browser / StorybookSummary
Snapshot testing Vue components is most valuable for stable UI components — design system elements, component library pieces — where the rendered structure should change only when deliberately updated. Use shallow mounting when you want to test the component's own template in isolation; use deep mounting for integration-level checks. Keep snapshots small by testing individual states and components separately, and always review snapshot diffs as part of code review.