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-suspense-async-components.md
1---2title: Wrap Async Setup Components in Suspense for Testing3impact: HIGH4impactDescription: Components with async setup() fail to render in tests without Suspense wrapper, causing cryptic errors5type: gotcha6tags: [vue3, testing, suspense, async-setup, vue-test-utils, vitest]7---89# Wrap Async Setup Components in Suspense for Testing1011**Impact: HIGH** - Components using `async setup()` require a `<Suspense>` wrapper to function correctly. Testing them without Suspense causes the component to never render, leading to test failures and confusing errors.1213Create a test wrapper component with Suspense or use a `mountSuspense` helper function for testing async components.1415## Task Checklist1617- [ ] Identify components with async setup (uses `await` in `<script setup>` or `async setup()`)18- [ ] Create a wrapper component with `<Suspense>` for testing19- [ ] Use `flushPromises()` after mounting to wait for async resolution20- [ ] Access the actual component via `findComponent()` for assertions21- [ ] Consider using `@testing-library/vue` with caution (has Suspense issues)2223**Incorrect:**24```javascript25import { mount } from '@vue/test-utils'26import AsyncUserProfile from './AsyncUserProfile.vue'2728// BAD: Async component without Suspense wrapper29test('displays user data', async () => {30// This won't render - Vue expects Suspense wrapper for async setup31const wrapper = mount(AsyncUserProfile, {32props: { userId: 1 }33})3435await flushPromises()3637// This fails - component never rendered38expect(wrapper.find('.username').text()).toBe('John')39})40```4142**Correct - Manual Wrapper Component:**43```javascript44import { mount, flushPromises } from '@vue/test-utils'45import { defineComponent, Suspense } from 'vue'46import AsyncUserProfile from './AsyncUserProfile.vue'4748test('displays user data', async () => {49// Create wrapper component with Suspense50const TestWrapper = defineComponent({51components: { AsyncUserProfile },52template: `53<Suspense>54<AsyncUserProfile :user-id="1" />55<template #fallback>Loading...</template>56</Suspense>57`58})5960const wrapper = mount(TestWrapper)6162// Initially shows fallback63expect(wrapper.text()).toContain('Loading...')6465// Wait for async setup to complete66await flushPromises()6768// Find the actual component for detailed assertions69const profile = wrapper.findComponent(AsyncUserProfile)70expect(profile.find('.username').text()).toBe('John')71})72```7374**Correct - Reusable Helper Function:**75```javascript76// test-utils.js77import { mount, flushPromises } from '@vue/test-utils'78import { defineComponent, Suspense, h } from 'vue'7980export async function mountSuspense(component, options = {}) {81const { props, slots, ...mountOptions } = options8283const wrapper = mount(84defineComponent({85render() {86return h(87Suspense,88null,89{90default: () => h(component, props, slots),91fallback: () => h('div', 'Loading...')92}93)94}95}),96mountOptions97)9899// Wait for async component to resolve100await flushPromises()101102return {103wrapper,104// Provide easy access to the actual component105component: wrapper.findComponent(component)106}107}108```109110```javascript111// AsyncUserProfile.test.js112import { mountSuspense } from './test-utils'113import AsyncUserProfile from './AsyncUserProfile.vue'114115test('displays user data', async () => {116const { component } = await mountSuspense(AsyncUserProfile, {117props: { userId: 1 },118global: {119stubs: {120// Stub any child components if needed121}122}123})124125expect(component.find('.username').text()).toBe('John')126})127128test('handles errors gracefully', async () => {129const { component } = await mountSuspense(AsyncUserProfile, {130props: { userId: 'invalid' }131})132133expect(component.find('.error').exists()).toBe(true)134})135```136137## Testing with onErrorCaptured138139```javascript140import { mount, flushPromises } from '@vue/test-utils'141import { defineComponent, Suspense, h, ref, onErrorCaptured } from 'vue'142import AsyncComponent from './AsyncComponent.vue'143144test('catches async errors', async () => {145const capturedError = ref(null)146147const TestWrapper = defineComponent({148setup() {149onErrorCaptured((error) => {150capturedError.value = error151return true // Prevent error propagation152})153return { capturedError }154},155render() {156return h(Suspense, null, {157default: () => h(AsyncComponent, { shouldFail: true }),158fallback: () => h('div', 'Loading...')159})160}161})162163const wrapper = mount(TestWrapper)164await flushPromises()165166expect(capturedError.value).toBeTruthy()167expect(capturedError.value.message).toContain('Failed to load')168})169```170171## Using with Nuxt's mountSuspended172173```javascript174// If using Nuxt, use the built-in mountSuspended helper175import { mountSuspended } from '@nuxt/test-utils/runtime'176import AsyncPage from './AsyncPage.vue'177178test('renders async page', async () => {179const wrapper = await mountSuspended(AsyncPage, {180props: { id: 1 }181})182183expect(wrapper.find('h1').text()).toBe('Page Title')184})185```186187## Important Caveats188189### @testing-library/vue Limitation190```javascript191// CAUTION: @testing-library/vue has issues with Suspense192// Use @vue/test-utils for async components instead193194// If you must use Testing Library, create manual wrapper:195import { render, waitFor } from '@testing-library/vue'196197test('async component with testing library', async () => {198const TestWrapper = {199template: `200<Suspense>201<AsyncComponent />202</Suspense>203`,204components: { AsyncComponent }205}206207const { getByText } = render(TestWrapper)208209await waitFor(() => {210expect(getByText('Loaded content')).toBeInTheDocument()211})212})213```214215### Accessing Component Instance216```javascript217test('access vm on async component', async () => {218const { wrapper, component } = await mountSuspense(AsyncComponent)219220// The wrapper.vm is the Suspense wrapper - not useful221// Use component.vm for the actual async component222expect(component.vm.someData).toBe('value')223})224```225226## Reference227- [Vue Test Utils - Async Suspense](https://test-utils.vuejs.org/guide/advanced/async-suspense)228- [Vue.js Suspense Documentation](https://vuejs.org/guide/built-ins/suspense.html)229- [Testing Library Vue Suspense Issue](https://github.com/testing-library/vue-testing-library/issues/230)230