Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3.5+ Composition API reference with progressive sub-file loading for components, composables, reactivity, and testing.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/testing.md
1# Vue Testing23Test patterns for Vue 3 components, composables, and utilities.45## Quick Reference67| Test Type | Pattern |8| ---------------- | ------------------------------------ |9| Component | `mount(Component, { props, slots })` |10| User interaction | `await wrapper.trigger('click')` |11| Emitted events | `wrapper.emitted('update')` |12| Composable | Call directly, test return values |13| Utils | Pure function testing (easiest) |1415## Stack1617- **Vitest** - test runner18- **@vue/test-utils** - component mounting, interaction19- **@testing-library/vue** - user-centric alternative20- **happy-dom / jsdom** - DOM environment2122## File Location2324Colocate tests with code:2526```27Button.vue → Button.spec.ts28useAuth.ts → useAuth.spec.ts29formatters.ts → formatters.spec.ts30```3132## Component Tests3334### Basic3536```ts37import { mount } from '@vue/test-utils'38import Button from './Button.vue'3940it('renders slot', () => {41const wrapper = mount(Button, {42slots: { default: 'Click me' }43})44expect(wrapper.text()).toBe('Click me')45})4647it('emits on click', async () => {48const wrapper = mount(Button)49await wrapper.trigger('click')50expect(wrapper.emitted('click')).toHaveLength(1)51})52```5354### Props5556```ts57it('applies variant class', () => {58const wrapper = mount(Button, {59props: { variant: 'primary' }60})61expect(wrapper.classes()).toContain('btn-primary')62})63```6465### Emits6667```ts68it('emits update with payload', async () => {69const wrapper = mount(Input)70await wrapper.find('input').setValue('new value')71expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['new value'])72})73```7475### Slots7677```ts78it('renders named slots', () => {79const wrapper = mount(Card, {80slots: {81header: '<h1>Title</h1>',82default: '<p>Content</p>'83}84})85expect(wrapper.html()).toContain('<h1>Title</h1>')86})87```8889## Composable Tests9091Call directly, no mounting needed:9293```ts94import { useCounter } from './useCounter'9596it('increments count', () => {97const { count, increment } = useCounter(0)98expect(count.value).toBe(0)99increment()100expect(count.value).toBe(1)101})102103it('resets to initial', () => {104const { count, increment, reset } = useCounter(5)105increment()106increment()107expect(count.value).toBe(7)108reset()109expect(count.value).toBe(5)110})111```112113## Utils Tests114115Easiest - pure functions:116117```ts118import { formatCurrency, slugify } from './formatters'119120describe('formatCurrency', () => {121it('formats USD', () => {122expect(formatCurrency(10.5)).toBe('$10.50')123})124})125126describe('slugify', () => {127it('converts to lowercase', () => {128expect(slugify('Hello World')).toBe('hello-world')129})130131it('removes special chars', () => {132expect(slugify('Hello! World?')).toBe('hello-world')133})134})135```136137## Mocking138139**Composables:**140141```ts142import { vi } from 'vitest'143144vi.mock('./useAuth', () => ({145useAuth: vi.fn(() => ({146user: { id: 1, name: 'Test' },147isAuthenticated: true148}))149}))150```151152**API calls:**153154```ts155global.fetch = vi.fn(() =>156Promise.resolve({157json: () => Promise.resolve({ data: [] })158})159)160```161162## Router Mocking163164Mock `useRoute` and `useRouter` for component tests:165166```ts167import { vi } from 'vitest'168import { mount } from '@vue/test-utils'169170vi.mock('vue-router', () => ({171useRoute: vi.fn(() => ({172params: { id: '123' },173query: { filter: 'active' },174path: '/users/123',175})),176useRouter: vi.fn(() => ({177push: vi.fn(),178replace: vi.fn(),179})),180}))181182it('uses route params', () => {183const wrapper = mount(UserPage)184expect(wrapper.text()).toContain('123')185})186```187188**Dynamic route mocking per test:**189190```ts191import { useRoute } from 'vue-router'192193it('handles different routes', () => {194vi.mocked(useRoute).mockReturnValue({195params: { id: '456' },196} as any)197198const wrapper = mount(UserPage)199expect(wrapper.text()).toContain('456')200})201```202203## Suspense and Teleport204205**Testing async components with Suspense:**206207```ts208import { flushPromises, mount } from '@vue/test-utils'209210it('renders async content', async () => {211const wrapper = mount(AsyncComponent, {212global: {213stubs: { Suspense: false }, // Don't stub Suspense214},215})216217// Wait for async setup to complete218await flushPromises()219220expect(wrapper.text()).toContain('Loaded content')221})222```223224**Testing Teleport:**225226```ts227it('teleports modal content', () => {228const wrapper = mount(Modal, {229global: {230stubs: {231teleport: true, // Stub teleport to render inline232},233},234})235236expect(wrapper.text()).toContain('Modal content')237})238```239240**Access teleported content:**241242```ts243it('finds teleported content', () => {244document.body.innerHTML = '<div id="modal-target"></div>'245246mount(Modal, { props: { open: true } })247248// Content teleports to #modal-target249expect(document.body.innerHTML).toContain('Modal content')250})251```252253## Best Practices254255**Do:**256257- Test behavior (what user sees/does), not implementation258- Arrange-Act-Assert structure259- One assertion per test260- Descriptive test names261- Mock external dependencies262263**Don't:**264265- Test Vue internals (reactivity)266- Test third-party libraries267- Test trivial getters/setters268- Test implementation details269270## What to Test271272**Test:**273274- User interactions (clicks, inputs)275- Conditional rendering276- Props validation, emitted events277- Computed values, business logic278279**Skip:**280281- Vue internals, third-party libs282- Trivial getters/setters283- Implementation details284285## Running286287```bash288pnpm test # all289pnpm exec vitest Button.spec.ts # specific290pnpm exec vitest --watch # watch291pnpm test --coverage # coverage292```293294**Docs:** [vitest.dev](https://vitest.dev/) · [test-utils.vuejs.org](https://test-utils.vuejs.org/)295