Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 testing best practices with Vitest, Vue Test Utils, component testing, mocking, and Playwright E2E.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/async-component-testing.md
1---2title: Use flushPromises for Testing Async Components3impact: HIGH4impactDescription: Without awaiting async operations, tests make assertions before the component has rendered, causing false negatives5type: gotcha6tags: [vue3, testing, async, defineAsyncComponent, flushPromises, vitest]7---89# Use flushPromises for Testing Async Components1011**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.1213## Task Checklist1415- [ ] Use `async/await` in test functions for async components16- [ ] Call `await flushPromises()` after mounting async components17- [ ] Test loading states by making assertions before `flushPromises()`18- [ ] Test error states using rejected promises in `defineAsyncComponent`19- [ ] Use `trigger()` with `await` as it returns a Promise2021**Incorrect:**2223```javascript24import { mount } from '@vue/test-utils'25import { defineAsyncComponent } from 'vue'2627const AsyncWidget = defineAsyncComponent(() =>28import('./Widget.vue')29)3031test('renders async component', () => {32const wrapper = mount(AsyncWidget)3334// FAILS: Component hasn't loaded yet35expect(wrapper.text()).toContain('Widget Content')36})37```3839**Correct:**4041```javascript42import { mount, flushPromises } from '@vue/test-utils'43import { defineAsyncComponent, nextTick } from 'vue'4445const AsyncWidget = defineAsyncComponent(() =>46import('./Widget.vue')47)4849test('renders async component', async () => {50const wrapper = mount(AsyncWidget)5152// Wait for async component to load53await flushPromises()5455expect(wrapper.text()).toContain('Widget Content')56})5758test('shows loading state initially', async () => {59const AsyncWithLoading = defineAsyncComponent({60loader: () => import('./Widget.vue'),61loadingComponent: { template: '<div>Loading...</div>' },62delay: 063})6465const wrapper = mount(AsyncWithLoading)6667// Check loading state immediately68expect(wrapper.text()).toContain('Loading...')6970// Wait for component to load71await flushPromises()7273// Check final state74expect(wrapper.text()).toContain('Widget Content')75})76```7778## Testing with Suspense7980```javascript81import { mount, flushPromises } from '@vue/test-utils'82import { Suspense, defineAsyncComponent, h } from 'vue'8384const AsyncWidget = defineAsyncComponent(() =>85import('./Widget.vue')86)8788test('renders async component with Suspense', async () => {89const wrapper = mount({90components: { AsyncWidget },91template: `92<Suspense>93<AsyncWidget />94<template #fallback>95<div>Loading...</div>96</template>97</Suspense>98`99})100101// Initially shows fallback102expect(wrapper.text()).toContain('Loading...')103104// Wait for async resolution105await flushPromises()106107// Now shows actual content108expect(wrapper.text()).toContain('Widget Content')109})110```111112## Testing Error States113114```javascript115import { mount, flushPromises } from '@vue/test-utils'116import { defineAsyncComponent } from 'vue'117118test('shows error component on load failure', async () => {119const AsyncWithError = defineAsyncComponent({120loader: () => Promise.reject(new Error('Failed to load')),121errorComponent: { template: '<div>Error loading component</div>' }122})123124const wrapper = mount(AsyncWithError)125126await flushPromises()127128expect(wrapper.text()).toContain('Error loading component')129})130```131132## Utilities Reference133134| Utility | Purpose |135|---------|---------|136| `await flushPromises()` | Resolves all pending promises |137| `await nextTick()` | Waits for Vue's next DOM update cycle |138| `await wrapper.trigger('click')` | Triggers event and waits for update |139140## Dynamic Import Handling141142**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:143144- Mocking the dynamic import to return the component synchronously145- Using multiple `await flushPromises()` calls in sequence146- Wrapping assertions in `waitFor()` or retry utilities147- Configuring your test runner's module resolution settings148149```javascript150// If flushPromises() isn't sufficient, mock the import151vi.mock('./Widget.vue', () => ({152default: { template: '<div>Widget Content</div>' }153}))154155// Or use multiple flush calls for nested async operations156await flushPromises()157await flushPromises()158```159160## References161162- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense)163- [Vue.js Async Components Documentation](https://vuejs.org/guide/components/async)164