Vue.js
async mounted
testing
JavaScript
front-end development

How to wait for async mounted of Vue component to finish before continuing with testing

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Vue component tests often fail when assertions run before asynchronous work inside mounted or lifecycle-triggered methods completes. Waiting for one tick is not always enough because network calls, timers, and chained promises may still be pending. Reliable tests explicitly wait for both Vue DOM updates and promise resolution.

Why nextTick Alone Is Sometimes Insufficient

await nextTick() waits for Vue to flush reactive DOM updates scheduled in the current cycle. It does not automatically wait for pending promises created by API calls.

If mounted does this sequence, one tick is not enough:

  • set loading flag
  • call async function
  • receive response
  • update state
  • render final DOM

You usually need to flush promises and then wait for Vue render ticks.

Use flush-promises plus nextTick in a deterministic order.

typescript
1import { mount } from '@vue/test-utils'
2import { nextTick } from 'vue'
3import flushPromises from 'flush-promises'
4import UserPanel from './UserPanel.vue'
5
6vi.mock('./api', () => ({
7  fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Mina' })
8}))
9
10test('renders user name after mounted fetch', async () => {
11  const wrapper = mount(UserPanel)
12
13  await flushPromises()
14  await nextTick()
15
16  expect(wrapper.text()).toContain('Mina')
17  expect(wrapper.find('[data-test="loading"]').exists()).toBe(false)
18})

This pattern works because promise callbacks run first, then Vue applies resulting state changes to the DOM.

Component Example for Context

A minimal component with async mounted logic:

vue
1<script setup lang="ts">
2import { ref, onMounted } from 'vue'
3import { fetchUser } from './api'
4
5const loading = ref(true)
6const userName = ref('')
7
8onMounted(async () => {
9  const user = await fetchUser()
10  userName.value = user.name
11  loading.value = false
12})
13</script>
14
15<template>
16  <div>
17    <p v-if="loading" data-test="loading">Loading...</p>
18    <p v-else data-test="name">{{ userName }}</p>
19  </div>
20</template>

In tests, assert the loading state first when useful, then flush and assert final state.

Handling Timers and Debounced Logic

If mounted triggers timer-based work, combine fake timers with promise flushing.

typescript
1vi.useFakeTimers()
2
3const wrapper = mount(UserPanel)
4await flushPromises()
5vi.runAllTimers()
6await nextTick()
7
8expect(wrapper.find('[data-test="name"]').exists()).toBe(true)

Do not mix real and fake timers in one test unless you explicitly switch modes, because that creates non-deterministic behavior.

Test the Error Path, Not Only Success

Mounted hooks also fail in real systems, so tests should validate rejected requests and fallback rendering. This confirms user feedback remains useful when APIs are unavailable.

typescript
1import { fetchUser } from './api'
2
3vi.mocked(fetchUser).mockRejectedValueOnce(new Error('network down'))
4
5const wrapper = mount(UserPanel)
6await flushPromises()
7await nextTick()
8
9expect(wrapper.text()).toContain('Could not load user')
10expect(wrapper.find('[data-test="loading"]').exists()).toBe(false)

By testing both success and failure states, you avoid brittle UI logic that only works on happy paths and breaks silently in production.

If your component emits events after loading, assert those emissions after the same synchronization steps so parent-child integration remains trustworthy.

This also reduces false positives where stale DOM fragments satisfy broad text assertions.

Common Pitfalls

One pitfall is asserting immediately after mount for async components. Tests may pass locally and fail in CI due to timing variation.

Another issue is mocking APIs with unresolved promises. If the mock never resolves, your test hangs or times out.

Some developers call setTimeout delays in tests to “wait long enough.” This makes tests slow and flaky. Prefer deterministic primitives like flushPromises, nextTick, and fake timers.

Finally, verify your assertion targets the right DOM state. A text search can pass unexpectedly if stale content still exists somewhere else in the wrapper.

Summary

  • Async work in mounted usually needs both promise flushing and Vue tick waiting.
  • Use flushPromises then nextTick for stable assertions.
  • Mock async dependencies with explicit resolved or rejected values.
  • Use fake timers for timer-based lifecycle logic.
  • Avoid arbitrary sleeps and rely on deterministic test synchronization.

Course illustration
Course illustration

All Rights Reserved.