test(admin): Reports + date-range — fill gaps + lifecycle test

This commit is contained in:
Bas van Rossem
2026-07-23 14:53:38 +02:00
parent e7eefe8f62
commit 5aa6febd65
4 changed files with 229 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } 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';
@@ -6,6 +6,7 @@ import type { ReportResponse } from '@solelog/shared';
import Reports from './Reports';
import { apiFetch } from '../lib/api';
import * as reportsApi from '../api/reports';
import { allTime, dayEndISO, dayStartISO, thisMonth, thisWeek } from '../lib/date-range';
vi.mock('../lib/api', () => ({ apiFetch: vi.fn(), API_URL: 'http://test' }));
const mockApiFetch = vi.mocked(apiFetch);
@@ -127,4 +128,139 @@ describe('Reports', () => {
expect(arg.from).toBeTruthy();
expect(arg.to).toBeTruthy();
});
it('shows a loading state while the report is pending', async () => {
mockApiFetch.mockImplementation((path?: string) => {
if (path?.startsWith('/api/admin/report')) return new Promise(() => {}) as never; // never resolves
return Promise.resolve([] as never);
});
renderReports();
expect(await screen.findByText(/Laden/)).toBeInTheDocument();
});
it('shows an error message when the report fails to load', async () => {
mockApiFetch.mockImplementation((path?: string) => {
if (path?.startsWith('/api/admin/report')) return Promise.reject(new Error('boom'));
return Promise.resolve([] as never);
});
renderReports();
expect(await screen.findByText('Kon rapport niet laden.')).toBeInTheDocument();
});
it('renders zero totals and empty tables when the report has no data', async () => {
// An empty object exercises every `?? 0` / `?? []` defensive fallback.
mockApiFetch.mockImplementation((path?: string) => {
if (path?.startsWith('/api/admin/report')) return Promise.resolve({} as never);
return Promise.resolve([] as never);
});
renderReports();
const empties = await screen.findAllByText('Geen gegevens.');
expect(empties).toHaveLength(3);
const totals = document.querySelector('.reports-totals') as HTMLElement;
expect(totals).toHaveTextContent('00:00:00');
expect(totals).toHaveTextContent('0 zolen');
});
it('shows an export error message when the download fails', async () => {
mockEndpoints();
vi.spyOn(reportsApi, 'downloadExport').mockRejectedValue(new Error('nope'));
renderReports();
await screen.findByRole('cell', { name: 'Jan' });
await userEvent.click(screen.getByRole('button', { name: 'Exporteer CSV' }));
expect(await screen.findByText('Export mislukt. Probeer opnieuw.')).toBeInTheDocument();
});
it('lists activity options and includes the chosen activity in the query', async () => {
mockApiFetch.mockImplementation((path?: string) => {
if (path?.startsWith('/api/admin/report')) return Promise.resolve(REPORT as never);
if (path === '/api/admin/users')
return Promise.resolve([{ id: 'u1', name: 'Jan', email: 'jan@x' }] as never);
if (path === '/api/activities')
return Promise.resolve([
{ id: 7, name: 'Frezen', insole_types: ['Kurk'], sort_order: 0 },
] as never);
return Promise.resolve([] as never);
});
renderReports();
await screen.findByRole('cell', { name: 'Jan' });
await userEvent.selectOptions(screen.getByLabelText('Handeling'), '7');
await waitFor(() => {
const reportCalls = mockApiFetch.mock.calls.filter((c) =>
String(c[0]).startsWith('/api/admin/report'),
);
expect(String(reportCalls[reportCalls.length - 1][0])).toContain('activity_id=7');
});
});
it('lifecycle: "Deze maand" + a worker filter both reflect in the query and rows render', async () => {
mockEndpoints();
renderReports();
await screen.findByRole('cell', { name: 'Jan' });
await userEvent.click(screen.getByRole('button', { name: 'Deze maand' }));
await userEvent.selectOptions(screen.getByLabelText('Medewerker'), 'u1');
const expectedFrom = dayStartISO(thisMonth(new Date()).from);
await waitFor(() => {
const reportCalls = mockApiFetch.mock.calls.filter((c) =>
String(c[0]).startsWith('/api/admin/report'),
);
const last = String(reportCalls[reportCalls.length - 1][0]);
const params = new URLSearchParams(last.split('?')[1]);
expect(params.get('user_id')).toBe('u1');
expect(params.get('from')).toBe(expectedFrom);
});
// Returned rows still render after the filters change.
expect(screen.getByRole('cell', { name: 'Jan' })).toBeInTheDocument();
expect(screen.getByRole('cell', { name: 'Frezen' })).toBeInTheDocument();
});
it('applies the week/all presets and the type filter to the query', async () => {
mockEndpoints();
renderReports();
await screen.findByRole('cell', { name: 'Jan' });
const lastReportParams = () => {
const calls = mockApiFetch.mock.calls.filter((c) =>
String(c[0]).startsWith('/api/admin/report'),
);
return new URLSearchParams(String(calls[calls.length - 1][0]).split('?')[1]);
};
await userEvent.click(screen.getByRole('button', { name: 'Alles' }));
await waitFor(() =>
expect(lastReportParams().get('from')).toBe(dayStartISO(allTime(new Date()).from)),
);
await userEvent.click(screen.getByRole('button', { name: 'Deze week' }));
await waitFor(() =>
expect(lastReportParams().get('from')).toBe(dayStartISO(thisWeek(new Date()).from)),
);
await userEvent.selectOptions(screen.getByLabelText('Type'), 'Berk');
await waitFor(() => expect(lastReportParams().get('insole_type')).toBe('Berk'));
});
it('updates the query when the Van/Tot dates change', async () => {
mockEndpoints();
renderReports();
await screen.findByRole('cell', { name: 'Jan' });
fireEvent.change(screen.getByLabelText('Van'), { target: { value: '2026-06-10' } });
fireEvent.change(screen.getByLabelText('Tot'), { target: { value: '2026-06-12' } });
await waitFor(() => {
const calls = mockApiFetch.mock.calls.filter((c) =>
String(c[0]).startsWith('/api/admin/report'),
);
const params = new URLSearchParams(String(calls[calls.length - 1][0]).split('?')[1]);
expect(params.get('from')).toBe(dayStartISO('2026-06-10'));
expect(params.get('to')).toBe(dayEndISO('2026-06-12'));
});
});
});