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/testing-async-await-flushpromises.md
1---2title: Properly Handle Async Updates with nextTick and flushPromises3impact: HIGH4impactDescription: Race conditions and flaky tests occur when async DOM updates or API calls complete after assertions run5type: gotcha6tags: [vue3, testing, async, flushPromises, nextTick, vitest, vue-test-utils, race-condition]7---89# Properly Handle Async Updates with nextTick and flushPromises1011**Impact: HIGH** - Vue updates the DOM asynchronously. Without properly awaiting these updates, tests may assert against stale DOM state, causing intermittent failures and false negatives.1213Use `await` with triggers and `setValue`, use `nextTick` for reactive updates, and use `flushPromises` for external async operations like API calls.1415## Task Checklist1617- [ ] Always await `trigger()` and `setValue()` calls18- [ ] Use `await nextTick()` after programmatic reactive state changes19- [ ] Use `await flushPromises()` for external async operations (API calls, timers)20- [ ] Don't chain multiple `nextTick` calls - use `flushPromises` instead21- [ ] Consider using `waitFor` from testing-library for polling assertions2223**Incorrect:**24```javascript25import { mount } from '@vue/test-utils'26import SearchComponent from './SearchComponent.vue'2728// BAD: Not awaiting trigger - assertion runs before DOM updates29test('search filters results', () => {30const wrapper = mount(SearchComponent)3132wrapper.find('input').setValue('vue') // Missing await!33wrapper.find('button').trigger('click') // Missing await!3435// This assertion likely fails - DOM hasn't updated yet36expect(wrapper.findAll('.result').length).toBe(3)37})3839// BAD: Using nextTick for API calls40test('loads data from API', async () => {41const wrapper = mount(DataLoader)4243await nextTick() // This won't wait for the API call!4445// Assertion runs before fetch completes46expect(wrapper.find('.data').text()).toBe('Loaded data')47})48```4950**Correct:**51```javascript52import { mount, flushPromises } from '@vue/test-utils'53import { nextTick } from 'vue'54import SearchComponent from './SearchComponent.vue'55import DataLoader from './DataLoader.vue'5657// CORRECT: Await trigger and setValue58test('search filters results', async () => {59const wrapper = mount(SearchComponent)6061await wrapper.find('input').setValue('vue')62await wrapper.find('button').trigger('click')6364expect(wrapper.findAll('.result').length).toBe(3)65})6667// CORRECT: Use flushPromises for API calls68test('loads data from API', async () => {69const wrapper = mount(DataLoader)7071// Wait for all pending promises to resolve72await flushPromises()7374expect(wrapper.find('.data').text()).toBe('Loaded data')75})76```7778## When to Use Each Method7980### `await trigger()` / `await setValue()` - User Interactions81```javascript82// These methods return nextTick internally83await wrapper.find('button').trigger('click')84await wrapper.find('input').setValue('new value')85await wrapper.find('form').trigger('submit')86```8788### `await nextTick()` - Programmatic Reactive Updates89```javascript90import { nextTick } from 'vue'9192test('reflects programmatic state changes', async () => {93const wrapper = mount(Counter)9495// Direct state modification (when testing with exposed internals)96wrapper.vm.count = 59798await nextTick() // Wait for Vue to update DOM99100expect(wrapper.find('.count').text()).toBe('5')101})102```103104### `await flushPromises()` - External Async Operations105```javascript106import { flushPromises } from '@vue/test-utils'107108test('displays fetched data', async () => {109const wrapper = mount(UserProfile, {110props: { userId: 1 }111})112113// Wait for component's API call to complete114await flushPromises()115116expect(wrapper.find('.username').text()).toBe('John')117})118119// Sometimes you need multiple flushPromises for chained async operations120test('processes data after fetch', async () => {121const wrapper = mount(DataProcessor)122123await flushPromises() // Wait for fetch124await flushPromises() // Wait for processing triggered by fetch125126expect(wrapper.find('.processed').exists()).toBe(true)127})128```129130## Common Pattern: Combining Methods131```javascript132test('submits form and shows success', async () => {133const wrapper = mount(ContactForm)134135// Fill form (awaiting each interaction)136await wrapper.find('#name').setValue('John')137await wrapper.find('#email').setValue('[email protected]')138139// Submit form140await wrapper.find('form').trigger('submit')141142// Wait for API submission to complete143await flushPromises()144145// Assert success state146expect(wrapper.find('.success-message').exists()).toBe(true)147})148```149150## Testing with MSW or Mock APIs151```javascript152import { flushPromises } from '@vue/test-utils'153import { rest } from 'msw'154import { setupServer } from 'msw/node'155156const server = setupServer(157rest.get('/api/user', (req, res, ctx) => {158return res(ctx.json({ name: 'John' }))159})160)161162test('displays user data', async () => {163const wrapper = mount(UserCard)164165// MSW might require multiple flushPromises166await flushPromises()167await flushPromises()168169expect(wrapper.find('.name').text()).toBe('John')170})171```172173## Reference174- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense)175- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)176