import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'; import { listMyUploads, listMyUploadsOrEmpty } from './uploads'; function ok(body: unknown): Response { return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } }); } function envelope(status: number, code: string, message: string): Response { return new Response(JSON.stringify({ error: { code, message } }), { status, headers: { 'content-type': 'application/json' } }); } describe('uploads api client', () => { let fetchSpy: MockInstance; beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); }); afterEach(() => { vi.restoreAllMocks(); }); it('listMyUploads returns the discriminated union of entries', async () => { fetchSpy.mockResolvedValueOnce( ok({ items: [ { kind: 'manga', manga: { id: 'm1', title: 'A', status: 'ongoing', alt_titles: [], description: null, cover_image_path: null, created_at: '2026-05-17T12:00:00Z', updated_at: '2026-05-17T12:00:00Z' }, created_at: '2026-05-17T12:00:00Z' }, { kind: 'chapter', manga_id: 'm1', manga_title: 'A', manga_cover_image_path: null, chapter: { id: 'c1', manga_id: 'm1', number: 1, title: null, page_count: 3, created_at: '2026-05-17T13:00:00Z' }, created_at: '2026-05-17T13:00:00Z' } ], page: { limit: 50, offset: 0, total: 2 } }) ); const r = await listMyUploads(); expect(r.items[0].kind).toBe('manga'); expect(r.items[1].kind).toBe('chapter'); // Discriminant pattern-match (compile-time check via the union). if (r.items[1].kind === 'chapter') { expect(r.items[1].chapter.number).toBe(1); } }); it('listMyUploadsOrEmpty returns empty page on 401', async () => { fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'login required')); const r = await listMyUploadsOrEmpty(); expect(r.items).toEqual([]); }); });