<!-- generated-by: gsd-doc-writer -->

# Testing

## Test framework and setup

Both apps use **Vitest** (`^4.1.8`).

| App        | Environment | Setup file                   |
| ---------- | ----------- | ---------------------------- |
| `apps/api` | `node`      | `apps/api/test/setup.ts`     |
| `apps/pwa` | `jsdom`     | `apps/pwa/src/test-setup.ts` |

**apps/api setup** (`test/setup.ts`) registers a global `afterEach` that truncates `list_items`, `list_shares`, `push_subscriptions`, and `lists` in FK-safe order after every test. This keeps DB-backed integration tests isolated without requiring a full DB reset between runs. Parallel file execution is disabled (`fileParallelism: false`) to prevent FK violations when multiple test files share the same MariaDB.

**apps/pwa setup** (`src/test-setup.ts`) imports `@testing-library/jest-dom` for extended matchers and polyfills `window.matchMedia` for jsdom (required because Zustand's `calendarStore` calls `window.matchMedia` at module initialisation time). The timezone is pinned to `UTC` via `env: { TZ: 'UTC' }` so date-extraction assertions are deterministic across developer machines and CI.

No additional install step is needed beyond the normal `pnpm install` at the repo root.

## Running tests

### Unit and integration tests

**All API tests (from repo root):**

```bash
pnpm --filter @familysync/api test
```

This is also the command run by `pnpm test` at the root.

**All PWA unit tests:**

```bash
pnpm --filter @familysync/pwa test
```

**Watch mode (API):**

```bash
pnpm --filter @familysync/api test:watch
```

**Single test file:**

```bash
pnpm --filter @familysync/api exec vitest run tests/routes/lists.test.ts
```

### End-to-end tests (Playwright)

The PWA has a Playwright harness configured in `apps/pwa/playwright.config.ts` with two device profiles:

| Profile  | Viewport | Engine   | User-Agent                |
| -------- | -------- | -------- | ------------------------- |
| `iphone` | 390×844  | WebKit   | Mobile Safari (iPhone 14) |
| `pixel`  | 412×915  | Chromium | Chrome Android (Pixel 7)  |

Both profiles block the service worker (`serviceWorkers: 'block'`) so the Workbox SW does not intercept requests during tests. Auth is handled via `DEV_AUTH_BYPASS=true` on the API — never via stored browser state.

**Run all e2e tests (both profiles):**

```bash
pnpm test:e2e
# or
pnpm --filter @familysync/pwa test:e2e
```

**Run a single profile:**

```bash
pnpm --filter @familysync/pwa exec playwright test --project=pixel
pnpm --filter @familysync/pwa exec playwright test --project=iphone
```

**Interactive UI mode:**

```bash
pnpm --filter @familysync/pwa test:e2e:ui
```

The `baseURL` is driven by `PLAYWRIGHT_BASE_URL` (default: `http://localhost:5173`). In local mode the config reuses a running Vite dev server; in CI it starts Vite itself. The API, MariaDB, and Redis must already be running via Docker Compose before launching e2e tests locally — see `docs/DEVELOPMENT.md`.

### Type checking (separate from tests — required)

Vitest uses esbuild, which strips TypeScript types at runtime. A test run can pass while `tsc` reports errors. Always run type checks separately:

```bash
pnpm typecheck          # runs tsc --noEmit across both apps
pnpm --filter @familysync/api typecheck
pnpm --filter @familysync/pwa typecheck
```

The PWA typecheck also covers the e2e spec files: `tsc --project tsconfig.e2e.json --noEmit`.

## Quality gate

The full local quality gate before opening a PR:

```bash
pnpm lint && pnpm format:check && pnpm typecheck && pnpm test
```

Add e2e when changing PWA behaviour:

```bash
pnpm test:e2e
```

| Step             | Command                              | What it checks                                           |
| ---------------- | ------------------------------------ | -------------------------------------------------------- |
| Lint             | `pnpm lint`                          | ESLint `--max-warnings 0` across both apps (type-aware)  |
| Format check     | `pnpm format:check`                  | Prettier — fails on any unformatted file                 |
| Typecheck        | `pnpm typecheck`                     | `tsc --noEmit` across both apps (including e2e tsconfig) |
| Unit / API tests | `pnpm test`                          | API integration tests via Vitest                         |
| PWA unit tests   | `pnpm --filter @familysync/pwa test` | Component and logic tests in jsdom                       |
| E2E              | `pnpm test:e2e`                      | Playwright iphone + pixel profiles                       |

A deliberate ESLint violation makes `pnpm lint` exit non-zero; a formatting deviation makes `pnpm format:check` exit non-zero. Both block the PR in CI.

To auto-fix formatting locally:

```bash
pnpm format     # prettier --write .
```

## Integration tests requiring a real database

Several API tests in `apps/api/tests/lib/` and `apps/api/tests/routes/` connect to the real dev MariaDB rather than mocking the DB layer. These tests require the dev Docker stack to be running with port 3306 exposed.

**Start the dev stack:**

```bash
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mariadb
```

**Set environment variables, then run:**

```bash
set -a; . ./.env; set +a
export DB_HOST=127.0.0.1 DB_PORT=3306
pnpm --filter @familysync/api test
```

DB-backed tests that require this setup include:

- `apps/api/tests/lib/listAccess.test.ts` — `getAccessibleListIds` access-scope queries
- `apps/api/tests/lib/listChangeDispatcher.test.ts` — list change dispatcher with real DB rows
- `apps/api/tests/routes/lists.test.ts` — full lists API router (creates/deletes real rows)

Pure-logic tests (e.g. `apps/api/tests/broker/expand.test.ts`, `apps/api/tests/lib/rank.test.ts`) do not require DB — the `afterEach` cleanup is a no-op when tables are empty or no DB connection is available.

## Writing new tests

### File naming and location

| App        | Convention                            | Example                              |
| ---------- | ------------------------------------- | ------------------------------------ |
| `apps/api` | `apps/api/tests/{category}/*.test.ts` | `apps/api/tests/routes/push.test.ts` |
| `apps/pwa` | co-located `*.test.ts` / `*.test.tsx` | `src/components/AppNav.test.tsx`     |
| `apps/pwa` | e2e specs                             | `e2e/*.spec.ts`                      |

Test categories for `apps/api`:

- `apps/api/tests/auth/` — authentication middleware and session handling
- `apps/api/tests/broker/` — CalDAV sync, outbox worker, event expansion, push dispatch
- `apps/api/tests/lib/` — pure library functions and service logic
- `apps/api/tests/routes/` — HTTP route integration tests
- `apps/api/tests/health.test.ts` — health check endpoint
- `apps/api/tests/fixtures/` — shared `.ics` fixture files and DB fixture helpers

### Test helpers

- `apps/api/tests/helpers/db.ts` — `createMockDb()` returns a Vitest mock of the Drizzle `db` singleton; also exports sample VEVENT strings (`SAMPLE_VEVENT_TIMED`, `SAMPLE_VEVENT_ALLDAY`, `SAMPLE_VEVENT_RECURRING_TIMED`, `SAMPLE_VEVENT_RECURRING_ALLDAY`) for broker tests.
- `apps/api/tests/fixtures/*.ics` — Raw iCalendar fixture files for broker parsing tests (`allday-birthday.ics`, `exdate-series.ics`, `single-duration.ics`, `weekly-count3.ics`, `weekly-dst.ics`).
- `apps/api/tests/fixtures/vapid.ts` — VAPID key fixture for push tests.
- `apps/pwa/src/test-setup.ts` — Provides `matchMedia` polyfill and jest-dom matchers for all PWA tests automatically via `setupFiles`.

For PWA component tests, use `@testing-library/react` (`^16.3.0`) render helpers. Import from `vitest` for assertions — `@testing-library/jest-dom` matchers are available globally via the setup file.

## Coverage requirements

No coverage thresholds are configured in either `vitest.config.ts`. There is no minimum coverage enforcement in CI.

## CI integration

CI runs on a self-hosted Gitea Actions runner and triggers on every pull request targeting `main` (`.gitea/workflows/ci.yml`). Three jobs run in parallel:

### `fast-checks`

Runs lint, format check, typecheck, and PWA unit tests — no external services required.

| Step           | Command                              |
| -------------- | ------------------------------------ |
| Lint           | `pnpm lint`                          |
| Format check   | `pnpm format:check`                  |
| Typecheck      | `pnpm typecheck`                     |
| PWA unit tests | `pnpm --filter @familysync/pwa test` |

### `api`

Runs the full API test suite against a `mariadb:11` service container.

| Step              | Detail                                                     |
| ----------------- | ---------------------------------------------------------- |
| MariaDB service   | `mariadb:11` container; `DB_HOST=mariadb`, `DB_PORT=3306`  |
| Readiness poll    | Node script via `mysql2` driver (no `mysql` CLI in runner) |
| Schema migrations | `pnpm --filter @familysync/api db:migrate`                 |
| Tests             | `pnpm --filter @familysync/api test`                       |

The throwaway credentials (`DB_USER=familysync`, `DB_PASSWORD=testpass`) are scoped to the ephemeral CI container and are never production secrets.

`actions/cache@v4` is intentionally omitted — the cache server times out on this runner (socket hang-up). `pnpm install` without cache takes ~30 s and is acceptable.

### `harness`

Runs the Playwright mobile e2e harness (iphone + pixel) against a runner-hosted dev stack.

| Step               | Detail                                                                                                                                                 |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| MariaDB service    | Same `mariadb:11` setup as the `api` job                                                                                                               |
| Schema migrations  | `pnpm --filter @familysync/api db:migrate`                                                                                                             |
| Dev user seed      | Inserts `users` row id=1 (`INSERT IGNORE`) for `DEV_AUTH_BYPASS`                                                                                       |
| API build          | `pnpm --filter @familysync/api build` (dist/ is gitignored)                                                                                            |
| Playwright install | `npx playwright install --with-deps webkit chromium` (no cache)                                                                                        |
| API start + tests  | API started as a background process in the same step as `playwright test` to survive the step boundary; `DEV_AUTH_BYPASS=true`, `NODE_ENV=development` |
| Base URL           | `http://127.0.0.1:5173` (not `localhost` — runner resolves `localhost` to `::1` but Vite binds IPv4-only)                                              |
| Artifacts on fail  | Traces, screenshots, videos, and HTML report uploaded via `ChristopherHX/gitea-upload-artifact@v4` (standard `upload-artifact` aborts on Gitea)        |

The API process is started and the Playwright suite invoked within a single CI step. Starting the API in an earlier step causes it to be reaped at the step boundary before Playwright runs.
