--- title: Use flushPromises for Testing Async Components impact: HIGH impactDescription: Without awaiting async operations, tests make assertions before the component has rendered, causing false negatives type: gotcha tags: [vue3, testing, async, defineAsyncComponent, flushPromises, vitest] --- # 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: '
Loading...
' }, 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: ` ` }) // 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: '
Error loading component
' } }) 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 handle module resolution differently than runtime bundlers, which can cause timing issues with dynamic imports. If `flushPromises()` alone doesn't resolve the component, consider: - Mocking the dynamic import to return the component synchronously - Using multiple `await flushPromises()` calls in sequence - Wrapping assertions in `waitFor()` or retry utilities - Configuring your test runner's module resolution settings ```javascript // If flushPromises() isn't sufficient, mock the import vi.mock('./Widget.vue', () => ({ default: { template: '
Widget Content
' } })) // Or use multiple flush calls for nested async operations await flushPromises() await flushPromises() ``` ## References - [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense) - [Vue.js Async Components Documentation](https://vuejs.org/guide/components/async)