e-safework — Manual E2E Test Plan
Status (2026-08-19): suites written, fixtures live, nothing executed yet. This is the master plan across all three repos; each frontend carries its own harness (docs/modules/e2e-manual/) and its own suite files. 19 suites / ~170 cases exist under each repo's docs/testing/suites/.
Reading the source to write those suites produced 15 findings, three of them P0, before a single test ran — see PRE-RUN-FINDINGS.md.
1. The two blockers that shaped this plan — both now resolved
Resolved on 2026-08-19 by E2E-000 (smart-work-permit-api feat-013): bun run seed:e2e creates one account per role plus the full F0–F6 fixture set, and bun run seed:e2e:reset returns to baseline. Verified independently of the agent that built it — all three roles log in with the right role, the contractor sees 10 permits spanning all seven statuses and all three types, and both closure guards answer their 403. That verification closed a fixture permit on purpose; the reset restored the exact baseline and reported the audit chain intact.
The Contractor app can now be logged into for the first time.
The two subsections below are kept as the record of why the plan is shaped this way — a plan that opens with "run everything" would not have surfaced either blocker. Read them as history, not as current state.
B1 — Only one account exists, and it is a Safety Officer
GET /api/v1/permits and a live login confirm exactly one seeded user:
| Role | |
|---|---|
systemadmin@email.com / password123 | safety_officer |
There is no contractor account and no inspector account. Consequences:
- The Contractor app cannot be tested at all — its login refuses a non-contractor outright (
API-003). - Every Inspector suite is unexecutable (QR scan, entrant register, gas log, offline queue).
POST /auth/user/public/registertakes norolefield (verified againstdocs/openapi.json), so you cannot create these accounts through the app. This needs a backend change — a seed that creates one account per role — which belongs tosmart-work-permit-api, not to either frontend.
Resolved 2026-08-19 by
E2E-000(smart-work-permit-apifeat-013):bun run seed:e2ecreates one account per role and the full F0–F6 fixture set. Credentials and fixture ids are in § 7. B1 and B2 describe the state before that seed existed; the suites they blocked are now executable.
Also worth a look while someone is in there: GET /api/v1/users returns 0 rows for a safety_officer session even though a user demonstrably exists. Either intentional scoping or a bug; nobody has checked.
B2 — There is no test data to walk the main flow through
Current database contents: 2 permits, both DRAFT, both Hot Work. Nothing is PENDING, ACTIVE, FIRE_MONITOR, CLOSED, REJECTED or EXPIRED. No certificates, no entrants, no gas readings.
So today: the Review Queue is legitimately empty, no permit can be approved, no QR exists to scan, no entrant can be checked in, and no closure guard (ENTRANTS_STILL_INSIDE, FIRE_WATCH_NOT_ELAPSED) can be exercised. A tester following any end-to-end script would stop on step 2 and file a false defect.
Neither blocker is a frontend bug, and neither can be fixed from a frontend repo. E2E-000 (below) is the first item in both harnesses and every other suite depends on it.
2. What "the whole features" actually means right now
Manual E2E against half-built screens produces noise, not signal. Current build state, from each repo's own registries:
Safety/Inspector app — feat-001…feat-005 all done. All six Safety Officer screens and all four Inspector screens exist. feat-006 (shell/theme) is mid-flight but cosmetic. Testable in full, once accounts and data exist.
Contractor app — feat-003 (History) and feat-004 (Certificates) done; feat-005 (API) done. But feat-002 (Permit core) has 7 of 12 items not started:
| Not built | What it means for testing |
|---|---|
PMT-006 Wizard step 3 — Safety Checks | stub: z.object({}), so Next is never blocked |
PMT-007 Wizard step 4 — PPE, photos, workers | stub, same |
PMT-008 Wizard step 5 — JSA | stub, same |
PMT-009 Wizard step 6 — Review & Submit | reachable, and Submit enables — then only writes a console.info. Nothing can be submitted from the UI |
PMT-010 Permit Detail — banners, QR, audit timeline | No detail screen to verify |
PMT-011 Closure checklist modal | Closure guards untestable from the UI |
PMT-012 Mark-complete + Fire Watch countdown | — |
PLT-007 Notification polling, CRT-004 cert gate | Two cross-cutting rules unverifiable |
Every suite covering those is written now but marked not-built, not fail. A tester must be able to tell "this is broken" from "this does not exist yet" — conflating them is how manual passes lose trust.
Correction (2026-08-19). An earlier draft of this plan said "the wizard cannot get past step 2". That is wrong, and wrong in the more dangerous direction: steps 3–5 are stub components whose Zod schemas are
z.object({}), which always validate. So the wizard walks all the way to step 6, no step ever blocks Next, and Submit enables once a draft exists — then writes aconsole.infoand nothing else. A tester who has not read this will walk the whole wizard, see no validation, and file "Next should have blocked me on an out-of-range reading" as a defect. It is not one yet; the screen does not exist.CT-WIZARD.mdis written to what a tester actually sees.
3. Scope
In scope. Human-executed, scripted E2E across the two web apps against a real backend: every screen, every role, the cross-app permit lifecycle, the server-authoritative safety rules, EN/TH locale coverage, and the two responsive breakpoints the product commits to (375–430px phone, ≥1280px desktop).
Out of scope for this pass — say so rather than pretending:
- Automated Playwright specs. This is a manual plan; the suites are written so they can be automated later, and
playwright.config.tsalready exists in both repos when that day comes. - Real camera hardware (Inspector QR). The manual-entry fallback is testable; a physical badge scan is not scriptable here and is called out per case.
- True offline behaviour beyond devtools "Offline" — no plant-floor signal loss simulation.
- Load, security and accessibility audits. Different disciplines, different plans.
4. Environment
| Piece | How to start | Port |
|---|---|---|
| Backend | cd smart-work-permit-api && bun run seed && bun run seed:e2e && bun run dev | :3000 |
| Contractor app | cd smart-work-permit-contractor-frontend && bun run dev | :8080 |
| Safety/Inspector app | cd smart-work-permit-frontend && bun run dev --port 8081 | :8081 |
Do not run docker compose up -d in the API repo — its compose file defines only the api service and references ${ENV_FILE} / ${HOST_PORT}, which are not in its .env. Postgres, Redis and MinIO come from the shared local stack; check with docker ps and confirm the schema with bunx prisma migrate status.
Both frontends default to vite :8080 with strictPort: false, so whichever starts second silently lands on :8081 — and the backend's CORS_ORIGIN names specific origins, so an unexpected port makes login appear to succeed and then 401 every subsequent call. Pin the ports. See ../../CONTEXT.md § 4.
Nothing is running right now — all three servers were killed by session cleanup on 2026-08-19. Start them in the order above before any run; the backend must be up first or login fails silently.
Browsers: Chrome (primary), Safari (iOS-family check for the Inspector phone flows). Timezone must be Asia/Bangkok — timestamps are stored UTC and displayed local, and a tester in another zone will file false defects on every date.
Two stale claims in the repos' own docs — do not trust them mid-run
A tester following the repo docs will hit both of these:
smart-work-permit-contractor-frontend/CLAUDE.md§ Agent harness says "It is red today" of./init.sh, while its own state block two sections above says green. Run the gate and believe the output, not either sentence.- The same file says the
historymodule is "Not built — its route is not registered", butHistory.router.tsexists, is registered, andfeat-003isdonein the registry.CT-HISTORYis a real suite; do not skip it on the strength of that line.
Both are documentation drift, not product defects — but they will cost a tester an hour each.
5. The three journeys that matter
Suites are grouped by screen, but the point of E2E is the cross-app spine. These three are the ones a release must not break:
J1 — Permit lifecycle (cross-app). Contractor drafts → submits → Safety Officer reviews the backend's validation verdict → approves → QR issues → Inspector scans and sees live status → Inspector runs the entrant register → Safety Officer closes → the audit log shows every step in order. Blocked by PMT-009 (no submit) — see § 2.
J2 — The guards hold (server-authoritative). Out-of-range gas blocks progression with no override; an expired certificate blocks field entry and writes CERT_BLOCKED; Hot Work refuses to close before the 30-minute Fire Watch elapses (403 FIRE_WATCH_NOT_ELAPSED), no override. Amended by wayfinder 098 (2026-09-11): a Confined Space permit with an entrant still inside no longer refuses to close — it closes, auto-checking every open entrant out at the closure timestamp with 'system' provenance; 403 ENTRANTS_STILL_INSIDE is retired (never emitted, though still declared). What to verify instead: closing succeeds, the entrant register is empty afterward, and the officer's reason was required (403 CLOSURE_REASON_REQUIRED without one — now unconditional, since only safety_officer reaches POST /:id/close at all; a contractor/inspector gets POST /:id/close-request instead, which raises a request without closing anything). Each must be verified from the UI, and the UI must surface the server's verdict — the frontends are forbidden from recomputing these.
J3 — Field reality. Inspector on a phone: offline actions queue with a client id, sync on reconnect, and conflicts surface rather than silently vanishing. Touch targets ≥44px. Thai default locale.
6. Case format
Every case lives in a suite file as a row, and every row is executable by someone who has never seen the app. Shared format across both repos:
### CT-PL-003 — Filter chips narrow the list
Priority: P1 · Role: contractor · Viewport: 1280 / 390 · Locale: th, en
Preconditions: logged in as contractor; ≥1 ACTIVE and ≥1 CLOSED permit exist (fixture F2)
1. Open My Permits.
2. Tap the "Active" chip.
3. Tap the "Closed" chip.
Expected:
- Step 2 shows only ACTIVE and FIRE_MONITOR permits; the chip reads selected.
- Step 3 shows only CLOSED and REJECTED permits.
- The result count matches the number of cards rendered.
Known gap: the API takes a single status value, so grouped chips filter client-side and the
paginated total counts every status (contractor GAPS.md row B). A short page is expected, not a defect.Known gap is load-bearing: where a documented backend gap makes the correct behaviour look wrong, the case says so, so the tester does not re-file it.
Result values: pass · fail · blocked (a dependency failed) · not-built (the feature does not exist yet) · skip (out of scope for this run, with a reason). Never leave a case blank.
Every fail gets a defect entry with: case id, environment, steps to reproduce, expected vs actual, screenshot, browser console output, and the failing network request. A fail without a repro is a rumour.
7. Fixtures
E2E-000 must produce a seed that creates these, because a manual pass cannot create most of them through the UI today:
| Id | Fixture |
|---|---|
| F0 | One account per role: contractor, safety_officer, inspector (documented credentials) |
| F1 | One permit per type (hot, confined, heights) in DRAFT |
| F2 | One permit in each status: PENDING, ACTIVE, FIRE_MONITOR, CLOSED, REJECTED, EXPIRED |
| F3 | Workers with a valid certificate and workers with an expired one (drives the CERT_BLOCKED path) |
| F4 | A Confined Space permit with an entrant currently checked in (drives ENTRANTS_STILL_INSIDE) |
| F5 | A Hot Work permit in FIRE_MONITOR with the countdown still running (drives FIRE_WATCH_NOT_ELAPSED) |
| F6 | A gas log with a reading old enough to trigger the "overdue" banner |
The seed must be idempotent and re-runnable, and reset must be one command — a manual pass that mutates state (approve, close, check in) cannot be repeated otherwise, and the second tester gets different results from the first.
Status: E2E-000 is DONE (2026-08-19, smart-work-permit-api feat-013)
cd smart-work-permit-api
bun run seed:e2e # create/refresh F0–F6 — idempotent, re-run any time
bun run seed:e2e:reset # RESET: teardown + reseed, back to the post-first-seed baseline| Password | Role | App | |
|---|---|---|---|
contractor@e2e.test | password123 | contractor | Contractor app |
safety@e2e.test | password123 | safety_officer | Safety/Inspector app |
inspector@e2e.test | password123 | inspector | Safety/Inspector app |
systemadmin@email.com | password123 | safety_officer | pre-existing, untouched |
Fixture permit ids all contain -E2E- and are owned by contractor@e2e.test: WP-{HOT,CONF,HT}-E2E-001 are the DRAFTs (F1); WP-HOT-E2E-002 is PENDING; WP-CONF-E2E-002 is ACTIVE and carries the entrant who is still inside (F4), the stale gas log (F6) and the expired-/missing-cert workers (F3); WP-HOT-E2E-003 is FIRE_MONITOR with the countdown elapsed (closable) and WP-HOT-E2E-004 is FIRE_MONITOR with it still running (F5); WP-HT-E2E-002, WP-HOT-E2E-005 and WP-CONF-E2E-003 are CLOSED, REJECTED and EXPIRED.
Workers: Somchai Prasert (valid cert) · Wichai Thongdee (valid, expiring in 14 days) · Anan Wongchai (expired → CERT_EXPIRED + CERT_BLOCKED) · Krit Boonmee (no cert at all → CERT_MISSING).
The Fire Watch countdown and the stale gas reading are anchored to seed time, so re-run bun run seed:e2e if a session sits idle long enough for the countdown to elapse — the fixture does not rot, but it does tick.
8. Suites
Per-repo detail lives in each harness. Summary:
Safety/Inspector app (smart-work-permit-frontend/docs/modules/e2e-manual/)
| Suite | Covers | Executable today |
|---|---|---|
SO-AUTH | login, role routing, route guards, logout, session expiry | ✅ |
SO-QUEUE | Review Queue: list, empty state, notification badge, pagination | after E2E-000 |
SO-REVIEW | Review Detail: validation verdict, approve + e-signature, reject with reason | after E2E-000 |
SO-PERMITS | All Permits register: search, filter, sort, bulk approve, CSV export | after E2E-000 |
SO-MAP | Live Facility Risk Map: pins, statuses, mini-summary | after E2E-000 |
SO-AUDIT | Audit Log: filters, pagination, append-only, CERT_BLOCKED rows | after E2E-000 |
SO-DASH | Executive Dashboard: 7-day chart, counts, expiring certs | after E2E-000 |
IN-SCAN | QR scan, camera-denied state, manual permit-id fallback, live status | after E2E-000 |
IN-ENTRANT | Entrant register, badge scan, cert-blocked deny state | after E2E-000 |
IN-GAS | Gas log: colour-coded pass/fail, overdue banner, add reading | after E2E-000 |
IN-OFFLINE | Offline queue, sync-now, conflict surfacing | after E2E-000 |
XX-SHELL | Shell, nav, EN/TH switch, responsive at 390/768/1440, 44px targets | ✅ |
Contractor app (smart-work-permit-contractor-frontend/docs/modules/e2e-manual/)
| Suite | Covers | Executable today |
|---|---|---|
CT-AUTH | login (contractor-only), guard, reset password, logout | after E2E-000 (F0) |
CT-PERMITS | My Permits: cards, status chips, empty state | after E2E-000 |
CT-WIZARD | 6-step wizard, per-step validation, draft persistence | walks to step 6 on empty stub schemas; PMT-006…009 not built, Submit is a console.info |
CT-DETAIL | Permit Detail: banners, QR, closure modal, audit timeline | not built — PMT-010…012 |
CT-HISTORY | History: search, filters, date range, CSV export, pagination (drill-in is a drawer, not a route) | after E2E-000 |
CT-CERTS | Certificates: list, valid/expiring/expired badges, add form | after E2E-000 |
XX-SHELL | Shell, nav, EN/TH switch, responsive, 44px targets | ✅ |
9. Order of work
E2E-000— the backend seed (F0–F6) and a documented reset command. Everything else waits on it. This is asmart-work-permit-apichange; both frontend harnesses depend on it and neither can fix it.XX-SHELLandSO-AUTH— the two suites executable today. Run them first; they will find shell and guard defects while the seed is being written.- The Safety/Inspector suites — that app is feature-complete, so a full pass there is meaningful.
- The Contractor suites that have screens (
CT-PERMITS,CT-HISTORY,CT-CERTS). CT-WIZARD/CT-DETAIL— asPMT-006…012land, not before.J1cross-app journey last: it needs both apps and cannot run until the wizard can submit.
10. Open questions for the reviewer
- Who executes this? Written for a human tester. If the answer is "an agent driving Chrome", the suites need machine-checkable assertions (selectors, exact copy) rather than prose expectations — a different, larger document. Worth deciding before the cases are mass-produced.
- Is
E2E-000(the backend seed) in scope for us, or does that repo own it? It is the gate on everything else. - Contractor suites for unbuilt screens — write them now against the design prototype and the task doc (they become the acceptance script for
PMT-006…012), or wait until the screens exist? Writing now is more useful and risks churn if the implementation diverges. - Real device coverage — is an actual phone required for the Inspector suites, or is Chrome device emulation acceptable? Camera behaviour genuinely differs.