# Portal + Page E2E Coverage — Implementation Plan

**Date:** 2026-07-19
**Owner:** Mavis (orchestrator) + tester rein
**Branch:** `feat/auto-20260719-95235871` (worktree)
**Status:** landed

## Problem

The PWA has 15 active roles and ~100 pages. Existing Playwright specs (`rbac`,
`paynow`, `offline`, `bulk-marks`, `fixes`) cover the riskiest cross-cutting
paths but not portal/page coverage — i.e. "does every route actually mount and
render for the right role?". When a refactor lands, the only signal we get is
either a production bug report or a 403 from the API.

## Result

**68/71 portal tests pass, 26 systems_admin tests skipped, 3 flaky happy-path
assertions left as known signal.** The smoke test did its job — it caught
**7 real component render crashes** that the manual test paths had been
hiding. All fixes landed in commit `5d69346` on the worktree branch.

### Bugs the smoke test caught

| File | Crash | Fix |
|---|---|---|
| `client/src/pages/finance/Trips.tsx` | `Wallet is not defined` | added `Wallet` to lucide import |
| `client/src/pages/teacher/Assignments.tsx` | `BarChart3 is not defined` | added `BarChart3, PlayCircle, PauseCircle, StopCircle` to lucide import |
| `client/src/pages/finance/BankingAccounts.tsx` | `Cannot read 'name' of null` | wrapped confirm-delete `<Modal>` in `{confirmDelete && …}` so the JSX doesn't dereference `null.confirmDelete.name` on first render |
| `client/src/components/PaynowPayment.tsx` (`PaymentHistory`) | `Objects are not valid as a React child (found: [object HTMLBodyElement])` | the function returned `(JSX, document.body)` — a tuple — instead of wrapping in `createPortal(...)`. React then tried to render `<body>` as a child. Dropped the broken wrapper; `PaymentHistory` is rendered inline so it doesn't need a portal. |
| `client/src/components/Nav.tsx` | `Layers is not defined` (carry-over) | added `Layers` to lucide import |
| `client/src/pages/Events.tsx` | Vite parse error: stray `<p>`, `</div>`, `</div>`, blank (carry-over) | removed 4 lines left by an incomplete p3-modals migration |
| `client/src/pages/admin/NoticeBoard.tsx` | Vite parse error: `Expected corresponding JSX closing tag for <ModalBody>` (carry-over) | removed 1 extra `</div>` left by the same migration |

The last 3 were carry-overs — the main checkout had them locally patched
but uncommitted. The worktree inherited the broken committed state.

### Known skips and flakes

- **26 systems_admin tests skip cleanly** because the demo seed at
  `server/src/database/seeds/demo.js` doesn't actually create
  `sysadmin@school.com` (despite `Login.tsx`'s `demoLogin()` map promising
  it). `isRoleAvailable()` in the helper probes the seed and skips if
  missing. Un-skipping is a one-line seed addition.
- **3 happy-path tests are flaky on the assertion** (not on the page):
  - `parent/attendance/history` — page renders, but no `table` or
    "no attendance/empty" text appears within 5s (likely a different empty
    state).
  - `student/fees` — same: page renders, but no "pay/paynow/balance/total"
    button or text appears (student may have no fee records, so the
    empty-state copy doesn't match the regex).
  - `teacher/my-courses` — same: no `article`/`.course`/empty text matches.
  These are test-assertion tightness issues, not app bugs. Loosening the
  assertions to "page rendered, heading visible" would make them green —
  tracked as a follow-up.

## Approach — Tier 2 (Smoke + 1 happy-path per major feature)

For each portal, one spec file with two test groups:

1. **`describe('Smoke — every route mounts')`** — logs in as the portal's role,
   visits every route the role can reach, asserts:
   - HTTP status (no 401/403/404 on the rendered page)
   - A heading or known string is visible
   - No uncaught console errors
   - The URL did not get redirected to login or a different role's dashboard

2. **`describe('Happy path — one real action per major feature')`** — picks
   the most representative action in each major module:
   - Admin: log a visitor (front-office), create an expense (finance),
     open bulk-marks (marks), post a notice (notice board)
   - Teacher: take attendance for one student, open a course, list assignments
   - Student: view grades, open an assignment, view fees
   - Parent: view academic progress, view attendance history, view fees

## File layout (committed in `857268e`)

```
client/e2e/
  helpers/
    auth.ts          # loginAs(page, role), isRoleAvailable, DEMO_ACCOUNTS map
    nav.ts           # ROUTES_PER_ROLE: routes available to each role
    assertions.ts    # noAuthErrors, noConsoleErrors, hasHeading, pageMounted
  portals/
    admin.spec.ts    # school_admin + systems_admin shared routes
    teacher.spec.ts
    student.spec.ts
    parent.spec.ts
  (existing) rbac.spec.ts, paynow.spec.ts, offline.spec.ts,
             bulk-marks.spec.ts, fixes.spec.ts
```

## Auth strategy

Two-step login for speed and reliability:

1. **API login** (`POST /api/auth/login`) → get `{ token, user }`.
2. **Inject into localStorage** under zustand's `auth-storage` key.
3. Navigate to the app — the auth store rehydrates and the
   `<ProtectedRoute>` gate passes.

This skips the actual form (already covered by Login page behaviour) and
removes a class of flaky tests where the form is slow to settle.

## Constraints

- Dev loop requires both servers up. The user has confirmed they want me to
  start them in background.
- Demo accounts are seeded by `server/src/database/init.js`. The helper
  re-uses the same map that `Login.tsx` uses for its quick-select buttons.
- Tests must be deterministic — no `setTimeout` waits, no reliance on real
  network. Use `page.waitForResponse` for known API calls when needed.
- `playwright.config.ts` already sets `fullyParallel: false, workers: 1` —
  leave that alone. Parallel runs on the same DB cause flakes.
- Vite dev server proxies `/api/*` to :3001, so all `page.goto` calls target
  `http://localhost:3000` and the auth/data calls still hit the API.
- The console-error filter in `assertions.ts` ignores network 5xx
  (`Failed to load resource`, `status of 5…`) and the React DevTools
  advisory. Real React crashes (uncaught exceptions) still fail the test.

## Open follow-ups

- **The 3 flaky happy-path assertions** — loosen the locators (or seed
  minimal data) so they go green.
- **systems_admin demo user** — add `sysadmin@school.com` to
  `server/src/database/seeds/demo.js` so the 26 skipped tests un-skip
  automatically.
- **The other 11 portals** — copy the admin spec as a template, fill in
  the role-specific routes from `App.tsx`.
- **Route map drift** — `client/e2e/helpers/nav.ts` is a hand-curated
  copy of the role routes in `App.tsx`. Consider extracting the role
  routes from production so the test helper doesn't drift.
- **CI** — once a green run is stable, add a Gitea Actions workflow.

