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