196 lines
6.2 KiB
TypeScript
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();
|
|
});
|
|
});
|