# Sprint 14 Verification — Vitest for `useOnboarding` (Q4)

**Date:** 2026-06-05. **Owner:** this agent. **Status:** code complete, 7/7 tests green, `npm run build` green, awaiting commit + push.

## What Sprint 14 does

Adds Vitest + @testing-library/react + happy-dom to the frontend devDeps and locks the `useOnboarding` state-transition contract with 7 unit tests. The S9 bug class (wiring `onComplete` to a call that flips `isComplete` to `false`, e.g. `onboarding.reset()`) is now caught at `npm test` time. Sprint 9 (F1 Onboarding Tour) shipped a hand-rolled ~420-line component; the bug `1562929` shipped a post-deploy fix the same day. Sprint 14 prevents recurrence.

## Why this sprint, why this scope

- **Q4** (open question from Sprint 9): "Add Vitest to lock `useOnboarding` state transitions." Cost: ~1 hr. Value: prevents the S9 bug class. Decision: lift the "no new npm deps" rule for **testing-only** (devDeps).
- **Hook-only, not component-level.** The S9 bug was at the App.tsx call-site. Sprint 14 includes one integration test (Case 7) that statically reads `App.tsx` and asserts the wiring. Component-level tests (focus, arrow keys, dialog a11y) are a future sprint.

## Files added

- `frontend/vitest.config.ts` — happy-dom env, setup file, `src/**/*.test.{ts,tsx}` glob.
- `frontend/vitest-setup.ts` — `@testing-library/jest-dom/vitest` matchers.
- `frontend/src/components/OnboardingTour.test.tsx` — 7 cases.

## Files modified

- `frontend/package.json` — 4 new devDeps (`vitest`, `happy-dom`, `@testing-library/react`, `@testing-library/jest-dom`) + 1 devDep for tsc (`@types/node`) + 2 scripts (`test`, `test:watch`).
- `.agent/plan.md` — S14 sections (S14.1-S14.4 + Done when + Out of scope).
- `.agent/context.md` — S14 decisions (D1-D7), open question (Q1), file:line references.

## Test cases (S14.3)

| # | Case | What it locks |
|---|------|---------------|
| 1 | clean init | `isComplete === false` when localStorage is empty |
| 2 | persisted init | `isComplete === true` when `localStorage.getItem(KEY) === '1'` |
| 3 | `markComplete` | state → true, localStorage **stays** at `'1'` (locks one direction of the S9 bug) |
| 4 | `reset` | localStorage cleared, state → false |
| 5 | `show` | identical to `reset` (intentional mirror) |
| 6 | localStorage throw on read | silently swallowed, `isComplete === false`, no crash |
| 7 | App.tsx wiring | static check: `onComplete` calls `markComplete`, `onReset` calls `reset`; neither inverts (locks the original S9 bug at the call site) |

## Verification commands

```bash
ssh docker-willester
cd /home/peter/MealPlanner/frontend

# 1. install (already done on host; rerun if pulling to a fresh checkout)
npm install

# 2. run tests
npm test
# → 7 passed in ~25 ms

# 3. build (still green; vitest is devDeps only)
npm run build
# → tsc 0 errors, vite built in ~2.6 s, bundle 503.82 kB unchanged

# 4. prove Case 7 catches the bug (manual)
sed -i 's|onboarding.markComplete()|onboarding.reset()|' src/App.tsx
npm test
# → 1 failed (Case 7: onCompleteBody does not match /markComplete/)
sed -i 's|onboarding.reset()|onboarding.markComplete()|' src/App.tsx  # restore
npm test
# → 7 passed
```

## Deploy

No backend change. No migration. No new runtime dep. The 5 new devDeps land on `npm install` on the host.

```bash
ssh docker-willester
cd /home/peter/MealPlanner
git pull
cd frontend && npm install && npm test   # confirm 7/7 in CI
cd .. && docker compose up -d --build frontend
```

If the user wants Vitest in CI (currently not wired), that is a follow-up — outside Sprint 14 scope.

## What Sprint 14 does NOT do

- **No component-level tests for `<OnboardingTour/>`.** Focus, arrow keys, dialog a11y, portal positioning. Future sprint. Would add `@testing-library/user-event` for keyboard simulation.
- **No backend tests.** Venv on `docker-willester` is broken (Nix symlinks to `/run/current-system/sw/bin/python`). Frontend-only sprint.
- **No CI integration.** Tests are local-only. Wiring them into a CI runner is a follow-up.

## Risk table

| Risk | Mitigation | Status |
|------|------------|--------|
| Vitest conflicts with Vite 5 config | vitest.config.ts uses `defineConfig` from `vitest/config` which extends Vite's; tested, no conflict | Resolved |
| `node:*` imports in tests break tsc | Added `@types/node` as devDep | Resolved |
| Tests slow CI | 7 cases run in 25 ms; not a concern | N/A |
| Devs forget to run `npm test` before commit | Follow-up: pre-commit hook or CI; not in Sprint 14 | Open |
| Future refactor adds a hook dependency that breaks Case 7's regex | Case 7 uses a single-shot regex match; if `App.tsx` gains a `markComplete` call in unrelated code (e.g. button), the regex still works because the `onComplete` arrow body is the captured group | Resolved (verified) |

## Open question for follow-up

**Q1 — Should Sprint 15 add component-level tests for `<OnboardingTour/>` (focus, arrow keys, dialog a11y)?** Default: yes, future sprint. Adds `@testing-library/user-event`. ~1.5 hr.
