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>
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
---
|
||||
title: Test Complex Composables with Host Component Wrapper
|
||||
impact: MEDIUM
|
||||
impactDescription: Composables using lifecycle hooks or provide/inject fail when tested directly without a component context
|
||||
type: capability
|
||||
tags: [vue3, testing, composables, vitest, lifecycle-hooks, provide-inject]
|
||||
---
|
||||
|
||||
# Test Complex Composables with Host Component Wrapper
|
||||
|
||||
**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.
|
||||
|
||||
Simple composables using only reactivity APIs can be tested directly. Complex composables need a helper function that creates a host component context.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Identify if composable uses lifecycle hooks or inject
|
||||
- [ ] For simple composables (refs, computed only): test directly
|
||||
- [ ] For complex composables: use `withSetup` helper pattern
|
||||
- [ ] Clean up by unmounting the test app after each test
|
||||
- [ ] Use `app.provide()` to mock injected dependencies
|
||||
|
||||
**Simple Composable - Test Directly:**
|
||||
```javascript
|
||||
// composables/useCounter.js
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export function useCounter(initialValue = 0) {
|
||||
const count = ref(initialValue)
|
||||
const doubled = computed(() => count.value * 2)
|
||||
const increment = () => count.value++
|
||||
|
||||
return { count, doubled, increment }
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// useCounter.test.js
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { useCounter } from './useCounter'
|
||||
|
||||
// CORRECT: Simple composable can be tested directly
|
||||
describe('useCounter', () => {
|
||||
it('initializes with default value', () => {
|
||||
const { count } = useCounter()
|
||||
expect(count.value).toBe(0)
|
||||
})
|
||||
|
||||
it('increments count', () => {
|
||||
const { count, increment } = useCounter()
|
||||
increment()
|
||||
expect(count.value).toBe(1)
|
||||
})
|
||||
|
||||
it('computes doubled value', () => {
|
||||
const { count, doubled, increment } = useCounter(5)
|
||||
expect(doubled.value).toBe(10)
|
||||
increment()
|
||||
expect(doubled.value).toBe(12)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Complex Composable - Use Host Wrapper:**
|
||||
```javascript
|
||||
// composables/useFetch.js
|
||||
import { ref, onMounted, onUnmounted, inject } from 'vue'
|
||||
|
||||
export function useFetch(url) {
|
||||
const data = ref(null)
|
||||
const error = ref(null)
|
||||
const loading = ref(true)
|
||||
let controller = null
|
||||
|
||||
// Uses inject - needs component context
|
||||
const apiClient = inject('apiClient')
|
||||
|
||||
// Uses lifecycle hooks - needs component context
|
||||
onMounted(async () => {
|
||||
controller = new AbortController()
|
||||
try {
|
||||
const response = await apiClient.get(url, { signal: controller.signal })
|
||||
data.value = response.data
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') error.value = e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
controller?.abort()
|
||||
})
|
||||
|
||||
return { data, error, loading }
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// test-utils.js
|
||||
import { createApp } from 'vue'
|
||||
|
||||
/**
|
||||
* Helper to test composables that need component context
|
||||
*/
|
||||
export function withSetup(composable) {
|
||||
let result
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
result = composable()
|
||||
// Return a render function to suppress warnings
|
||||
return () => {}
|
||||
}
|
||||
})
|
||||
|
||||
app.mount(document.createElement('div'))
|
||||
|
||||
return [result, app]
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// useFetch.test.js
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { withSetup } from './test-utils'
|
||||
import { useFetch } from './useFetch'
|
||||
|
||||
describe('useFetch', () => {
|
||||
let app
|
||||
const mockApiClient = {
|
||||
get: vi.fn()
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
// IMPORTANT: Clean up to trigger onUnmounted
|
||||
app?.unmount()
|
||||
})
|
||||
|
||||
it('fetches data on mount', async () => {
|
||||
mockApiClient.get.mockResolvedValue({ data: { id: 1, name: 'Test' } })
|
||||
|
||||
const [result, testApp] = withSetup(() => useFetch('/api/test'))
|
||||
app = testApp
|
||||
|
||||
// Provide mocked dependency
|
||||
app.provide('apiClient', mockApiClient)
|
||||
|
||||
// Wait for async operations
|
||||
await flushPromises()
|
||||
|
||||
expect(result.data.value).toEqual({ id: 1, name: 'Test' })
|
||||
expect(result.loading.value).toBe(false)
|
||||
expect(result.error.value).toBeNull()
|
||||
})
|
||||
|
||||
it('handles errors', async () => {
|
||||
const testError = new Error('Network error')
|
||||
mockApiClient.get.mockRejectedValue(testError)
|
||||
|
||||
const [result, testApp] = withSetup(() => useFetch('/api/test'))
|
||||
app = testApp
|
||||
app.provide('apiClient', mockApiClient)
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(result.error.value).toBe(testError)
|
||||
expect(result.data.value).toBeNull()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Enhanced withSetup Helper with Provide Support
|
||||
```javascript
|
||||
// test-utils.js
|
||||
export function withSetup(composable, options = {}) {
|
||||
let result
|
||||
|
||||
const app = createApp({
|
||||
setup() {
|
||||
result = composable()
|
||||
return () => {}
|
||||
}
|
||||
})
|
||||
|
||||
// Apply global provides before mounting
|
||||
if (options.provide) {
|
||||
Object.entries(options.provide).forEach(([key, value]) => {
|
||||
app.provide(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
app.mount(document.createElement('div'))
|
||||
|
||||
return [result, app]
|
||||
}
|
||||
|
||||
// Usage
|
||||
const [result, app] = withSetup(() => useMyComposable(), {
|
||||
provide: {
|
||||
apiClient: mockApiClient,
|
||||
currentUser: { id: 1, name: 'Test User' }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Testing with @vue/test-utils mount
|
||||
```javascript
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { defineComponent } from 'vue'
|
||||
import { useFetch } from './useFetch'
|
||||
|
||||
test('useFetch in component context', async () => {
|
||||
const TestComponent = defineComponent({
|
||||
setup() {
|
||||
const { data, loading } = useFetch('/api/users')
|
||||
return { data, loading }
|
||||
},
|
||||
template: '<div>{{ loading ? "Loading..." : data }}</div>'
|
||||
})
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
global: {
|
||||
provide: {
|
||||
apiClient: mockApiClient
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('Test data')
|
||||
})
|
||||
```
|
||||
|
||||
## Reference
|
||||
- [Vue.js Testing Guide - Testing Composables](https://vuejs.org/guide/scaling-up/testing#testing-composables)
|
||||
- [Vue Test Utils - Mounting Components](https://test-utils.vuejs.org/guide/)
|
||||
Reference in New Issue
Block a user