
Vue Testing Best Practices
Write reliable Vue 3 component tests that await async loads via flushPromises and Vitest patterns.
Overview
Vue Testing Best Practices is an agent skill for the Ship phase that teaches correct async testing patterns for Vue 3 defineAsyncComponent with flushPromises and Vitest.
Install
npx skills add https://github.com/vuejs-ai/skills --skill vue-testing-best-practicesWhat is this skill?
- HIGH-impact gotcha: assertions before async render cause false negatives
- 5-item task checklist: async tests, flushPromises after mount, loading states, error states, awaited trigger()
- Correct vs incorrect mount examples for defineAsyncComponent
- Covers rejected promises for error-state tests and nextTick alongside flushPromises
- Tagged for Vue 3, Vitest, and @vue/test-utils
- HIGH impact rating on async testing gotcha
Adoption & trust: 6.3k installs on skills.sh; 2.5k GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
Your Vitest suite fails or passes randomly because tests assert on async components before imports finish and Vue finishes updating the DOM.
Who is it for?
Indie devs or small teams on Vue 3 + Vitest who use code-splitting and defineAsyncComponent and need one clear async testing recipe.
Skip if: Projects on Vue 2 only, teams wanting E2E Playwright guidance, or backends with no Vue frontend.
When should I use this skill?
Testing Vue 3 async or lazy-loaded components with Vitest and seeing failures or flakes before the component finishes loading.
What do I get? / Deliverables
Tests reliably await async resolution with flushPromises and structured loading/error cases so CI reflects real component behavior before you ship.
- Corrected async test patterns
- Loading and error-state test examples
Recommended Skills
Journey fit
Async component testing is a Ship-phase quality gate before release, not an idea or launch marketing task. Testing subphase is where Vitest and Vue Test Utils patterns prevent false-negative CI runs on defineAsyncComponent.
How it compares
A narrow testing gotcha skill—not a full test generator or Cypress replacement.
Common Questions / FAQ
Who is vue-testing-best-practices for?
Solo builders and frontend-heavy indies using Vue 3, @vue/test-utils, and Vitest who split routes or widgets with defineAsyncComponent.
When should I use vue-testing-best-practices?
During Ship testing when adding or fixing component tests, debugging CI false negatives on lazy imports, or before release when async UI must be covered.
Is vue-testing-best-practices safe to install?
It is documentation-only patterns for local tests; review the Security Audits panel on this page and your repo’s test runner permissions.
SKILL.md
READMESKILL.md - Vue Testing Best Practices
# Use flushPromises for Testing Async Components **Impact: HIGH** - When testing async components created with `defineAsyncComponent`, you must use `await flushPromises()` to ensure the component has loaded before making assertions. Vue updates asynchronously, so tests that don't account for this will make assertions before the component has rendered. ## Task Checklist - [ ] Use `async/await` in test functions for async components - [ ] Call `await flushPromises()` after mounting async components - [ ] Test loading states by making assertions before `flushPromises()` - [ ] Test error states using rejected promises in `defineAsyncComponent` - [ ] Use `trigger()` with `await` as it returns a Promise **Incorrect:** ```javascript import { mount } from '@vue/test-utils' import { defineAsyncComponent } from 'vue' const AsyncWidget = defineAsyncComponent(() => import('./Widget.vue') ) test('renders async component', () => { const wrapper = mount(AsyncWidget) // FAILS: Component hasn't loaded yet expect(wrapper.text()).toContain('Widget Content') }) ``` **Correct:** ```javascript import { mount, flushPromises } from '@vue/test-utils' import { defineAsyncComponent, nextTick } from 'vue' const AsyncWidget = defineAsyncComponent(() => import('./Widget.vue') ) test('renders async component', async () => { const wrapper = mount(AsyncWidget) // Wait for async component to load await flushPromises() expect(wrapper.text()).toContain('Widget Content') }) test('shows loading state initially', async () => { const AsyncWithLoading = defineAsyncComponent({ loader: () => import('./Widget.vue'), loadingComponent: { template: '<div>Loading...</div>' }, delay: 0 }) const wrapper = mount(AsyncWithLoading) // Check loading state immediately expect(wrapper.text()).toContain('Loading...') // Wait for component to load await flushPromises() // Check final state expect(wrapper.text()).toContain('Widget Content') }) ``` ## Testing with Suspense ```javascript import { mount, flushPromises } from '@vue/test-utils' import { Suspense, defineAsyncComponent, h } from 'vue' const AsyncWidget = defineAsyncComponent(() => import('./Widget.vue') ) test('renders async component with Suspense', async () => { const wrapper = mount({ components: { AsyncWidget }, template: ` <Suspense> <AsyncWidget /> <template #fallback> <div>Loading...</div> </template> </Suspense> ` }) // Initially shows fallback expect(wrapper.text()).toContain('Loading...') // Wait for async resolution await flushPromises() // Now shows actual content expect(wrapper.text()).toContain('Widget Content') }) ``` ## Testing Error States ```javascript import { mount, flushPromises } from '@vue/test-utils' import { defineAsyncComponent } from 'vue' test('shows error component on load failure', async () => { const AsyncWithError = defineAsyncComponent({ loader: () => Promise.reject(new Error('Failed to load')), errorComponent: { template: '<div>Error loading component</div>' } }) const wrapper = mount(AsyncWithError) await flushPromises() expect(wrapper.text()).toContain('Error loading component') }) ``` ## Utilities Reference | Utility | Purpose | |---------|---------| | `await flushPromises()` | Resolves all pending promises | | `await nextTick()` | Waits for Vue's next DOM update cycle | | `await wrapper.trigger('click')` | Triggers event and waits for update | ## Dynamic Import Handling **Note:** Dynamic imports (`import('./File.vue')`) may require additional handling beyond `flushPromises()` in test environments. Test runners like Vitest