import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'; import { updateReadProgress, listMyReadProgress, listMyReadProgressOrEmpty, getMyReadProgressForManga, clearReadProgress } from './read_progress'; function ok(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } }); } function noContent(): Response { return new Response(null, { status: 204 }); } function envelope(status: number, code: string, message: string): Response { return new Response(JSON.stringify({ error: { code, message } }), { status, headers: { 'content-type': 'application/json' } }); } describe('read_progress api client', () => { let fetchSpy: MockInstance; beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); }); afterEach(() => { vi.restoreAllMocks(); }); it('updateReadProgress PUTs to /v1/me/read-progress', async () => { fetchSpy.mockResolvedValueOnce( ok({ user_id: 'u1', manga_id: 'm1', chapter_id: 'c1', page: 5, updated_at: '2026-05-17T12:00:00Z' }) ); const r = await updateReadProgress({ manga_id: 'm1', chapter_id: 'c1', page: 5 }); expect(r.page).toBe(5); const init = fetchSpy.mock.calls[0][1] as RequestInit; expect(init.method).toBe('PUT'); expect(JSON.parse(init.body as string)).toEqual({ manga_id: 'm1', chapter_id: 'c1', page: 5 }); }); it('listMyReadProgress returns the paged envelope', async () => { fetchSpy.mockResolvedValueOnce( ok({ items: [], page: { limit: 50, offset: 0, total: 0 } }) ); const r = await listMyReadProgress(); expect(r.items).toEqual([]); const url = fetchSpy.mock.calls[0][0] as string; expect(url).toMatch(/\/v1\/me\/read-progress$/); }); it('listMyReadProgressOrEmpty returns empty page on 401', async () => { fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'login required')); const r = await listMyReadProgressOrEmpty(); expect(r.items).toEqual([]); }); it('getMyReadProgressForManga returns null on 404 (not yet read)', async () => { fetchSpy.mockResolvedValueOnce(envelope(404, 'not_found', 'no progress')); const r = await getMyReadProgressForManga('m1'); expect(r).toBeNull(); }); it('getMyReadProgressForManga returns null on 401 (guest)', async () => { fetchSpy.mockResolvedValueOnce(envelope(401, 'unauthenticated', 'login')); const r = await getMyReadProgressForManga('m1'); expect(r).toBeNull(); }); it('getMyReadProgressForManga returns the row with chapter_number when present', async () => { fetchSpy.mockResolvedValueOnce( ok({ manga_id: 'm1', chapter_id: 'c1', chapter_number: 7, page: 3, updated_at: '2026-05-17T12:00:00Z' }) ); const r = await getMyReadProgressForManga('m1'); expect(r?.chapter_id).toBe('c1'); expect(r?.chapter_number).toBe(7); expect(r?.page).toBe(3); }); it('clearReadProgress DELETEs the resource', async () => { fetchSpy.mockResolvedValueOnce(noContent()); await clearReadProgress('m1'); const init = fetchSpy.mock.calls[0][1] as RequestInit; expect(init.method).toBe('DELETE'); const url = fetchSpy.mock.calls[0][0] as string; expect(url).toMatch(/\/v1\/me\/read-progress\/m1$/); }); });