# CLAUDE.md — test/

## Skill precedence

When writing or modifying tests, load the **nuxt-testing** skill first:
```
/nuxt-testing
```
It takes precedence over other testing skills (vue-testing-best-practices, vitest, etc.) because it covers the exact `@nuxt/test-utils` setup used here.

## Test structure

| Directory | Project | Environment | Purpose |
|-----------|---------|-------------|---------|
| `test/unit/` | `unit` | node | Pure logic, Zod schemas, utilities |
| `test/nuxt/` | `nuxt` | nuxt (happy-dom) | Components and composables that need Nuxt runtime |
| `test/e2e/` | `e2e` | node | Browser tests against the running dev server |

Run a single project: `pnpm test --project unit` / `nuxt` / `e2e`

## Known gotchas

### Nuxt component tests — mock stores in `vi.hoisted`, not in `it()`

Global middleware (e.g. `00.fetchUser.global.ts`) runs during `mountSuspended` **before** the test body executes. If a mocked store returns `undefined` at that point, Nuxt throws and the test crashes.

**Wrong** — mock is set up too late:
```ts
const useUserMock = vi.fn()
mockNuxtImport('useUser', () => useUserMock)

it('...', async () => {
  useUserMock.mockReturnValue({ isAuthenticated: false, authRefresh: vi.fn() })
  // ❌ middleware already ran with undefined
})
```

**Correct** — default implementation lives in `vi.hoisted`:
```ts
const { useUserMock } = vi.hoisted(() => ({
  useUserMock: vi.fn(() => ({ isAuthenticated: false, user: null, authRefresh: vi.fn() }))
}))
mockNuxtImport('useUser', () => useUserMock)
```

### E2E tests — do not pass a path to `createPage()` on this SPA

`createPage('/some-path')` internally calls `waitForHydration`, which polls
`window.useNuxtApp?.().isHydrating === false`. Because `useNuxtApp` is not
exposed on `window` in SPA mode, this never resolves and the test times out.

**Wrong:**
```ts
const page = await createPage('/login') // ❌ hangs forever
```

**Correct** — call without a path, then navigate manually:
```ts
const page = await createPage()
await page.goto('http://localhost:3000/login', { waitUntil: 'networkidle' })
```

### E2E tests — use `isVisible()` + Vitest `expect`, not Playwright matchers

`@playwright/test` is not installed (only `playwright-core`). Playwright's
`expect(locator).toBeVisible()` is not available.

```ts
// ❌ crashes — @playwright/test not installed
import { expect } from '@playwright/test'
await expect(page.locator('input')).toBeVisible()

// ✅ use the async boolean method with Vitest's expect
import { expect } from 'vitest'
expect(await page.locator('input').isVisible()).toBe(true)
```
