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:
Bas van Rossem
2026-07-23 14:30:26 +02:00
parent 64911a0052
commit 77db308e41
2 changed files with 124 additions and 0 deletions

View File

@@ -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();
});
});