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,