Files
agent-skills/skills/vue-testing-best-practices/reference/testing-suspense-async-components.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

6.3 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Wrap Async Setup Components in Suspense for Testing HIGH Components with async setup() fail to render in tests without Suspense wrapper, causing cryptic errors gotcha
vue3
testing
suspense
async-setup
vue-test-utils
vitest

Wrap Async Setup Components in Suspense for Testing

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.

Create a test wrapper component with Suspense or use a mountSuspense helper function for testing async components.

Task Checklist

  • Identify components with async setup (uses await in <script setup> or async setup())
  • Create a wrapper component with <Suspense> for testing
  • Use flushPromises() after mounting to wait for async resolution
  • Access the actual component via findComponent() for assertions
  • Consider using @testing-library/vue with caution (has Suspense issues)

Incorrect:

import { mount } from '@vue/test-utils'
import AsyncUserProfile from './AsyncUserProfile.vue'

// BAD: Async component without Suspense wrapper
test('displays user data', async () => {
  // This won't render - Vue expects Suspense wrapper for async setup
  const wrapper = mount(AsyncUserProfile, {
    props: { userId: 1 }
  })

  await flushPromises()

  // This fails - component never rendered
  expect(wrapper.find('.username').text()).toBe('John')
})

Correct - Manual Wrapper Component:

import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, Suspense } from 'vue'
import AsyncUserProfile from './AsyncUserProfile.vue'

test('displays user data', async () => {
  // Create wrapper component with Suspense
  const TestWrapper = defineComponent({
    components: { AsyncUserProfile },
    template: `
      <Suspense>
        <AsyncUserProfile :user-id="1" />
        <template #fallback>Loading...</template>
      </Suspense>
    `
  })

  const wrapper = mount(TestWrapper)

  // Initially shows fallback
  expect(wrapper.text()).toContain('Loading...')

  // Wait for async setup to complete
  await flushPromises()

  // Find the actual component for detailed assertions
  const profile = wrapper.findComponent(AsyncUserProfile)
  expect(profile.find('.username').text()).toBe('John')
})

Correct - Reusable Helper Function:

// test-utils.js
import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, Suspense, h } from 'vue'

export async function mountSuspense(component, options = {}) {
  const { props, slots, ...mountOptions } = options

  const wrapper = mount(
    defineComponent({
      render() {
        return h(
          Suspense,
          null,
          {
            default: () => h(component, props, slots),
            fallback: () => h('div', 'Loading...')
          }
        )
      }
    }),
    mountOptions
  )

  // Wait for async component to resolve
  await flushPromises()

  return {
    wrapper,
    // Provide easy access to the actual component
    component: wrapper.findComponent(component)
  }
}
// AsyncUserProfile.test.js
import { mountSuspense } from './test-utils'
import AsyncUserProfile from './AsyncUserProfile.vue'

test('displays user data', async () => {
  const { component } = await mountSuspense(AsyncUserProfile, {
    props: { userId: 1 },
    global: {
      stubs: {
        // Stub any child components if needed
      }
    }
  })

  expect(component.find('.username').text()).toBe('John')
})

test('handles errors gracefully', async () => {
  const { component } = await mountSuspense(AsyncUserProfile, {
    props: { userId: 'invalid' }
  })

  expect(component.find('.error').exists()).toBe(true)
})

Testing with onErrorCaptured

import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, Suspense, h, ref, onErrorCaptured } from 'vue'
import AsyncComponent from './AsyncComponent.vue'

test('catches async errors', async () => {
  const capturedError = ref(null)

  const TestWrapper = defineComponent({
    setup() {
      onErrorCaptured((error) => {
        capturedError.value = error
        return true // Prevent error propagation
      })
      return { capturedError }
    },
    render() {
      return h(Suspense, null, {
        default: () => h(AsyncComponent, { shouldFail: true }),
        fallback: () => h('div', 'Loading...')
      })
    }
  })

  const wrapper = mount(TestWrapper)
  await flushPromises()

  expect(capturedError.value).toBeTruthy()
  expect(capturedError.value.message).toContain('Failed to load')
})

Using with Nuxt's mountSuspended

// If using Nuxt, use the built-in mountSuspended helper
import { mountSuspended } from '@nuxt/test-utils/runtime'
import AsyncPage from './AsyncPage.vue'

test('renders async page', async () => {
  const wrapper = await mountSuspended(AsyncPage, {
    props: { id: 1 }
  })

  expect(wrapper.find('h1').text()).toBe('Page Title')
})

Important Caveats

@testing-library/vue Limitation

// CAUTION: @testing-library/vue has issues with Suspense
// Use @vue/test-utils for async components instead

// If you must use Testing Library, create manual wrapper:
import { render, waitFor } from '@testing-library/vue'

test('async component with testing library', async () => {
  const TestWrapper = {
    template: `
      <Suspense>
        <AsyncComponent />
      </Suspense>
    `,
    components: { AsyncComponent }
  }

  const { getByText } = render(TestWrapper)

  await waitFor(() => {
    expect(getByText('Loaded content')).toBeInTheDocument()
  })
})

Accessing Component Instance

test('access vm on async component', async () => {
  const { wrapper, component } = await mountSuspense(AsyncComponent)

  // The wrapper.vm is the Suspense wrapper - not useful
  // Use component.vm for the actual async component
  expect(component.vm.someData).toBe('value')
})

Reference