' }
+ })
+
+ const wrapper = mount(AsyncWithError)
+
+ await flushPromises()
+
+ expect(wrapper.text()).toContain('Error loading component')
+})
+```
+
+## Utilities Reference
+
+| Utility | Purpose |
+|---------|---------|
+| `await flushPromises()` | Resolves all pending promises |
+| `await nextTick()` | Waits for Vue's next DOM update cycle |
+| `await wrapper.trigger('click')` | Triggers event and waits for update |
+
+## Dynamic Import Handling
+
+**Note:** Dynamic imports (`import('./File.vue')`) may require additional handling beyond `flushPromises()` in test environments. Test runners like Vitest handle module resolution differently than runtime bundlers, which can cause timing issues with dynamic imports. If `flushPromises()` alone doesn't resolve the component, consider:
+
+- Mocking the dynamic import to return the component synchronously
+- Using multiple `await flushPromises()` calls in sequence
+- Wrapping assertions in `waitFor()` or retry utilities
+- Configuring your test runner's module resolution settings
+
+```javascript
+// If flushPromises() isn't sufficient, mock the import
+vi.mock('./Widget.vue', () => ({
+ default: { template: '
Widget Content
' }
+}))
+
+// Or use multiple flush calls for nested async operations
+await flushPromises()
+await flushPromises()
+```
+
+## References
+
+- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense)
+- [Vue.js Async Components Documentation](https://vuejs.org/guide/components/async)
diff --git a/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md b/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md
new file mode 100644
index 000000000..887836fff
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md
@@ -0,0 +1,158 @@
+---
+title: Teleported Content Requires Special Testing Approach
+impact: MEDIUM
+impactDescription: Vue Test Utils cannot find teleported content using standard wrapper.find() methods
+type: gotcha
+tags: [vue3, teleport, testing, vue-test-utils]
+---
+
+# Teleported Content Requires Special Testing Approach
+
+**Impact: MEDIUM** - Vue Test Utils scopes queries to the mounted component. Teleported content renders outside the component's DOM tree, so `wrapper.find()` cannot locate it. This leads to failing tests and confusion.
+
+## Task Checklist
+
+- [ ] Stub Teleport in unit tests to keep content in component tree
+- [ ] Use `document.body` queries for integration tests with real Teleport
+- [ ] Consider using `getComponent()` instead of DOM queries for teleported components
+
+**Problem - Standard Testing Fails:**
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+```ts
+// Modal.spec.ts - BROKEN
+import { mount } from '@vue/test-utils'
+import Modal from './Modal.vue'
+
+test('modal input exists', async () => {
+ const wrapper = mount(Modal)
+ await wrapper.find('button').trigger('click')
+
+ // FAILS: Teleported content is not in wrapper's DOM tree
+ expect(wrapper.find('[data-testid="modal-input"]').exists()).toBe(true)
+})
+```
+
+**Solution 1 - Stub Teleport:**
+```ts
+import { mount } from '@vue/test-utils'
+import Modal from './Modal.vue'
+
+test('modal input exists', async () => {
+ const wrapper = mount(Modal, {
+ global: {
+ stubs: {
+ // Stub teleport to render content inline
+ Teleport: true
+ }
+ }
+ })
+
+ await wrapper.find('button').trigger('click')
+
+ // Works: Content renders inside wrapper
+ expect(wrapper.find('[data-testid="modal-input"]').exists()).toBe(true)
+})
+```
+
+**Solution 2 - Query Document Body:**
+```ts
+import { mount } from '@vue/test-utils'
+import Modal from './Modal.vue'
+
+test('modal renders to body', async () => {
+ const wrapper = mount(Modal, {
+ attachTo: document.body // Required for Teleport to work
+ })
+
+ await wrapper.find('button').trigger('click')
+
+ // Query the actual DOM
+ const modal = document.querySelector('[data-testid="modal"]')
+ expect(modal).toBeTruthy()
+
+ const input = document.querySelector('[data-testid="modal-input"]')
+ expect(input).toBeTruthy()
+
+ // Cleanup
+ wrapper.unmount()
+})
+```
+
+**Solution 3 - Custom Teleport Stub with Content Access:**
+```ts
+import { mount, config } from '@vue/test-utils'
+import { h, Teleport } from 'vue'
+import Modal from './Modal.vue'
+
+// Custom stub that renders content in a testable way
+const TeleportStub = {
+ setup(props, { slots }) {
+ return () => h('div', { class: 'teleport-stub' }, slots.default?.())
+ }
+}
+
+test('modal with custom stub', async () => {
+ const wrapper = mount(Modal, {
+ global: {
+ stubs: {
+ Teleport: TeleportStub
+ }
+ }
+ })
+
+ await wrapper.find('button').trigger('click')
+
+ // Content is inside .teleport-stub
+ expect(wrapper.find('.teleport-stub [data-testid="modal-input"]').exists()).toBe(true)
+})
+```
+
+## Testing Vue Final Modal and UI Libraries
+
+Libraries like Vue Final Modal use Teleport internally, causing test failures:
+
+```ts
+// Problem: Vue Final Modal teleports to body
+import { VueFinalModal } from 'vue-final-modal'
+
+test('modal content', async () => {
+ const wrapper = mount(MyComponent, {
+ global: {
+ stubs: {
+ // Stub the modal component to avoid teleport issues
+ VueFinalModal: true
+ }
+ }
+ })
+})
+```
+
+## E2E Testing (Cypress, Playwright)
+
+E2E tests query the real DOM, so Teleport works naturally:
+
+```ts
+// Cypress
+it('opens modal', () => {
+ cy.visit('/page-with-modal')
+ cy.get('button').click()
+
+ // Works: Cypress queries the real DOM
+ cy.get('[data-testid="modal"]').should('be.visible')
+})
+```
+
+## Reference
+- [Vue Test Utils - Teleport](https://test-utils.vuejs.org/guide/advanced/teleport)
+- [Vue Test Utils - Stubs](https://test-utils.vuejs.org/guide/advanced/stubs-shallow-mount)
diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md b/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md
new file mode 100644
index 000000000..597d9c836
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md
@@ -0,0 +1,175 @@
+---
+title: Properly Handle Async Updates with nextTick and flushPromises
+impact: HIGH
+impactDescription: Race conditions and flaky tests occur when async DOM updates or API calls complete after assertions run
+type: gotcha
+tags: [vue3, testing, async, flushPromises, nextTick, vitest, vue-test-utils, race-condition]
+---
+
+# Properly Handle Async Updates with nextTick and flushPromises
+
+**Impact: HIGH** - Vue updates the DOM asynchronously. Without properly awaiting these updates, tests may assert against stale DOM state, causing intermittent failures and false negatives.
+
+Use `await` with triggers and `setValue`, use `nextTick` for reactive updates, and use `flushPromises` for external async operations like API calls.
+
+## Task Checklist
+
+- [ ] Always await `trigger()` and `setValue()` calls
+- [ ] Use `await nextTick()` after programmatic reactive state changes
+- [ ] Use `await flushPromises()` for external async operations (API calls, timers)
+- [ ] Don't chain multiple `nextTick` calls - use `flushPromises` instead
+- [ ] Consider using `waitFor` from testing-library for polling assertions
+
+**Incorrect:**
+```javascript
+import { mount } from '@vue/test-utils'
+import SearchComponent from './SearchComponent.vue'
+
+// BAD: Not awaiting trigger - assertion runs before DOM updates
+test('search filters results', () => {
+ const wrapper = mount(SearchComponent)
+
+ wrapper.find('input').setValue('vue') // Missing await!
+ wrapper.find('button').trigger('click') // Missing await!
+
+ // This assertion likely fails - DOM hasn't updated yet
+ expect(wrapper.findAll('.result').length).toBe(3)
+})
+
+// BAD: Using nextTick for API calls
+test('loads data from API', async () => {
+ const wrapper = mount(DataLoader)
+
+ await nextTick() // This won't wait for the API call!
+
+ // Assertion runs before fetch completes
+ expect(wrapper.find('.data').text()).toBe('Loaded data')
+})
+```
+
+**Correct:**
+```javascript
+import { mount, flushPromises } from '@vue/test-utils'
+import { nextTick } from 'vue'
+import SearchComponent from './SearchComponent.vue'
+import DataLoader from './DataLoader.vue'
+
+// CORRECT: Await trigger and setValue
+test('search filters results', async () => {
+ const wrapper = mount(SearchComponent)
+
+ await wrapper.find('input').setValue('vue')
+ await wrapper.find('button').trigger('click')
+
+ expect(wrapper.findAll('.result').length).toBe(3)
+})
+
+// CORRECT: Use flushPromises for API calls
+test('loads data from API', async () => {
+ const wrapper = mount(DataLoader)
+
+ // Wait for all pending promises to resolve
+ await flushPromises()
+
+ expect(wrapper.find('.data').text()).toBe('Loaded data')
+})
+```
+
+## When to Use Each Method
+
+### `await trigger()` / `await setValue()` - User Interactions
+```javascript
+// These methods return nextTick internally
+await wrapper.find('button').trigger('click')
+await wrapper.find('input').setValue('new value')
+await wrapper.find('form').trigger('submit')
+```
+
+### `await nextTick()` - Programmatic Reactive Updates
+```javascript
+import { nextTick } from 'vue'
+
+test('reflects programmatic state changes', async () => {
+ const wrapper = mount(Counter)
+
+ // Direct state modification (when testing with exposed internals)
+ wrapper.vm.count = 5
+
+ await nextTick() // Wait for Vue to update DOM
+
+ expect(wrapper.find('.count').text()).toBe('5')
+})
+```
+
+### `await flushPromises()` - External Async Operations
+```javascript
+import { flushPromises } from '@vue/test-utils'
+
+test('displays fetched data', async () => {
+ const wrapper = mount(UserProfile, {
+ props: { userId: 1 }
+ })
+
+ // Wait for component's API call to complete
+ await flushPromises()
+
+ expect(wrapper.find('.username').text()).toBe('John')
+})
+
+// Sometimes you need multiple flushPromises for chained async operations
+test('processes data after fetch', async () => {
+ const wrapper = mount(DataProcessor)
+
+ await flushPromises() // Wait for fetch
+ await flushPromises() // Wait for processing triggered by fetch
+
+ expect(wrapper.find('.processed').exists()).toBe(true)
+})
+```
+
+## Common Pattern: Combining Methods
+```javascript
+test('submits form and shows success', async () => {
+ const wrapper = mount(ContactForm)
+
+ // Fill form (awaiting each interaction)
+ await wrapper.find('#name').setValue('John')
+ await wrapper.find('#email').setValue('john@example.com')
+
+ // Submit form
+ await wrapper.find('form').trigger('submit')
+
+ // Wait for API submission to complete
+ await flushPromises()
+
+ // Assert success state
+ expect(wrapper.find('.success-message').exists()).toBe(true)
+})
+```
+
+## Testing with MSW or Mock APIs
+```javascript
+import { flushPromises } from '@vue/test-utils'
+import { rest } from 'msw'
+import { setupServer } from 'msw/node'
+
+const server = setupServer(
+ rest.get('/api/user', (req, res, ctx) => {
+ return res(ctx.json({ name: 'John' }))
+ })
+)
+
+test('displays user data', async () => {
+ const wrapper = mount(UserCard)
+
+ // MSW might require multiple flushPromises
+ await flushPromises()
+ await flushPromises()
+
+ expect(wrapper.find('.name').text()).toBe('John')
+})
+```
+
+## Reference
+- [Vue Test Utils - Asynchronous Behavior](https://test-utils.vuejs.org/guide/advanced/async-suspense)
+- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)
diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md b/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md
new file mode 100644
index 000000000..a112a420f
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md
@@ -0,0 +1,208 @@
+---
+title: Choose Browser-Based Runner for Style and DOM Event Testing
+impact: MEDIUM
+impactDescription: Node-based runners cannot test real CSS behavior, native DOM events, cookies, or computed styles
+type: capability
+tags: [vue3, testing, component-testing, vitest, browser, jsdom]
+---
+
+# Choose Browser-Based Runner for Style and DOM Event Testing
+
+**Impact: MEDIUM** - Node-based test runners (Vitest with jsdom/happy-dom) simulate the DOM but cannot test real CSS rendering, native browser events, cookies, computed styles, or cross-browser behavior. Use browser-based runners when these matter.
+
+Use Vitest for most component tests (fast), but use Vitest Browser Mode when testing visual/DOM-dependent features.
+
+## Task Checklist
+
+- [ ] Use Vitest (node) for logic-focused component tests
+- [ ] Use Vitest Browser Mode for style-dependent tests
+- [ ] Use Vitest Browser Mode for native events (focus, drag, resize)
+- [ ] Use Vitest Browser Mode for cookies and computed CSS styles
+- [ ] Accept slower speed tradeoff for browser accuracy
+
+## When to Use Each Approach
+
+### Node-Based Runner (Vitest + happy-dom/jsdom)
+Best for:
+- Pure logic testing
+- State management
+- Event emission
+- Props/slots behavior
+- Most component interactions
+- Fast CI/CD pipelines
+
+```javascript
+// vitest.config.js
+export default defineConfig({
+ test: {
+ environment: 'happy-dom', // or 'jsdom'
+ }
+})
+```
+
+```javascript
+// Fast but limited - fine for most tests
+test('button emits click event', async () => {
+ const wrapper = mount(Button)
+ await wrapper.trigger('click')
+ expect(wrapper.emitted('click')).toBeTruthy()
+})
+```
+
+### Vitest Browser Mode
+Required for:
+- CSS computed styles verification
+- CSS transitions/animations
+- Real focus/blur behavior
+- Drag and drop
+- Cookie operations
+- Viewport-dependent behavior
+- Cross-browser validation
+
+## Vitest Browser Mode Setup
+
+```bash
+npm install -D @vitest/browser playwright
+```
+
+```javascript
+// vitest.config.js
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ browser: {
+ enabled: true,
+ name: 'chromium',
+ provider: 'playwright',
+ },
+ },
+})
+```
+
+```javascript
+// Button.browser.test.js
+import { render } from 'vitest-browser-vue'
+import Button from './Button.vue'
+
+test('has correct hover styling', async () => {
+ const { getByRole } = render(Button, { props: { label: 'Click me' } })
+
+ const button = getByRole('button')
+
+ // Check initial style
+ await expect.element(button).toHaveStyle({
+ backgroundColor: 'rgb(59, 130, 246)' // blue
+ })
+})
+
+test('maintains focus after click', async () => {
+ const { getByRole } = render(Button)
+
+ const button = getByRole('button')
+ await button.click()
+
+ await expect.element(button).toHaveFocus()
+})
+```
+
+## Examples: What Each Runner Can/Cannot Test
+
+### Styles - Browser Required
+```javascript
+// Node runner: CANNOT verify actual CSS
+test('danger button has red background', () => {
+ const wrapper = mount(Button, { props: { variant: 'danger' } })
+ // This only checks class exists, not actual color
+ expect(wrapper.classes()).toContain('bg-red-500')
+})
+
+// Vitest Browser Mode: CAN verify computed styles
+test('danger button renders red', async () => {
+ const { getByRole } = render(Button, { props: { variant: 'danger' } })
+ await expect.element(getByRole('button')).toHaveStyle({
+ backgroundColor: 'rgb(239, 68, 68)'
+ })
+})
+```
+
+### Computed CSS Styles - Browser Required
+```javascript
+// Node runner: CANNOT get real computed styles
+test('button has correct padding', () => {
+ const wrapper = mount(Button)
+ // getComputedStyle returns empty/default values in jsdom
+ const style = window.getComputedStyle(wrapper.element)
+ // style.padding will be empty string, not actual computed value
+})
+
+// Vitest Browser Mode: Real computed styles
+test('button has correct padding', async () => {
+ const { getByRole } = render(Button)
+ const button = getByRole('button')
+
+ await expect.element(button).toHaveStyle({
+ padding: '12px 24px'
+ })
+})
+```
+
+### Native Events - Browser Required
+```javascript
+// Node runner: Synthetic events only
+test('handles drag and drop', async () => {
+ const wrapper = mount(DraggableList)
+ // trigger('dragstart') is synthetic - may not work as expected
+ await wrapper.find('.item').trigger('dragstart')
+})
+
+// Vitest Browser Mode: Real native events via userEvent
+import { userEvent } from '@vitest/browser/context'
+
+test('reorders items on drag', async () => {
+ const { getByTestId } = render(DraggableList)
+
+ const item = getByTestId('item-1')
+ const target = getByTestId('item-3')
+
+ await userEvent.dragAndDrop(item, target)
+
+ // Assert reordering
+})
+```
+
+## Recommended Testing Strategy
+
+```javascript
+// vitest.config.js - Separate test configurations
+
+export default defineConfig({
+ test: {
+ // Default: Node environment for speed
+ environment: 'happy-dom',
+
+ // Browser tests in separate directory
+ include: ['src/**/*.test.{js,ts}'],
+ },
+})
+
+// Run browser tests separately
+// npx vitest --browser.enabled
+```
+
+### Directory Structure
+```
+tests/
+├── unit/ # Fast node-based tests
+│ ├── Button.test.js
+│ └── useCounter.test.js
+├── component/ # Slower browser-based tests
+│ ├── Button.browser.test.js
+│ └── DragDrop.browser.test.js
+└── e2e/ # Full E2E tests (Playwright)
+ └── user-flow.spec.ts
+```
+
+## Reference
+- [Vue.js Testing - Component Testing](https://vuejs.org/guide/scaling-up/testing#component-testing)
+- [Vitest Browser Mode](https://vitest.dev/guide/browser.html)
diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md b/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md
new file mode 100644
index 000000000..e3a869c6b
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md
@@ -0,0 +1,144 @@
+---
+title: Test Components Using Blackbox Approach - Focus on Behavior Not Implementation
+impact: HIGH
+impactDescription: Implementation-aware tests become brittle and break during refactoring, leading to high maintenance burden
+type: best-practice
+tags: [vue3, testing, component-testing, vitest, vue-test-utils, blackbox]
+---
+
+# Test Components Using Blackbox Approach - Focus on Behavior Not Implementation
+
+**Impact: HIGH** - Tests that rely on implementation details (internal state, private methods, component structure) break during refactoring even when functionality remains correct. This leads to false negatives and high test maintenance burden.
+
+Follow Kent C. Dodds' testing philosophy: "The more your tests resemble how your software is used, the more confidence they can give you."
+
+## Task Checklist
+
+- [ ] Test what the component does, not how it does it
+- [ ] Query elements by user-visible attributes (text, role, testid)
+- [ ] Simulate user interactions (click, type) rather than calling methods directly
+- [ ] Assert on rendered output, emitted events, and visible state changes
+- [ ] Avoid accessing component internal state or private methods
+- [ ] Use data-testid attributes for elements without semantic meaning
+
+**Incorrect:**
+```javascript
+import { mount } from '@vue/test-utils'
+import Counter from './Counter.vue'
+
+// BAD: Testing implementation details
+test('counter increments', async () => {
+ const wrapper = mount(Counter)
+
+ // Accessing internal state directly
+ expect(wrapper.vm.count).toBe(0)
+
+ // Calling internal method instead of simulating user action
+ wrapper.vm.increment()
+
+ // Checking internal state instead of visible output
+ expect(wrapper.vm.count).toBe(1)
+})
+
+// BAD: Testing component structure
+test('has increment button', () => {
+ const wrapper = mount(Counter)
+
+ // Testing implementation detail - what if button becomes an anchor?
+ expect(wrapper.find('button').exists()).toBe(true)
+})
+```
+
+**Correct:**
+```javascript
+import { mount } from '@vue/test-utils'
+import Counter from './Counter.vue'
+
+// CORRECT: Testing behavior like a user would
+test('counter displays updated value after clicking increment', async () => {
+ const wrapper = mount(Counter, {
+ props: { max: 10 }
+ })
+
+ // Assert initial visible state
+ expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('0')
+
+ // Simulate user action
+ await wrapper.find('[data-testid="increment-button"]').trigger('click')
+
+ // Assert visible result
+ expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('1')
+})
+
+// CORRECT: Testing emitted events (public API)
+test('emits change event with new value when incremented', async () => {
+ const wrapper = mount(Counter)
+
+ await wrapper.find('[data-testid="increment-button"]').trigger('click')
+
+ expect(wrapper.emitted('change')).toHaveLength(1)
+ expect(wrapper.emitted('change')[0]).toEqual([1])
+})
+```
+
+## Using @testing-library/vue for Better Blackbox Tests
+
+```javascript
+import { render, screen, fireEvent } from '@testing-library/vue'
+import Counter from './Counter.vue'
+
+// Testing Library encourages accessible, user-centric queries
+test('increments counter on button click', async () => {
+ render(Counter)
+
+ // Query by role - how screen readers see it
+ const button = screen.getByRole('button', { name: /increment/i })
+ const display = screen.getByText('0')
+
+ await fireEvent.click(button)
+
+ expect(screen.getByText('1')).toBeInTheDocument()
+})
+```
+
+## What to Test vs What Not to Test
+
+### DO Test (Public Interface)
+```javascript
+// Props affect rendered output
+test('shows title from props', () => {
+ const wrapper = mount(Card, {
+ props: { title: 'Hello World' }
+ })
+ expect(wrapper.text()).toContain('Hello World')
+})
+
+// Slots render correctly
+test('renders slot content', () => {
+ const wrapper = mount(Card, {
+ slots: { default: '
Slot content
' }
+ })
+ expect(wrapper.text()).toContain('Slot content')
+})
+
+// Emitted events
+test('emits close event when X clicked', async () => {
+ const wrapper = mount(Modal)
+ await wrapper.find('[data-testid="close-button"]').trigger('click')
+ expect(wrapper.emitted('close')).toBeTruthy()
+})
+```
+
+### DON'T Test (Implementation Details)
+```javascript
+// Don't test internal computed properties
+// Don't test internal methods
+// Don't test component options/setup internals
+// Don't test that specific child components are rendered (unless critical)
+// Don't rely exclusively on snapshot tests for correctness
+```
+
+## Reference
+- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)
+- [Vue Test Utils - Testing Philosophy](https://test-utils.vuejs.org/guide/)
+- [Testing Library Guiding Principles](https://testing-library.com/docs/guiding-principles)
diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md b/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md
new file mode 100644
index 000000000..f6e4f0798
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md
@@ -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: '
{{ loading ? "Loading..." : data }}
'
+ })
+
+ 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/)
diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md b/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md
new file mode 100644
index 000000000..3df2ff0f8
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md
@@ -0,0 +1,242 @@
+---
+title: Use Playwright for E2E Testing - Cross-Browser Support and Better DX
+impact: MEDIUM
+impactDescription: Cypress has browser limitations and some features require paid subscriptions
+type: best-practice
+tags: [vue3, testing, e2e, playwright, cypress, end-to-end]
+---
+
+# Use Playwright for E2E Testing - Cross-Browser Support and Better DX
+
+**Impact: MEDIUM** - Playwright offers superior cross-browser testing (Chromium, WebKit, Firefox), excellent debugging tools, and is fully open source. Cypress has limitations with WebKit support and requires paid subscriptions for some features.
+
+Use Playwright for new E2E testing setups. Consider Cypress if team already has expertise or for its visual debugging UI.
+
+## Task Checklist
+
+- [ ] Install Playwright with browsers for your target platforms
+- [ ] Configure for Vue dev server integration
+- [ ] Set up projects for different browsers
+- [ ] Use locator strategies that match component test patterns
+- [ ] Configure CI for parallel test execution
+- [ ] Use trace and screenshot features for debugging
+
+## Quick Setup
+
+```bash
+# Install Playwright
+npm init playwright@latest
+
+# This will create:
+# - playwright.config.ts
+# - tests/ directory
+# - tests-examples/ directory
+```
+
+**playwright.config.ts:**
+```typescript
+import { defineConfig, devices } from '@playwright/test'
+
+export default defineConfig({
+ testDir: './e2e',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'html',
+
+ use: {
+ // Base URL for navigation
+ baseURL: 'http://localhost:5173',
+ // Capture trace on first retry
+ trace: 'on-first-retry',
+ // Screenshot on failure
+ screenshot: 'only-on-failure',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+ // Mobile viewports
+ {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ ],
+
+ // Run local dev server before tests
+ webServer: {
+ command: 'npm run dev',
+ url: 'http://localhost:5173',
+ reuseExistingServer: !process.env.CI,
+ },
+})
+```
+
+## E2E Test Example
+
+```typescript
+// e2e/user-flow.spec.ts
+import { test, expect } from '@playwright/test'
+
+test.describe('User Authentication', () => {
+ test('user can log in and see dashboard', async ({ page }) => {
+ // Navigate to login
+ await page.goto('/login')
+
+ // Fill login form
+ await page.getByLabel('Email').fill('user@example.com')
+ await page.getByLabel('Password').fill('password123')
+ await page.getByRole('button', { name: 'Sign In' }).click()
+
+ // Verify redirect to dashboard
+ await expect(page).toHaveURL('/dashboard')
+ await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible()
+ })
+
+ test('shows error for invalid credentials', async ({ page }) => {
+ await page.goto('/login')
+
+ await page.getByLabel('Email').fill('wrong@example.com')
+ await page.getByLabel('Password').fill('wrongpassword')
+ await page.getByRole('button', { name: 'Sign In' }).click()
+
+ await expect(page.getByRole('alert')).toContainText('Invalid credentials')
+ await expect(page).toHaveURL('/login')
+ })
+})
+```
+
+## Playwright vs Cypress Comparison
+
+| Feature | Playwright | Cypress |
+|---------|------------|---------|
+| Browsers | Chromium, Firefox, WebKit | Chromium, Firefox, Electron (WebKit experimental) |
+| Cross-browser | Full support | Limited |
+| Parallelization | Built-in | Requires Cypress Cloud |
+| Open source | Fully | Core only |
+| Mobile testing | Device emulation | Limited |
+| Debugging | Inspector, trace viewer | Time-travel UI |
+| API testing | Built-in | Plugin required |
+| Iframes | Full support | Limited |
+
+## Testing Vue Components with Data-Testid
+
+```typescript
+// e2e/product-list.spec.ts
+import { test, expect } from '@playwright/test'
+
+test('user can add product to cart', async ({ page }) => {
+ await page.goto('/products')
+
+ // Use data-testid for reliable selectors
+ await page.getByTestId('product-card').first().click()
+
+ // Verify product detail page
+ await expect(page.getByTestId('product-title')).toBeVisible()
+
+ // Add to cart
+ await page.getByTestId('add-to-cart-button').click()
+
+ // Verify cart updated
+ await expect(page.getByTestId('cart-count')).toHaveText('1')
+})
+```
+
+## Page Object Pattern for Vue Apps
+
+```typescript
+// e2e/pages/LoginPage.ts
+import { Page, Locator } from '@playwright/test'
+
+export class LoginPage {
+ readonly page: Page
+ readonly emailInput: Locator
+ readonly passwordInput: Locator
+ readonly submitButton: Locator
+ readonly errorMessage: Locator
+
+ constructor(page: Page) {
+ this.page = page
+ this.emailInput = page.getByLabel('Email')
+ this.passwordInput = page.getByLabel('Password')
+ this.submitButton = page.getByRole('button', { name: 'Sign In' })
+ this.errorMessage = page.getByRole('alert')
+ }
+
+ async goto() {
+ await this.page.goto('/login')
+ }
+
+ async login(email: string, password: string) {
+ await this.emailInput.fill(email)
+ await this.passwordInput.fill(password)
+ await this.submitButton.click()
+ }
+}
+```
+
+```typescript
+// e2e/auth.spec.ts
+import { test, expect } from '@playwright/test'
+import { LoginPage } from './pages/LoginPage'
+
+test('successful login', async ({ page }) => {
+ const loginPage = new LoginPage(page)
+ await loginPage.goto()
+ await loginPage.login('user@example.com', 'password123')
+
+ await expect(page).toHaveURL('/dashboard')
+})
+```
+
+## Visual Regression Testing
+
+```typescript
+test('homepage visual regression', async ({ page }) => {
+ await page.goto('/')
+
+ // Full page screenshot comparison
+ await expect(page).toHaveScreenshot('homepage.png')
+
+ // Element-specific screenshot
+ await expect(page.getByTestId('hero-section')).toHaveScreenshot('hero.png')
+})
+```
+
+## Running Tests
+
+```bash
+# Run all tests
+npx playwright test
+
+# Run in headed mode (see browser)
+npx playwright test --headed
+
+# Run specific file
+npx playwright test e2e/auth.spec.ts
+
+# Run in specific browser
+npx playwright test --project=chromium
+
+# Debug mode
+npx playwright test --debug
+
+# Generate test from actions
+npx playwright codegen localhost:5173
+```
+
+## Reference
+- [Playwright Documentation](https://playwright.dev/)
+- [Vue.js E2E Testing Recommendations](https://vuejs.org/guide/scaling-up/testing#e2e-testing)
+- [Playwright Best Practices](https://playwright.dev/docs/best-practices)
diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md b/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md
new file mode 100644
index 000000000..e44f43797
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md
@@ -0,0 +1,197 @@
+---
+title: Avoid Snapshot-Only Tests - They Don't Prove Correctness
+impact: MEDIUM
+impactDescription: Snapshot tests verify structure but not functionality, leading to false confidence and brittle tests
+type: best-practice
+tags: [vue3, testing, snapshot, vitest, vue-test-utils, anti-pattern]
+---
+
+# Avoid Snapshot-Only Tests - They Don't Prove Correctness
+
+**Impact: MEDIUM** - Snapshot tests only verify that HTML structure hasn't changed - they don't verify that the component works correctly. Relying exclusively on snapshots leads to false confidence and tests that break on any refactoring, even when functionality is preserved.
+
+Use snapshots sparingly for regression detection. Prefer behavioral assertions that test what the component does.
+
+## Task Checklist
+
+- [ ] Don't use snapshots as the only assertion for component behavior
+- [ ] Use snapshots for regression detection on stable UI components
+- [ ] Always pair snapshots with behavioral assertions
+- [ ] Keep snapshots small and focused (avoid full component snapshots)
+- [ ] Review snapshot diffs carefully - don't blindly update
+- [ ] Consider inline snapshots for small, critical structures
+
+**Incorrect:**
+```javascript
+import { mount } from '@vue/test-utils'
+import UserCard from './UserCard.vue'
+
+// BAD: Snapshot-only test proves nothing about functionality
+test('UserCard renders correctly', () => {
+ const wrapper = mount(UserCard, {
+ props: { user: { name: 'John', email: 'john@example.com' } }
+ })
+
+ expect(wrapper.html()).toMatchSnapshot()
+})
+
+// This test passes even if:
+// - The email isn't clickable
+// - The avatar doesn't load
+// - User actions are completely broken
+// - Accessibility is broken
+```
+
+**Correct:**
+```javascript
+import { mount } from '@vue/test-utils'
+import UserCard from './UserCard.vue'
+
+// CORRECT: Test actual behavior
+test('UserCard displays user information', () => {
+ const wrapper = mount(UserCard, {
+ props: { user: { name: 'John', email: 'john@example.com' } }
+ })
+
+ expect(wrapper.find('[data-testid="user-name"]').text()).toBe('John')
+ expect(wrapper.find('[data-testid="user-email"]').text()).toBe('john@example.com')
+})
+
+test('UserCard email link is clickable', async () => {
+ const wrapper = mount(UserCard, {
+ props: { user: { name: 'John', email: 'john@example.com' } }
+ })
+
+ const emailLink = wrapper.find('a[href^="mailto:"]')
+ expect(emailLink.exists()).toBe(true)
+ expect(emailLink.attributes('href')).toBe('mailto:john@example.com')
+})
+
+test('UserCard emits select event when clicked', async () => {
+ const wrapper = mount(UserCard, {
+ props: { user: { id: 1, name: 'John' } }
+ })
+
+ await wrapper.trigger('click')
+
+ expect(wrapper.emitted('select')).toBeTruthy()
+ expect(wrapper.emitted('select')[0]).toEqual([{ id: 1, name: 'John' }])
+})
+```
+
+## When Snapshots ARE Useful
+
+### Regression Detection for Stable Components
+```javascript
+// ACCEPTABLE: Snapshot as additional check, not the only check
+test('ErrorBoundary renders error message', () => {
+ const wrapper = mount(ErrorBoundary, {
+ props: { error: new Error('Something went wrong') }
+ })
+
+ // Primary assertions - verify behavior
+ expect(wrapper.find('.error-title').text()).toBe('Error')
+ expect(wrapper.find('.error-message').text()).toContain('Something went wrong')
+
+ // Secondary snapshot - catches unexpected structural changes
+ expect(wrapper.find('.error-container').html()).toMatchSnapshot()
+})
+```
+
+### Inline Snapshots for Small Structures
+```javascript
+// ACCEPTABLE: Inline snapshot for small, critical structure
+test('generates correct list markup', () => {
+ const wrapper = mount(ListItem, { props: { item: 'Test' } })
+
+ expect(wrapper.html()).toMatchInlineSnapshot(`
+ "
Test
"
+ `)
+})
+```
+
+### Complex SVG or Icon Output
+```javascript
+// ACCEPTABLE: Snapshot for complex generated content
+test('renders correct chart SVG', () => {
+ const wrapper = mount(PieChart, {
+ props: { data: [30, 40, 30] }
+ })
+
+ // Verify key behavior
+ expect(wrapper.findAll('path').length).toBe(3)
+
+ // Snapshot for full SVG structure
+ expect(wrapper.find('svg').html()).toMatchSnapshot()
+})
+```
+
+## Better Alternatives to Snapshots
+
+### Test Specific Elements
+```javascript
+// Instead of snapshotting entire component
+test('renders product with all required fields', () => {
+ const wrapper = mount(ProductCard, {
+ props: { product: { name: 'Widget', price: 9.99, inStock: true } }
+ })
+
+ expect(wrapper.find('.product-name').text()).toBe('Widget')
+ expect(wrapper.find('.product-price').text()).toContain('9.99')
+ expect(wrapper.find('.in-stock-badge').exists()).toBe(true)
+})
+```
+
+### Test CSS Classes for Styling
+```javascript
+test('applies danger styling for errors', () => {
+ const wrapper = mount(Alert, {
+ props: { type: 'error', message: 'Failed!' }
+ })
+
+ expect(wrapper.classes()).toContain('alert-danger')
+ expect(wrapper.find('.alert-icon').classes()).toContain('icon-error')
+})
+```
+
+### Use Testing Library Queries
+```javascript
+import { render, screen } from '@testing-library/vue'
+
+test('form has accessible labels', () => {
+ render(LoginForm)
+
+ // Testing Library queries verify accessibility
+ expect(screen.getByLabelText('Email')).toBeInTheDocument()
+ expect(screen.getByLabelText('Password')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument()
+})
+```
+
+## Snapshot Anti-Patterns
+
+```javascript
+// ANTI-PATTERN: Giant component snapshot
+test('page renders', () => {
+ const wrapper = mount(EntirePageComponent)
+ expect(wrapper.html()).toMatchSnapshot() // 500+ lines of HTML
+})
+
+// ANTI-PATTERN: Snapshot with dynamic content
+test('shows current date', () => {
+ const wrapper = mount(DateDisplay)
+ expect(wrapper.html()).toMatchSnapshot() // Fails every day!
+})
+
+// ANTI-PATTERN: Snapshot after every test
+test('button works', async () => {
+ const wrapper = mount(Counter)
+ await wrapper.find('button').trigger('click')
+ expect(wrapper.html()).toMatchSnapshot() // Redundant
+})
+```
+
+## Reference
+- [Vue.js Testing Guide - What Not to Test](https://vuejs.org/guide/scaling-up/testing)
+- [Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing)
+- [Vitest Snapshot Testing](https://vitest.dev/guide/snapshot.html)
diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md b/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md
new file mode 100644
index 000000000..3f8d4400d
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md
@@ -0,0 +1,228 @@
+---
+title: Configure Pinia Testing with createTestingPinia and setActivePinia
+impact: HIGH
+impactDescription: Missing Pinia configuration causes 'injection Symbol(pinia) not found' errors and failing tests
+type: gotcha
+tags: [vue3, testing, pinia, vitest, store, mocking, createTestingPinia]
+---
+
+# Configure Pinia Testing with createTestingPinia and setActivePinia
+
+**Impact: HIGH** - Testing components or composables that use Pinia stores without proper configuration results in "[Vue warn]: injection Symbol(pinia) not found" errors. Tests will fail or behave unexpectedly.
+
+Use `@pinia/testing` package with `createTestingPinia` for component tests and `setActivePinia(createPinia())` for unit testing stores directly.
+
+## Task Checklist
+
+- [ ] Install `@pinia/testing` as a dev dependency
+- [ ] Use `createTestingPinia` in component tests with `global.plugins`
+- [ ] Use `setActivePinia(createPinia())` in `beforeEach` for store unit tests
+- [ ] Configure `createSpy: vi.fn` when NOT using `globals: true` in Vitest
+- [ ] Initialize store inside each test to get fresh state
+- [ ] Use `stubActions: false` when you need real action execution
+
+**Incorrect:**
+```javascript
+import { mount } from '@vue/test-utils'
+import UserProfile from './UserProfile.vue'
+
+// BAD: Missing Pinia - causes injection error
+test('displays user name', () => {
+ const wrapper = mount(UserProfile) // ERROR: injection "Symbol(pinia)" not found
+ expect(wrapper.text()).toContain('John')
+})
+```
+
+```javascript
+import { useUserStore } from '@/stores/user'
+
+// BAD: No active Pinia instance
+test('user store actions', () => {
+ const store = useUserStore() // ERROR: no active Pinia
+ store.login('john', 'password')
+})
+```
+
+**Correct - Component Testing:**
+```javascript
+import { mount } from '@vue/test-utils'
+import { createTestingPinia } from '@pinia/testing'
+import { vi } from 'vitest'
+import UserProfile from './UserProfile.vue'
+import { useUserStore } from '@/stores/user'
+
+// CORRECT: Provide testing pinia with stubbed actions
+test('displays user name', () => {
+ const wrapper = mount(UserProfile, {
+ global: {
+ plugins: [
+ createTestingPinia({
+ createSpy: vi.fn, // Required if not using globals: true
+ initialState: {
+ user: { name: 'John', email: 'john@example.com' }
+ }
+ })
+ ]
+ }
+ })
+
+ expect(wrapper.text()).toContain('John')
+})
+
+// CORRECT: Test with stubbed actions (default behavior)
+test('calls logout action', async () => {
+ const wrapper = mount(UserProfile, {
+ global: {
+ plugins: [createTestingPinia({ createSpy: vi.fn })]
+ }
+ })
+
+ // Get store AFTER mounting with createTestingPinia
+ const store = useUserStore()
+
+ await wrapper.find('[data-testid="logout"]').trigger('click')
+
+ // Actions are stubbed and wrapped in spies
+ expect(store.logout).toHaveBeenCalled()
+})
+```
+
+**Correct - Store Unit Testing:**
+```javascript
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { setActivePinia, createPinia } from 'pinia'
+import { useUserStore } from '@/stores/user'
+
+describe('User Store', () => {
+ beforeEach(() => {
+ // Create fresh Pinia instance for each test
+ setActivePinia(createPinia())
+ })
+
+ it('initializes with empty user', () => {
+ const store = useUserStore()
+ expect(store.user).toBeNull()
+ expect(store.isLoggedIn).toBe(false)
+ })
+
+ it('updates user on login', async () => {
+ const store = useUserStore()
+
+ // Real action executes - not stubbed
+ await store.login('john', 'password')
+
+ expect(store.user).toEqual({ name: 'John' })
+ expect(store.isLoggedIn).toBe(true)
+ })
+
+ it('clears user on logout', () => {
+ const store = useUserStore()
+ store.user = { name: 'John' } // Set initial state
+
+ store.logout()
+
+ expect(store.user).toBeNull()
+ })
+})
+```
+
+## Testing with Real Actions vs Stubbed Actions
+
+```javascript
+import { createTestingPinia } from '@pinia/testing'
+
+// Stubbed actions (default) - for isolation
+const wrapper = mount(Component, {
+ global: {
+ plugins: [
+ createTestingPinia({
+ createSpy: vi.fn,
+ // stubActions: true (default) - actions are mocked
+ })
+ ]
+ }
+})
+
+// Real actions - for integration testing
+const wrapper = mount(Component, {
+ global: {
+ plugins: [
+ createTestingPinia({
+ createSpy: vi.fn,
+ stubActions: false // Actions execute normally
+ })
+ ]
+ }
+})
+```
+
+## Mocking Specific Action Implementations
+
+```javascript
+import { mount } from '@vue/test-utils'
+import { createTestingPinia } from '@pinia/testing'
+import { vi } from 'vitest'
+import { useCartStore } from '@/stores/cart'
+
+test('handles checkout failure', async () => {
+ const wrapper = mount(Checkout, {
+ global: {
+ plugins: [createTestingPinia({ createSpy: vi.fn })]
+ }
+ })
+
+ const cartStore = useCartStore()
+
+ // Mock specific action behavior
+ cartStore.checkout.mockRejectedValue(new Error('Payment failed'))
+
+ await wrapper.find('[data-testid="checkout"]').trigger('click')
+ await flushPromises()
+
+ expect(wrapper.find('.error').text()).toContain('Payment failed')
+})
+```
+
+## Spying on Actions with vi.spyOn
+
+```javascript
+import { setActivePinia, createPinia } from 'pinia'
+import { vi } from 'vitest'
+import { useUserStore } from '@/stores/user'
+
+test('tracks action calls', async () => {
+ setActivePinia(createPinia())
+ const store = useUserStore()
+
+ const loginSpy = vi.spyOn(store, 'login')
+ loginSpy.mockResolvedValue({ success: true })
+
+ await store.login('john', 'password')
+
+ expect(loginSpy).toHaveBeenCalledWith('john', 'password')
+})
+```
+
+## Testing Store $subscribe
+
+```javascript
+import { setActivePinia, createPinia } from 'pinia'
+import { useUserStore } from '@/stores/user'
+
+test('subscription triggers on state change', () => {
+ setActivePinia(createPinia())
+ const store = useUserStore()
+
+ const callback = vi.fn()
+ store.$subscribe(callback)
+
+ store.user = { name: 'John' }
+
+ expect(callback).toHaveBeenCalled()
+})
+```
+
+## Reference
+- [Pinia Testing Guide](https://pinia.vuejs.org/cookbook/testing.html)
+- [@pinia/testing Package](https://www.npmjs.com/package/@pinia/testing)
+- [Vue Test Utils - Plugins](https://test-utils.vuejs.org/guide/advanced/plugins.html)
diff --git a/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md b/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md
new file mode 100644
index 000000000..f935533c0
--- /dev/null
+++ b/.agents/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md
@@ -0,0 +1,229 @@
+---
+title: Wrap Async Setup Components in Suspense for Testing
+impact: HIGH
+impactDescription: Components with async setup() fail to render in tests without Suspense wrapper, causing cryptic errors
+type: gotcha
+tags: [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 `` 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 `