All skills
hyf0 avatar

/vue-testing-best-practices

@bc922e4 official
by hyf0hyf0/vue-skills2.9k stars
167

Use for Vue.js testing. Covers Vitest, Vue Test Utils, component testing, mocking, testing patterns, and Playwright for E2E testing.

Use this Skill: https://skilld.dev/gh/hyf0/vue-skills/vue-testing-best-practices

This session only. Nothing lands on disk.

referencetesting-suspense-async-components.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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

Source: SKILL.md on GitHub

No alerts17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides standard documentation, configuration guidelines, and best practices for testing Vue 3 applications with Vitest, Vue Test Utils, and Playwright. No security vulnerabilities or malicious patterns were detected.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    1/12 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at bc922e4. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 8 months ago
version
1.0.0
author
github.com/vuejs-ai
  • Vue
  • Testing
  • vitest
  • vue-test-utils
  • playwright
  • e2e-testing
  • composables
  • pinia
  • component-testing
  • mocking

README badge

README badge for hyf0/vue-skills/vue-testing-best-practices

Covers Vue 3 testing patterns and infrastructure with Vitest, Vue Test Utils, and Playwright, including component testing, composables, Pinia integration, and E2E testing. Addresses common gotchas like async race conditions, lifecycle hook testing, and Suspense components.

Generated from the current SKILL.md.

Does this skill cover unit testing, component testing, and E2E testing?
Yes. It covers unit testing with Vitest, component testing with Vue Test Utils, composables testing with helper wrappers, and E2E testing with Playwright.
What testing framework does this skill recommend for Vue 3?
Vitest is recommended for unit and component testing. Playwright is recommended for end-to-end testing.
Does this address Pinia store testing?
Yes. The skill includes guidance on setting up Pinia stores in tests and resolving injection Symbol errors.
Does this cover testing async components and Suspense?
Yes. It includes patterns for testing components with async setup, Suspense boundaries, and defineAsyncComponent.
Does this address flaky tests caused by race conditions?
Yes. The skill covers async/await patterns and flushing promises to handle race conditions and intermittent test failures.

Generated from the current SKILL.md. These answers refresh after source changes.