Snapshot Testing Vue Components with Vitest and Jest

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 --updateSnapshot

Look 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 / Storybook

Summary

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.

Read more

Start now free