import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { listMangas, createManga, getManga } from './mangas'; function ok(body: unknown): Response { return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } }); } function fail(status: number, body = ''): Response { return new Response(body, { status }); } describe('mangas api client', () => { let fetchSpy: ReturnType; beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); }); afterEach(() => { vi.restoreAllMocks(); }); it('listMangas hits /mangas with no params by default', async () => { fetchSpy.mockResolvedValueOnce(ok([])); await listMangas(); expect(fetchSpy).toHaveBeenCalledTimes(1); const url = fetchSpy.mock.calls[0][0] as string; expect(url).toMatch(/\/mangas$/); }); it('listMangas encodes search, limit, offset', async () => { fetchSpy.mockResolvedValueOnce(ok([])); await listMangas({ search: 'one piece', limit: 10, offset: 20 }); const url = fetchSpy.mock.calls[0][0] as string; expect(url).toContain('search=one+piece'); expect(url).toContain('limit=10'); expect(url).toContain('offset=20'); }); it('createManga POSTs JSON', async () => { fetchSpy.mockResolvedValueOnce( ok({ id: 'abc', title: 'Berserk', author: 'Miura', description: null, cover_image_path: null, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z' }) ); const m = await createManga({ title: 'Berserk', author: 'Miura' }); expect(m.title).toBe('Berserk'); const init = fetchSpy.mock.calls[0][1] as RequestInit; expect(init.method).toBe('POST'); expect(init.headers).toMatchObject({ 'content-type': 'application/json' }); expect(JSON.parse(init.body as string)).toEqual({ title: 'Berserk', author: 'Miura' }); }); it('getManga throws ApiError on non-2xx', async () => { fetchSpy.mockResolvedValue(fail(404, 'not found')); await expect(getManga('missing')).rejects.toMatchObject({ name: 'ApiError', status: 404 }); }); });