Files
solelog/apps/admin/src/screens/Sessions.test.tsx

196 lines
6.2 KiB
TypeScript

import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { WorkSession } from '@solelog/shared';
import Sessions from './Sessions';
import { apiFetch } from '../lib/api';
vi.mock('../lib/api', () => ({
apiFetch: vi.fn(),
}));
const mockApiFetch = vi.mocked(apiFetch);
function makeSession(over: Partial<WorkSession>): WorkSession {
return {
id: 1,
user_id: 'u1',
activity_id: 10,
activity_name: 'Frezen',
user_name: 'Jan',
insole_type: 'Kurk',
pair_count: 4,
start_time: new Date('2026-06-17T08:00:00Z').toISOString(),
end_time: new Date('2026-06-17T09:00:00Z').toISOString(),
duration_seconds: 3000,
paused_seconds: 600,
paused_at: null,
status: 'completed',
source: 'manual',
notes: null,
created_at: new Date('2026-06-17T09:00:00Z').toISOString(),
...over,
};
}
const SESSIONS: WorkSession[] = [
makeSession({
id: 1,
user_name: 'Jan',
activity_name: 'Frezen',
status: 'completed',
duration_seconds: 3000,
paused_seconds: 600,
end_time: new Date('2026-06-17T09:00:00Z').toISOString(),
}),
makeSession({
id: 2,
user_name: 'Piet',
activity_name: 'Lijmen',
status: 'active',
duration_seconds: null,
paused_seconds: 0,
end_time: null,
start_time: new Date(Date.now() - 120_000).toISOString(),
}),
makeSession({
id: 3,
user_name: 'Klaas',
activity_name: 'Snijden',
status: 'discarded',
duration_seconds: null,
paused_seconds: 0,
end_time: new Date('2026-06-17T07:00:00Z').toISOString(),
}),
];
// Route any non-sessions call (users roster, activities) to an empty list.
function mockEndpoints(sessions: WorkSession[]) {
mockApiFetch.mockImplementation((path: string) => {
if (path === '/api/admin/sessions') return Promise.resolve(sessions as never);
return Promise.resolve([] as never);
});
}
function renderSessions() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
<QueryClientProvider client={queryClient}>
<Sessions />
</QueryClientProvider>,
);
}
describe('Sessions', () => {
beforeEach(() => {
mockApiFetch.mockReset();
});
afterEach(() => {
vi.clearAllMocks();
});
it('renders a row per session with worker, activity and worked time', async () => {
mockEndpoints(SESSIONS);
renderSessions();
expect(await screen.findByText('Sessies')).toBeInTheDocument();
expect(await screen.findByText('Jan')).toBeInTheDocument();
expect(screen.getByText('Frezen')).toBeInTheDocument();
expect(screen.getByText('Piet')).toBeInTheDocument();
expect(screen.getByText('Klaas')).toBeInTheDocument();
// worked for the completed manual session = 3000s = 00:50:00
expect(screen.getByText('00:50:00')).toBeInTheDocument();
// pause shown when paused_seconds > 0 (600s = 00:10:00)
expect(screen.getByText(/Pauze 00:10:00/)).toBeInTheDocument();
});
it('narrows the list when the status filter changes', async () => {
mockEndpoints(SESSIONS);
renderSessions();
expect(await screen.findByText('Jan')).toBeInTheDocument();
await userEvent.selectOptions(screen.getByLabelText('Status'), 'active');
expect(screen.getByText('Piet')).toBeInTheDocument();
expect(screen.queryByText('Jan')).not.toBeInTheDocument();
expect(screen.queryByText('Klaas')).not.toBeInTheDocument();
});
it('shows Stop/Annuleer only on active rows and ✎ on all rows', async () => {
mockEndpoints(SESSIONS);
renderSessions();
expect(await screen.findByText('Jan')).toBeInTheDocument();
// ✎ edit on every row.
expect(screen.getAllByRole('button', { name: /Bewerk/ })).toHaveLength(3);
// Stop/Annuleer only on the single active row.
expect(screen.getAllByRole('button', { name: 'Stop' })).toHaveLength(1);
expect(screen.getAllByRole('button', { name: 'Annuleer' })).toHaveLength(1);
});
it('Stop on an active row calls the stop endpoint', async () => {
mockEndpoints(SESSIONS);
renderSessions();
expect(await screen.findByText('Piet')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Stop' }));
await waitFor(() =>
expect(mockApiFetch).toHaveBeenCalledWith(
'/api/admin/sessions/2/stop',
expect.objectContaining({ method: 'POST' }),
),
);
});
it('Annuleer on an active row calls the discard endpoint', async () => {
mockEndpoints(SESSIONS);
renderSessions();
expect(await screen.findByText('Piet')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Annuleer' }));
await waitFor(() =>
expect(mockApiFetch).toHaveBeenCalledWith(
'/api/admin/sessions/2/discard',
expect.objectContaining({ method: 'POST' }),
),
);
});
it('+ Nieuwe registratie opens the create form (worker picker visible)', async () => {
mockEndpoints(SESSIONS);
renderSessions();
expect(await screen.findByText('Jan')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: '+ Nieuwe registratie' }));
// Task 4 fills the form; for now we assert the create panel/region opens.
expect(await screen.findByTestId('session-form')).toBeInTheDocument();
});
it('shows the empty state when there are no sessions', async () => {
mockEndpoints([]);
renderSessions();
expect(await screen.findByText('Nog geen sessies.')).toBeInTheDocument();
});
it('keeps the discarded row hidden when filtering to voltooid', async () => {
mockEndpoints(SESSIONS);
renderSessions();
expect(await screen.findByText('Jan')).toBeInTheDocument();
await userEvent.selectOptions(screen.getByLabelText('Status'), 'completed');
const list = screen.getByRole('list', { name: 'Sessies' });
expect(within(list).getByText('Jan')).toBeInTheDocument();
expect(within(list).queryByText('Piet')).not.toBeInTheDocument();
expect(within(list).queryByText('Klaas')).not.toBeInTheDocument();
});
});