Skill 16 · Vue Testing Best Practices
Subchapter 16.1
reference/async-component-testing.mdMarkdown5 KBView on GitHub
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.
Also bundled
LICENSEasync/await in test functions for async componentsawait flushPromises() after mounting async componentsflushPromises()defineAsyncComponenttrigger() with await as it returns a PromiseIncorrect:
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:
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')
})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')
})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')
})| 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 |
Note: Dynamic imports (import('./File.vue')) may require additional handling beyond flushPromises() in test environments. Test runners like Vitest handle module resolution differently than runtime bundlers, which can cause timing issues with dynamic imports. If flushPromises() alone doesn’t resolve the component, consider:
await flushPromises() calls in sequencewaitFor() or retry utilities// If flushPromises() isn't sufficient, mock the import
vi.mock('./Widget.vue', () => ({
default: { template: '<div>Widget Content</div>' }
}))
// Or use multiple flush calls for nested async operations
await flushPromises()
await flushPromises()