Files
familienarchiv/frontend/src/routes/zeitstrahl/page.server.test.ts
Marcel 6f32299255 feat(timeline): add /zeitstrahl route, SSR load, and nav link
SSR-first load fetches GET /api/timeline via createApiClient (auth cookie
forwarded), no query params for the global view (REQ-001), returns { timeline }
with no client-side fetch (REQ-002); 401 -> /login, any other non-ok ->
error(status, getErrorMessage(...)), never raw JSON, no PII logged (REQ-022).
The page renders <TimelineView> under the layout's <main>. Adds the Zeitstrahl
nav link (desktop + mobile) and 'timeline' to the eslint routes boundary
allow-list so the route may import the domain.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 19:50:28 +02:00

73 lines
2.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$lib/shared/api.server', () => ({
createApiClient: vi.fn(),
extractErrorCode: (e: unknown) => (e as { code?: string } | undefined)?.code
}));
import { load } from './+page.server';
import { createApiClient } from '$lib/shared/api.server';
import { getErrorMessage } from '$lib/shared/errors';
beforeEach(() => vi.clearAllMocks());
const TIMELINE = { years: [{ year: 1914, entries: [] }], undated: [] };
function mockApi(opts: { ok?: boolean; status?: number; data?: unknown; error?: unknown }) {
const { ok = true, status = 200, data = TIMELINE, error } = opts;
const GET = vi.fn().mockResolvedValue({
response: { ok, status },
data: ok ? data : undefined,
error
});
vi.mocked(createApiClient).mockReturnValue({ GET } as unknown as ReturnType<
typeof createApiClient
>);
return GET;
}
function callLoad() {
return load({
fetch: vi.fn() as unknown as typeof fetch,
url: new URL('http://localhost/zeitstrahl'),
request: new Request('http://localhost/zeitstrahl'),
route: { id: '/zeitstrahl' },
params: {}
} as unknown as Parameters<typeof load>[0]);
}
describe('zeitstrahl +page.server load', () => {
it('fetches GET /api/timeline and returns { timeline } on ok (REQ-001/002)', async () => {
const GET = mockApi({ data: TIMELINE });
const result = await callLoad();
expect(GET).toHaveBeenCalledWith('/api/timeline');
expect(result).toEqual({ timeline: TIMELINE });
});
it('redirects to /login on 401 (REQ-022)', async () => {
mockApi({ ok: false, status: 401 });
await expect(callLoad()).rejects.toMatchObject({ status: 302, location: '/login' });
});
it('throws a mapped error on 404 (REQ-022)', async () => {
mockApi({ ok: false, status: 404, error: { code: 'TIMELINE_EVENT_NOT_FOUND' } });
await expect(callLoad()).rejects.toMatchObject({
status: 404,
body: { message: getErrorMessage('TIMELINE_EVENT_NOT_FOUND') }
});
});
it('throws a mapped error on 500 (REQ-022)', async () => {
mockApi({ ok: false, status: 500, error: undefined });
await expect(callLoad()).rejects.toMatchObject({ status: 500 });
});
it('throws a mapped FORBIDDEN error on 403 (REQ-022)', async () => {
mockApi({ ok: false, status: 403, error: { code: 'FORBIDDEN' } });
await expect(callLoad()).rejects.toMatchObject({
status: 403,
body: { message: getErrorMessage('FORBIDDEN') }
});
});
});