test(admin): Live view — fill gaps + lifecycle test
Drove Live.tsx from 93.24% to 100% (stmts/branch/funcs/lines); elapsed.ts already 100%. Added tests for the error state, non-array payload fallback, placeholder labels (missing name/activity/type), the busy/disabled action buttons, the loading state, and a combined running+paused lifecycle test. No production behavior changed; no bugs found.
This commit is contained in:
@@ -194,4 +194,101 @@ describe('Live', () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
expect(screen.getByText('00:00:12')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the loading state before the first response resolves', () => {
|
||||
mockApiFetch.mockReturnValue(new Promise(() => {})); // never resolves → stays loading
|
||||
|
||||
renderLive();
|
||||
|
||||
expect(screen.getByText('Laden…')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the error state when the query fails', async () => {
|
||||
mockApiFetch.mockRejectedValue(new Error('boom'));
|
||||
|
||||
renderLive();
|
||||
|
||||
expect(await screen.findByText('Kon gegevens niet laden.')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Actief nu/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('treats a non-array payload as no active sessions', async () => {
|
||||
mockApiFetch.mockResolvedValue({} as unknown as WorkSession[]);
|
||||
|
||||
renderLive();
|
||||
|
||||
expect(await screen.findByText('Niemand is nu aan het werk.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Actief nu (0)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to placeholder labels and hides the pill when name/activity/type are missing', async () => {
|
||||
mockApiFetch.mockResolvedValue([
|
||||
makeSession({ id: 1, user_name: undefined, activity_name: undefined, insole_type: null }),
|
||||
]);
|
||||
|
||||
renderLive();
|
||||
|
||||
expect(await screen.findByText('Onbekend')).toBeInTheDocument();
|
||||
expect(screen.getByText('Onbekende handeling')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kurk')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables both action buttons while a stop is in flight', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockApiFetch.mockImplementation((path: string) => {
|
||||
if (path === '/api/admin/sessions/active') {
|
||||
return Promise.resolve([makeSession({ id: 7, user_name: 'Jan' })]);
|
||||
}
|
||||
return new Promise(() => {}); // stop mutation never resolves → stays pending
|
||||
});
|
||||
|
||||
renderLive();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Stop' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Stop' })).toBeDisabled();
|
||||
});
|
||||
expect(screen.getByRole('button', { name: 'Annuleer' })).toBeDisabled();
|
||||
});
|
||||
|
||||
// Lifecycle: active-sessions data renders cards, and a paused session shows the paused treatment.
|
||||
it('lifecycle: renders a running card and a paused card side by side', async () => {
|
||||
const start = Date.now() - 100_000;
|
||||
const pausedAt = Date.now() - 40_000;
|
||||
mockApiFetch.mockResolvedValue([
|
||||
makeSession({
|
||||
id: 1,
|
||||
user_name: 'Jan',
|
||||
activity_name: 'Frezen',
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 4,
|
||||
paused_at: null,
|
||||
paused_seconds: 0,
|
||||
}),
|
||||
makeSession({
|
||||
id: 2,
|
||||
user_name: 'Piet',
|
||||
activity_name: 'Lijmen',
|
||||
insole_type: 'Berk',
|
||||
pair_count: 6,
|
||||
start_time: new Date(start).toISOString(),
|
||||
paused_at: new Date(pausedAt).toISOString(),
|
||||
paused_seconds: 90, // 00:01:30
|
||||
}),
|
||||
]);
|
||||
|
||||
renderLive();
|
||||
|
||||
// Both cards render under the header count.
|
||||
expect(await screen.findByText('Actief nu (2)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Jan')).toBeInTheDocument();
|
||||
expect(screen.getByText('Frezen')).toBeInTheDocument();
|
||||
expect(screen.getByText('Piet')).toBeInTheDocument();
|
||||
expect(screen.getByText('Lijmen')).toBeInTheDocument();
|
||||
|
||||
// Exactly the paused session shows the paused treatment.
|
||||
expect(screen.getAllByText('Gepauzeerd')).toHaveLength(1);
|
||||
expect(screen.getByText('Pauze 00:01:30')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -380,3 +380,30 @@ Guaranteed behaviors:
|
||||
- Lifecycle (`src/screens/Login.test.tsx`): from the unauthenticated app, worker credentials are
|
||||
rejected with the `"Geen toegang — alleen beheerders."` message and the shell never renders; admin
|
||||
credentials then flip the gate so the authed shell (Sidebar nav) renders.
|
||||
|
||||
## Live view — admin client
|
||||
|
||||
Src covered (admin `apps/admin`): `src/screens/Live.tsx` and `src/lib/elapsed.ts` — both at
|
||||
100% stmts/branch/funcs/lines. No `c8 ignore`s needed.
|
||||
|
||||
Guaranteed behaviors:
|
||||
|
||||
- Live renders one `.live-card` per active session under a `Actief nu (<count>)` header, each card
|
||||
showing the worker name, the activity, the pair count, and a running worked-time timer.
|
||||
- The per-card worked time is server-authoritative: `worked = (base − start_time) − paused_seconds`,
|
||||
where `base` is `now` while running (a 1s client tick counts it up between the 5s server polls) and
|
||||
the frozen `paused_at` moment while paused, floored at 0. `formatTime` (in `elapsed.ts`) renders it
|
||||
as zero-padded `HH:MM:SS` and never returns a negative time.
|
||||
- A running (not-paused) card's timer keeps counting up on the 1s tick; a paused card shows the
|
||||
`Gepauzeerd` badge and its worked timer stays frozen at the pause moment across ticks.
|
||||
- The `Pauze <duration>` total line shows only when `paused_seconds > 0` (absent when it is 0).
|
||||
- Placeholder fallbacks: a missing `user_name` renders `Onbekend`, a missing `activity_name` renders
|
||||
`Onbekende handeling`, and a null `insole_type` omits the type pill.
|
||||
- Each card exposes `Stop` and `Annuleer` buttons that call `POST /api/admin/sessions/:id/stop` and
|
||||
`.../discard` for that session; both buttons are disabled while either mutation is in flight.
|
||||
- Loading state renders `Laden…`; the error state renders `Kon gegevens niet laden.` (and no header);
|
||||
the empty state renders `Niemand is nu aan het werk.`; a non-array payload is treated as zero active
|
||||
sessions (defensive `Array.isArray` fallback).
|
||||
- Lifecycle (`src/screens/Live.test.tsx`): active-sessions data renders a running card and a paused
|
||||
card side by side, with exactly the paused session showing the `Gepauzeerd` badge and its
|
||||
`Pauze 00:01:30` total.
|
||||
|
||||
Reference in New Issue
Block a user