Skip to content

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:

EmailRole
[email protected] / password123safety_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/register takes no role field (verified against docs/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 to smart-work-permit-api, not to either frontend.

Resolved 2026-08-19 by E2E-000 (smart-work-permit-api feat-013): bun run seed:e2e creates 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 appfeat-001feat-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 appfeat-003 (History) and feat-004 (Certificates) done; feat-005 (API) done. But feat-002 (Permit core) has 7 of 12 items not started:

Not builtWhat it means for testing
PMT-006 Wizard step 3 — Safety Checksstub: z.object({}), so Next is never blocked
PMT-007 Wizard step 4 — PPE, photos, workersstub, same
PMT-008 Wizard step 5 — JSAstub, same
PMT-009 Wizard step 6 — Review & Submitreachable, and Submit enables — then only writes a console.info. Nothing can be submitted from the UI
PMT-010 Permit Detail — banners, QR, audit timelineNo detail screen to verify
PMT-011 Closure checklist modalClosure guards untestable from the UI
PMT-012 Mark-complete + Fire Watch countdown
PLT-007 Notification polling, CRT-004 cert gateTwo 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 a console.info and 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.md is 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.ts already 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

PieceHow to startPort
Backendcd smart-work-permit-api && bun run seed && bun run seed:e2e && bun run dev:3000
Contractor appcd smart-work-permit-contractor-frontend && bun run dev:8080
Safety/Inspector appcd 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 history module is "Not built — its route is not registered", but History.router.ts exists, is registered, and feat-003 is done in the registry. CT-HISTORY is 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:

IdFixture
F0One account per role: contractor, safety_officer, inspector (documented credentials)
F1One permit per type (hot, confined, heights) in DRAFT
F2One permit in each status: PENDING, ACTIVE, FIRE_MONITOR, CLOSED, REJECTED, EXPIRED
F3Workers with a valid certificate and workers with an expired one (drives the CERT_BLOCKED path)
F4A Confined Space permit with an entrant currently checked in (drives ENTRANTS_STILL_INSIDE)
F5A Hot Work permit in FIRE_MONITOR with the countdown still running (drives FIRE_WATCH_NOT_ELAPSED)
F6A 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)

bash
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
EmailPasswordRoleApp
[email protected]password123contractorContractor app
[email protected]password123safety_officerSafety/Inspector app
[email protected]password123inspectorSafety/Inspector app
[email protected]password123safety_officerpre-existing, untouched

Fixture permit ids all contain -E2E- and are owned by [email protected]: 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 (expiredCERT_EXPIRED + CERT_BLOCKED) · Krit Boonmee (no cert at allCERT_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/)

SuiteCoversExecutable today
SO-AUTHlogin, role routing, route guards, logout, session expiry
SO-QUEUEReview Queue: list, empty state, notification badge, paginationafter E2E-000
SO-REVIEWReview Detail: validation verdict, approve + e-signature, reject with reasonafter E2E-000
SO-PERMITSAll Permits register: search, filter, sort, bulk approve, CSV exportafter E2E-000
SO-MAPLive Facility Risk Map: pins, statuses, mini-summaryafter E2E-000
SO-AUDITAudit Log: filters, pagination, append-only, CERT_BLOCKED rowsafter E2E-000
SO-DASHExecutive Dashboard: 7-day chart, counts, expiring certsafter E2E-000
IN-SCANQR scan, camera-denied state, manual permit-id fallback, live statusafter E2E-000
IN-ENTRANTEntrant register, badge scan, cert-blocked deny stateafter E2E-000
IN-GASGas log: colour-coded pass/fail, overdue banner, add readingafter E2E-000
IN-OFFLINEOffline queue, sync-now, conflict surfacingafter E2E-000
XX-SHELLShell, nav, EN/TH switch, responsive at 390/768/1440, 44px targets

Contractor app (smart-work-permit-contractor-frontend/docs/modules/e2e-manual/)

SuiteCoversExecutable today
CT-AUTHlogin (contractor-only), guard, reset password, logoutafter E2E-000 (F0)
CT-PERMITSMy Permits: cards, status chips, empty stateafter E2E-000
CT-WIZARD6-step wizard, per-step validation, draft persistencewalks to step 6 on empty stub schemas; PMT-006009 not built, Submit is a console.info
CT-DETAILPermit Detail: banners, QR, closure modal, audit timelinenot builtPMT-010012
CT-HISTORYHistory: search, filters, date range, CSV export, pagination (drill-in is a drawer, not a route)after E2E-000
CT-CERTSCertificates: list, valid/expiring/expired badges, add formafter E2E-000
XX-SHELLShell, nav, EN/TH switch, responsive, 44px targets

9. Order of work

  1. E2E-000 — the backend seed (F0–F6) and a documented reset command. Everything else waits on it. This is a smart-work-permit-api change; both frontend harnesses depend on it and neither can fix it.
  2. XX-SHELL and SO-AUTH — the two suites executable today. Run them first; they will find shell and guard defects while the seed is being written.
  3. The Safety/Inspector suites — that app is feature-complete, so a full pass there is meaningful.
  4. The Contractor suites that have screens (CT-PERMITS, CT-HISTORY, CT-CERTS).
  5. CT-WIZARD / CT-DETAIL — as PMT-006012 land, not before.
  6. J1 cross-app journey last: it needs both apps and cannot run until the wizard can submit.

10. Open questions for the reviewer

  1. 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.
  2. Is E2E-000 (the backend seed) in scope for us, or does that repo own it? It is the gate on everything else.
  3. Contractor suites for unbuilt screens — write them now against the design prototype and the task doc (they become the acceptance script for PMT-006012), or wait until the screens exist? Writing now is more useful and risks churn if the implementation diverges.
  4. Real device coverage — is an actual phone required for the Inspector suites, or is Chrome device emulation acceptable? Camera behaviour genuinely differs.