Test Vue 3 components with Vue Test Utils and Vitest — mount vs shallowMount, finding and triggering DOM, asserting props and emitted events, awaiting async updates, and mocking Pinia stores and Vue Router.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Test Vue 3 components with Vue Test Utils and Vitest — mount vs shallowMount, finding and triggering DOM, asserting props and emitted events, awaiting async updates, and mocking Pinia stores and Vue Router.
This skill makes an AI agent write Vue 3 component tests with @vue/test-utils on Vitest: mounting with props and slots, querying via data-testid, triggering events and awaiting the render queue, asserting emitted() payloads, and wiring createTestingPinia and mocked routers through global.plugins. Trigger it on any Vue 3 + Vite project where components need unit or integration tests.
Core Principles
Default to mount, reach for shallowMount rarely. Stubbing all children tests a skeleton, not the component. Shallow-render only when a child is genuinely heavy (charts, maps) — and stub that one child explicitly instead.
Test the rendered contract: props in, DOM and emitted events out. Never reach into wrapper.vm internals or assert on ref values; those tests survive refactors only by accident.
await every interaction. Vue batches DOM updates; trigger, setValue, and setProps all return promises that resolve after the next tick. A missing await asserts against the stale DOM.
Use data-testid or roles, not class selectors. Tailwind/scoped-CSS classes change with styling work; test IDs change only when behavior does.
Emitted events are the component's API — assert names and payloads.wrapper.emitted('save') returns the calls array; check both that it fired and what it carried.
Real Pinia logic, fake server. With createTestingPinia({ stubActions: false }) plus mocked HTTP you test store-component integration honestly; stubbed actions are for pure-render tests only.
Use wrapper.get() when the element must exist (throws with a clear message); wrapper.find() + .exists() only for asserting absence.
Share mount defaults via a factory: const factory = (props = {}) => mount(Comp, { props: { ...defaults, ...props } }) — not via a mutable module-level wrapper.
Assert emitted() payload equality on the whole calls array (toEqual([[3]])) to catch double-fires for free.
For components using <Teleport>, target the teleport destination with document.querySelector or stub teleport with global.stubs: { teleport: true }.
Test accessibility-relevant output: attributes('aria-expanded'), attributes('disabled') — these are behavior, not styling.
Keep one component per test file mounted fresh per test; restoreMocks: true plus fresh mounts eliminates 90% of cross-test pollution.
Anti-Patterns
wrapper.vm.someRef = 5 to set state. Mutating internals bypasses the component contract; drive state through props, interactions, or store initial state.
Missing await on trigger/setValue/setProps. The assertion sees the previous DOM and passes or fails for the wrong reason.
shallowMount as the default everywhere. Stub names in snapshots (<child-component-stub>) verify nothing about integration.
Mounting the full real router and await router.isReady() for a unit test. Mock $router.push instead; real-router tests belong in a small dedicated navigation suite.
Asserting CSS classes as behavior (expect(wrapper.classes()).toContain('text-red-500')). Assert the state that drives the class (aria-invalid, emitted validation event) instead.
One beforeEach that mounts with a kitchen-sink global config for every test in the file — slot, store, and router config should appear in the tests that need them.
When to Trigger This Skill
A Vue 3 project needs component tests, or @vue/test-utils is in devDependencies.
The user asks how to test props, emits, v-model, slots, or async data fetching in a Vue component.
Tests are flaky from missing await/flushPromises or pollute each other through shared wrappers.
Components depend on Pinia or Vue Router and the user needs them mocked or stubbed in tests.
Migrating Vue 2 (createLocalVue, propsData) tests to the Vue 3 global/props API.