test(worker): Stopwatch + reconcile — fill gaps + lifecycle test

Fill the Stopwatch domain to 100% coverage (Stopwatch.tsx + stopwatch.ts,
both now 100% stmts/branch/funcs/lines) and add the reconcile lifecycle test.

New tests cover: idle display-tap start (and no-op without a handling),
pair-count stepper floor + free-typed entry (non-positive ignored), clearing
the handling via the empty option, stop/discard onSuccess reset, discard
arm-window expiry, clearing a pending discard timer on reconcile and on
unmount, and the reconcile lifecycle — running locally, then a poll without
the local session resets the timer and shows the admin-stopped notice.

No behaviour changes. Five defensive/unreachable guards in Stopwatch.tsx are
annotated /* c8 ignore next -- <reason> */ (comment-only): the
paused_seconds ?? 0 coalesce (contract types it non-null) and the early-return
guards in handleSelectZool/handleStart/handleStop/handleDiscard (each triggered
by a control disabled or unrendered in that state).

No bugs found.
This commit is contained in:
Bas van Rossem
2026-07-23 15:33:03 +02:00
parent 75c5a9f350
commit a30441b2a2
3 changed files with 239 additions and 1 deletions

View File

@@ -558,3 +558,43 @@ Guaranteed behaviors:
- Lifecycle (`src/screens/Activities.test.tsx`): create a handeling → edit its insole types down to a
single type → reorder it above another → delete it, with each step reflected in the refetched list;
the Sidebar test (`src/components/Sidebar.test.tsx`) covers navigation switching screens.
## Stopwatch + reconcile — worker client
Src covered (worker `apps/worker`): `src/screens/Stopwatch.tsx` and `src/lib/stopwatch.ts` — both at
100% stmts/branch/funcs/lines. Five `c8 ignore`s with reasons in `Stopwatch.tsx`: the
`paused_seconds ?? 0` coalesce in the reconcile adopt (the contract types `paused_seconds` as
`z.number().int()`, never null) and the four early-return guards in `handleSelectZool`, `handleStart`,
`handleStop`, and `handleDiscard` — each unreachable because the button that triggers it is
disabled or not rendered in exactly that state (and the `handleStart` guard also narrows the type).
Guaranteed behaviors:
- `formatTime` renders zero-padded `HH:MM:SS`, floors a negative total at `00:00:00`, and lets hours
exceed two digits; `elapsedSeconds` returns `(now − start − paused)` floored at 0 (paused time
excluded).
- The three sections (`Type zool` / `Type handeling` / `Aantal zolen`) and the Dutch Start button
render; the zool segmented control offers `Kurk` / `Berk` / `3D` with `Kurk` selected by default.
- Choosing a zooltype filters the handeling `<select>` to activities whose `insole_types` include that
type and clears any previously chosen handeling; picking the empty option (`Kies een handeling...`)
also clears the chosen handeling.
- Start is disabled until a handeling is chosen; starting fires `useStartSession` with
`{ activity_id, insole_type, pair_count }` from the current selection.
- Pair count: the stepper `+`/`−` buttons adjust the count with a floor of 1 (the `−` button is
disabled at 1); the free-typed field mirrors the raw text but only adopts a parsed value greater
than 0 (a non-positive or non-numeric entry keeps the last valid count).
- The stopwatch display is tappable: while idle with a handeling chosen it starts the session (idle
tap with no handeling is a no-op); while running it pauses via `usePauseSession`; while paused it
resumes via `useResumeSession` (folding the just-finished pause span into the local accumulator).
- On a successful start the screen adopts the returned session's running state (id, start time, cleared
pause); a paused server session is recovered into the paused UI (`Gepauzeerd — tik om te hervatten`).
- Stop (`Stop & Opslaan`) and discard both reset the timer to idle on success; a `409` (already closed
server-side) on either resets locally instead of leaving the timer stuck.
- Discard is a double-tap: the first `Annuleren` tap arms it (`Nogmaals tikken ter bevestiging`) and the
second confirms and fires `useDiscardSession`; the arm window lapses after 3s back to `Annuleren`, and
a pending arm timer is cleared both on reconcile-reset and on unmount.
- Reconcile against the active-sessions poll: while idle with a server-active session, the screen adopts
it (phone-died / resume-elsewhere); the running-vs-server check keys on session id.
- Lifecycle (`src/screens/Stopwatch.test.tsx`): running locally after a successful start, when the
active-sessions poll returns a list **without** the local session (only another worker's), the timer
resets to idle and the `Deze sessie is door de beheerder gestopt.` notice shows.