test(worker): History — fill gaps + lifecycle test

This commit is contained in:
Bas van Rossem
2026-07-23 14:03:04 +02:00
parent 36d4754722
commit 13cbc50c4f
3 changed files with 155 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { downloadExport } from './export';
import { setToken } from './auth-storage';
describe('downloadExport', () => {
beforeEach(() => {
localStorage.clear();
// jsdom does not implement the object-URL API; stub it so the download path runs.
URL.createObjectURL = vi.fn(() => 'blob:mock-url');
URL.revokeObjectURL = vi.fn();
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it('fetches the CSV with the bearer token and triggers an object-URL download', async () => {
setToken('tok');
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
const fetchMock = vi.fn().mockResolvedValue(new Response('a,b\n1,2\n', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
await downloadExport();
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe('http://localhost:3000/api/export');
expect(new Headers(init.headers).get('Authorization')).toBe('Bearer tok');
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
expect(clickSpy).toHaveBeenCalledTimes(1);
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:mock-url');
});
it('sends no Authorization header when there is no token', async () => {
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
const fetchMock = vi.fn().mockResolvedValue(new Response('x', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
await downloadExport();
const [, init] = fetchMock.mock.calls[0];
expect(new Headers(init.headers).has('Authorization')).toBe(false);
});
it('throws when the response is not ok', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 })));
await expect(downloadExport()).rejects.toThrow('Kan de export niet openen');
});
});

View File

@@ -120,4 +120,80 @@ describe('History', () => {
await user.click(screen.getByRole('button', { name: 'Exporteer CSV' }));
expect(mockedDownloadExport).toHaveBeenCalledTimes(1);
});
it('shows an error message when the export fails', async () => {
const user = userEvent.setup();
mockedDownloadExport.mockRejectedValueOnce(new Error('boom'));
renderHistory();
await user.click(screen.getByRole('button', { name: 'Exporteer CSV' }));
expect(await screen.findByText(/Kan de export niet openen/)).toBeInTheDocument();
});
it('formats a sub-minute duration as just seconds', async () => {
mockedApiFetch.mockResolvedValue([session({ duration_seconds: 45 })]);
renderHistory();
const card = (await screen.findByText('Frezen')).closest('.session-card') as HTMLElement;
expect(card.textContent).toContain('45s');
expect(card.textContent).not.toContain('m 45s');
});
it('renders 0s when duration_seconds is null', async () => {
mockedApiFetch.mockResolvedValue([session({ duration_seconds: null })]);
renderHistory();
const card = (await screen.findByText('Frezen')).closest('.session-card') as HTMLElement;
expect(card.textContent).toContain('0s');
});
it('falls back to "Onbekende handeling" when the activity name is null', async () => {
mockedApiFetch.mockResolvedValue([session({ activity_name: undefined })]);
renderHistory();
expect(await screen.findByText('Onbekende handeling')).toBeInTheDocument();
});
it('omits the type pill when insole_type is null', async () => {
mockedApiFetch.mockResolvedValue([session({ insole_type: null })]);
renderHistory();
const card = (await screen.findByText('Frezen')).closest('.session-card') as HTMLElement;
expect(card.textContent).not.toContain('Kurk');
});
it('renders sessions in the order the API returns them (newest-first)', async () => {
mockedApiFetch.mockResolvedValue([
session({ id: 2, activity_name: 'Frezen' }),
session({ id: 1, activity_name: 'Zagen' }),
]);
const { container } = renderHistory();
await screen.findByText('Frezen');
const names = [...container.querySelectorAll('.session-name')].map((el) => el.textContent);
expect(names).toEqual(['Frezen', 'Zagen']);
});
it('shows neither the empty state nor cards while loading', () => {
mockedApiFetch.mockReturnValue(new Promise<never>(() => {}));
const { container } = renderHistory();
expect(screen.queryByText('Nog geen opgeslagen sessies.')).not.toBeInTheDocument();
expect(container.querySelectorAll('.session-card')).toHaveLength(0);
});
it('lifecycle: renders a completed paused session, then export calls the download path', async () => {
const user = userEvent.setup();
mockedApiFetch.mockResolvedValue([
session({ status: 'completed', duration_seconds: 3661, paused_seconds: 125 }),
]);
renderHistory();
const card = (await screen.findByText('Frezen')).closest('.session-card') as HTMLElement;
expect(card.textContent).toContain('1h 1m');
expect(card.textContent).toContain('Pauze 2m 5s');
await user.click(screen.getByRole('button', { name: 'Exporteer CSV' }));
expect(mockedDownloadExport).toHaveBeenCalledTimes(1);
});
});