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-composables-helper-wrapper.md
1---2title: Test Complex Composables with Host Component Wrapper3impact: MEDIUM4impactDescription: Composables using lifecycle hooks or provide/inject fail when tested directly without a component context5type: capability6tags: [vue3, testing, composables, vitest, lifecycle-hooks, provide-inject]7---89# Test Complex Composables with Host Component Wrapper1011**Impact: MEDIUM** - Composables that use Vue lifecycle hooks (`onMounted`, `onUnmounted`) or dependency injection (`inject`) require a component context to function. Testing them directly will cause errors or incorrect behavior.1213Simple composables using only reactivity APIs can be tested directly. Complex composables need a helper function that creates a host component context.1415## Task Checklist1617- [ ] Identify if composable uses lifecycle hooks or inject18- [ ] For simple composables (refs, computed only): test directly19- [ ] For complex composables: use `withSetup` helper pattern20- [ ] Clean up by unmounting the test app after each test21- [ ] Use `app.provide()` to mock injected dependencies2223**Simple Composable - Test Directly:**24```javascript25// composables/useCounter.js26import { ref, computed } from 'vue'2728export function useCounter(initialValue = 0) {29const count = ref(initialValue)30const doubled = computed(() => count.value * 2)31const increment = () => count.value++3233return { count, doubled, increment }34}35```3637```javascript38// useCounter.test.js39import { describe, it, expect } from 'vitest'40import { useCounter } from './useCounter'4142// CORRECT: Simple composable can be tested directly43describe('useCounter', () => {44it('initializes with default value', () => {45const { count } = useCounter()46expect(count.value).toBe(0)47})4849it('increments count', () => {50const { count, increment } = useCounter()51increment()52expect(count.value).toBe(1)53})5455it('computes doubled value', () => {56const { count, doubled, increment } = useCounter(5)57expect(doubled.value).toBe(10)58increment()59expect(doubled.value).toBe(12)60})61})62```6364**Complex Composable - Use Host Wrapper:**65```javascript66// composables/useFetch.js67import { ref, onMounted, onUnmounted, inject } from 'vue'6869export function useFetch(url) {70const data = ref(null)71const error = ref(null)72const loading = ref(true)73let controller = null7475// Uses inject - needs component context76const apiClient = inject('apiClient')7778// Uses lifecycle hooks - needs component context79onMounted(async () => {80controller = new AbortController()81try {82const response = await apiClient.get(url, { signal: controller.signal })83data.value = response.data84} catch (e) {85if (e.name !== 'AbortError') error.value = e86} finally {87loading.value = false88}89})9091onUnmounted(() => {92controller?.abort()93})9495return { data, error, loading }96}97```9899```javascript100// test-utils.js101import { createApp } from 'vue'102103/**104* Helper to test composables that need component context105*/106export function withSetup(composable) {107let result108109const app = createApp({110setup() {111result = composable()112// Return a render function to suppress warnings113return () => {}114}115})116117app.mount(document.createElement('div'))118119return [result, app]120}121```122123```javascript124// useFetch.test.js125import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'126import { flushPromises } from '@vue/test-utils'127import { withSetup } from './test-utils'128import { useFetch } from './useFetch'129130describe('useFetch', () => {131let app132const mockApiClient = {133get: vi.fn()134}135136afterEach(() => {137// IMPORTANT: Clean up to trigger onUnmounted138app?.unmount()139})140141it('fetches data on mount', async () => {142mockApiClient.get.mockResolvedValue({ data: { id: 1, name: 'Test' } })143144const [result, testApp] = withSetup(() => useFetch('/api/test'))145app = testApp146147// Provide mocked dependency148app.provide('apiClient', mockApiClient)149150// Wait for async operations151await flushPromises()152153expect(result.data.value).toEqual({ id: 1, name: 'Test' })154expect(result.loading.value).toBe(false)155expect(result.error.value).toBeNull()156})157158it('handles errors', async () => {159const testError = new Error('Network error')160mockApiClient.get.mockRejectedValue(testError)161162const [result, testApp] = withSetup(() => useFetch('/api/test'))163app = testApp164app.provide('apiClient', mockApiClient)165166await flushPromises()167168expect(result.error.value).toBe(testError)169expect(result.data.value).toBeNull()170})171})172```173174## Enhanced withSetup Helper with Provide Support175```javascript176// test-utils.js177export function withSetup(composable, options = {}) {178let result179180const app = createApp({181setup() {182result = composable()183return () => {}184}185})186187// Apply global provides before mounting188if (options.provide) {189Object.entries(options.provide).forEach(([key, value]) => {190app.provide(key, value)191})192}193194app.mount(document.createElement('div'))195196return [result, app]197}198199// Usage200const [result, app] = withSetup(() => useMyComposable(), {201provide: {202apiClient: mockApiClient,203currentUser: { id: 1, name: 'Test User' }204}205})206```207208## Testing with @vue/test-utils mount209```javascript210import { mount } from '@vue/test-utils'211import { defineComponent } from 'vue'212import { useFetch } from './useFetch'213214test('useFetch in component context', async () => {215const TestComponent = defineComponent({216setup() {217const { data, loading } = useFetch('/api/users')218return { data, loading }219},220template: '<div>{{ loading ? "Loading..." : data }}</div>'221})222223const wrapper = mount(TestComponent, {224global: {225provide: {226apiClient: mockApiClient227}228}229})230231await flushPromises()232expect(wrapper.text()).toContain('Test data')233})234```235236## Reference237- [Vue.js Testing Guide - Testing Composables](https://vuejs.org/guide/scaling-up/testing#testing-composables)238- [Vue Test Utils - Mounting Components](https://test-utils.vuejs.org/guide/)239