feat(worker): Geschiedenis screen with session list and CSV export

This commit is contained in:
Bas van Rossem
2026-06-17 16:30:34 +02:00
parent 5af5a9c2bb
commit 134e01a2e8
5 changed files with 302 additions and 1 deletions

View File

@@ -0,0 +1,104 @@
import { render, screen } 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 History from './History';
import { apiFetch } from '../lib/api';
import { downloadExport } from '../lib/export';
// Mock the network layer so no real requests are made.
vi.mock('../lib/api', () => ({
apiFetch: vi.fn(),
}));
// Mock the authenticated CSV download helper.
vi.mock('../lib/export', () => ({
downloadExport: vi.fn(),
}));
const mockedApiFetch = vi.mocked(apiFetch);
const mockedDownloadExport = vi.mocked(downloadExport);
function session(overrides: Partial<WorkSession> = {}): WorkSession {
return {
id: 1,
user_id: 'u1',
activity_id: 1,
activity_name: 'Frezen',
insole_type: 'Kurk',
pair_count: 2,
start_time: '2026-01-02T08:30:00.000Z',
end_time: '2026-01-02T09:31:01.000Z',
duration_seconds: 3661,
status: 'completed',
source: 'app',
notes: null,
created_at: '2026-01-02T08:30:00.000Z',
...overrides,
};
}
function renderHistory() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
<QueryClientProvider client={queryClient}>
<History />
</QueryClientProvider>,
);
}
describe('History', () => {
beforeEach(() => {
mockedApiFetch.mockReset();
mockedDownloadExport.mockReset();
mockedApiFetch.mockResolvedValue([]);
mockedDownloadExport.mockResolvedValue(undefined);
});
afterEach(() => {
vi.clearAllMocks();
});
it('renders the header and export button in Dutch', () => {
renderHistory();
expect(screen.getByText('Geschiedenis')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Exporteer CSV' })).toBeInTheDocument();
});
it('shows the empty state when there are no sessions', async () => {
mockedApiFetch.mockResolvedValue([]);
renderHistory();
expect(await screen.findByText('Nog geen opgeslagen sessies.')).toBeInTheDocument();
});
it('renders a session card with activity name, type, count and duration', async () => {
mockedApiFetch.mockResolvedValue([session({ duration_seconds: 3661 })]);
renderHistory();
expect(await screen.findByText('Frezen')).toBeInTheDocument();
const card = screen.getByText('Frezen').closest('.session-card') as HTMLElement;
expect(card).not.toBeNull();
expect(card.textContent).toContain('Kurk');
expect(card.textContent).toContain('2 inlegzolen');
// 3661s -> "1h 1m" (hours present).
expect(card.textContent).toContain('1h 1m');
});
it('uses the singular noun for a count of 1', async () => {
mockedApiFetch.mockResolvedValue([session({ pair_count: 1 })]);
renderHistory();
const card = (await screen.findByText('Frezen')).closest('.session-card') as HTMLElement;
expect(card.textContent).toContain('1 inlegzool');
expect(card.textContent).not.toContain('1 inlegzolen');
});
it('triggers the CSV download on Exporteer CSV', async () => {
const user = userEvent.setup();
renderHistory();
await user.click(screen.getByRole('button', { name: 'Exporteer CSV' }));
expect(mockedDownloadExport).toHaveBeenCalledTimes(1);
});
});