Files
agent-skills/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

176 lines
5.1 KiB
Markdown

---
title: Properly Handle Async Updates with nextTick and flushPromises
impact: HIGH
impactDescription: Race conditions and flaky tests occur when async DOM updates or API calls complete after assertions run
type: gotcha
tags: [vue3, testing, async, flushPromises, nextTick, vitest, vue-test-utils, race-condition]
---
# Properly Handle Async Updates with nextTick and flushPromises
**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.
Use `await` with triggers and `setValue`, use `nextTick` for reactive updates, and use `flushPromises` for external async operations like API calls.
## Task Checklist
- [ ] Always await `trigger()` and `setValue()` calls
- [ ] Use `await nextTick()` after programmatic reactive state changes
- [ ] Use `await flushPromises()` for external async operations (API calls, timers)
- [ ] Don't chain multiple `nextTick` calls - use `flushPromises` instead
- [ ] Consider using `waitFor` from testing-library for polling assertions
**Incorrect:**
```javascript
import { mount } from '@vue/test-utils'
import SearchComponent from './SearchComponent.vue'
// BAD: Not awaiting trigger - assertion runs before DOM updates
test('search filters results', () => {
const wrapper = mount(SearchComponent)
wrapper.find('input').setValue('vue') // Missing await!
wrapper.find('button').trigger('click') // Missing await!
// This assertion likely fails - DOM hasn't updated yet
expect(wrapper.findAll('.result').length).toBe(3)
})
// BAD: Using nextTick for API calls
test('loads data from API', async () => {
const wrapper = mount(DataLoader)
await nextTick() // This won't wait for the API call!
// Assertion runs before fetch completes
expect(wrapper.find('.data').text()).toBe('Loaded data')
})
```
**Correct:**
```javascript
import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue'
import SearchComponent from './SearchComponent.vue'
import DataLoader from './DataLoader.vue'
// CORRECT: Await trigger and setValue
test('search filters results', async () => {
const wrapper = mount(SearchComponent)
await wrapper.find('input').setValue('vue')
await wrapper.find('button').trigger('click')
expect(wrapper.findAll('.result').length).toBe(3)
})
// CORRECT: Use flushPromises for API calls
test('loads data from API', async () => {
const wrapper = mount(DataLoader)
// Wait for all pending promises to resolve
await flushPromises()
expect(wrapper.find('.data').text()).toBe('Loaded data')
})
```
## When to Use Each Method
### `await trigger()` / `await setValue()` - User Interactions
```javascript
// These methods return nextTick internally
await wrapper.find('button').trigger('click')
await wrapper.find('input').setValue('new value')
await wrapper.find('form').trigger('submit')
```
### `await nextTick()` - Programmatic Reactive Updates
```javascript
import { nextTick } from 'vue'
test('reflects programmatic state changes', async () => {
const wrapper = mount(Counter)
// Direct state modification (when testing with exposed internals)
wrapper.vm.count = 5
await nextTick() // Wait for Vue to update DOM
expect(wrapper.find('.count').text()).toBe('5')
})
```
### `await flushPromises()` - External Async Operations
```javascript
import { flushPromises } from '@vue/test-utils'
test('displays fetched data', async () => {
const wrapper = mount(UserProfile, {
props: { userId: 1 }
})
// Wait for component's API call to complete
await flushPromises()
expect(wrapper.find('.username').text()).toBe('John')
})
// Sometimes you need multiple flushPromises for chained async operations
test('processes data after fetch', async () => {
const wrapper = mount(DataProcessor)
await flushPromises() // Wait for fetch
await flushPromises() // Wait for processing triggered by fetch
expect(wrapper.find('.processed').exists()).toBe(true)
})
```
## Common Pattern: Combining Methods
```javascript
test('submits form and shows success', async () => {
const wrapper = mount(ContactForm)
// Fill form (awaiting each interaction)
await wrapper.find('#name').setValue('John')
await wrapper.find('#email').setValue('john@example.com')
// Submit form
await wrapper.find('form').trigger('submit')
// Wait for API submission to complete
await flushPromises()
// Assert success state
expect(wrapper.find('.success-message').exists()).toBe(true)
})
```
## Testing with MSW or Mock APIs
```javascript
import { flushPromises } from '@vue/test-utils'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
rest.get('/api/user', (req, res, ctx) => {
return res(ctx.json({ name: 'John' }))
})
)
test('displays user data', async () => {
const wrapper = mount(UserCard)
// MSW might require multiple flushPromises
await flushPromises()
await flushPromises()
expect(wrapper.find('.name').text()).toBe('John')
})
```
## Reference
- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense)
- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)