feat(admin): sessions management screen (list + filter + actions)
This commit is contained in:
195
apps/admin/src/screens/Sessions.test.tsx
Normal file
195
apps/admin/src/screens/Sessions.test.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user