From 66281c992915f95c5567b335cbbefeb575b110f0 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 12 Jun 2026 11:46:24 +0200 Subject: [PATCH] test(geschichten): add failing tests for draft fetch in page loader RED: loader does not yet call parent() or fetch DRAFT stories. Also extracts settled() helper to $lib/shared/server/settled.ts and seeds makeData/callLoad factories with drafts/parent defaults. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/shared/server/settled.ts | 5 ++ frontend/src/routes/+page.server.ts | 7 +- .../routes/geschichten/page.server.test.ts | 68 ++++++++++++++++++- .../routes/geschichten/page.svelte.spec.ts | 1 + 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 frontend/src/lib/shared/server/settled.ts diff --git a/frontend/src/lib/shared/server/settled.ts b/frontend/src/lib/shared/server/settled.ts new file mode 100644 index 00000000..2fe321b8 --- /dev/null +++ b/frontend/src/lib/shared/server/settled.ts @@ -0,0 +1,5 @@ +export function settled(res: PromiseSettledResult | undefined): T | null { + if (res?.status !== 'fulfilled') return null; + const v = res.value as { response: Response; data: unknown }; + return v.response.ok ? ((v.data as T) ?? null) : null; +} diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index f1aea7d8..efcbeb18 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -1,6 +1,7 @@ import { redirect } from '@sveltejs/kit'; import { createApiClient } from '$lib/shared/api.server'; import type { components } from '$lib/generated/api'; +import { settled } from '$lib/shared/server/settled'; type StatsDTO = components['schemas']['StatsDTO']; type TranscriptionQueueItemDTO = components['schemas']['TranscriptionQueueItemDTO']; @@ -14,12 +15,6 @@ type DocumentListItem = components['schemas']['DocumentListItem']; type GeschichteSummary = components['schemas']['GeschichteSummary']; type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; -function settled(res: PromiseSettledResult | undefined): T | null { - if (res?.status !== 'fulfilled') return null; - const v = res.value as { response: Response; data: unknown }; - return v.response.ok ? ((v.data as T) ?? null) : null; -} - export async function load({ fetch, parent }) { const { canWrite, canAnnotate, canBlogWrite } = await parent(); // READ_ALL without WRITE_ALL or ANNOTATE_ALL — see ADR-007. diff --git a/frontend/src/routes/geschichten/page.server.test.ts b/frontend/src/routes/geschichten/page.server.test.ts index c9ce8b94..fd532424 100644 --- a/frontend/src/routes/geschichten/page.server.test.ts +++ b/frontend/src/routes/geschichten/page.server.test.ts @@ -24,11 +24,18 @@ function makeUrl(params: Record = {}) { return url; } -function callLoad(url: URL) { +function callLoad(url: URL, parentData: { canBlogWrite?: boolean } = {}) { return load({ url, request: new Request('http://localhost/geschichten'), - fetch: vi.fn() as unknown as typeof fetch + fetch: vi.fn() as unknown as typeof fetch, + parent: vi.fn().mockResolvedValue({ + canBlogWrite: false, + canWrite: false, + canAnnotate: false, + user: null, + ...parentData + }) }); } @@ -191,3 +198,60 @@ describe('geschichten page load — documentFilter title resolution', () => { ); }); }); + +// ─── draft fetch ────────────────────────────────────────────────────────────── + +describe('geschichten page load — draft fetch', () => { + it('makes a second API call for DRAFT stories when canBlogWrite is true', async () => { + const mockGet = mockApi(); + + await callLoad(makeUrl(), { canBlogWrite: true }); + + const draftCall = mockGet.mock.calls.find( + ([, opts]: [string, { params?: { query?: { status?: string } } }]) => + opts?.params?.query?.status === 'DRAFT' + ); + expect(draftCall).toBeDefined(); + }); + + it('does NOT make a DRAFT API call when canBlogWrite is false', async () => { + const mockGet = mockApi(); + + await callLoad(makeUrl(), { canBlogWrite: false }); + + const draftCall = mockGet.mock.calls.find( + ([, opts]: [string, { params?: { query?: { status?: string } } }]) => + opts?.params?.query?.status === 'DRAFT' + ); + expect(draftCall).toBeUndefined(); + }); + + it('returns empty drafts array when the DRAFT fetch rejects', async () => { + const mockGet = vi + .fn() + .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) + .mockRejectedValueOnce(new Error('network error')); + vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< + typeof createApiClient + >); + + const result = await callLoad(makeUrl(), { canBlogWrite: true }); + + expect(result.drafts).toEqual([]); + }); + + it('returns drafts in page data when canBlogWrite is true', async () => { + const draft = { id: 'draft-1', title: 'My Draft', status: 'DRAFT' }; + const mockGet = vi + .fn() + .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [draft] }); + vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< + typeof createApiClient + >); + + const result = await callLoad(makeUrl(), { canBlogWrite: true }); + + expect(result.drafts).toEqual([draft]); + }); +}); diff --git a/frontend/src/routes/geschichten/page.svelte.spec.ts b/frontend/src/routes/geschichten/page.svelte.spec.ts index 410b1678..a5caab98 100644 --- a/frontend/src/routes/geschichten/page.svelte.spec.ts +++ b/frontend/src/routes/geschichten/page.svelte.spec.ts @@ -26,6 +26,7 @@ function person(id: string, displayName: string) { function makeData(overrides: Partial = {}): PageData { return { geschichten: [], + drafts: [], personFilters: [], documentFilter: null, canBlogWrite: false,