import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getReadingProgress, handleResponse, handleVoidResponse } from "./api"; // api.ts registers an Alpine component at import time; the toast import it // carries installs a fetch interceptor. Neither interferes with the stubbed // global fetch below, but both are exercised by importing the module. const jsonResponse = (body: unknown, status = 200): Response => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" }, }); describe("handleResponse", () => { it("parses ok JSON bodies", async () => { await expect(handleResponse(jsonResponse({ ok: true }))).resolves.toEqual({ ok: true }); }); it("throws the server's error message", async () => { await expect(handleResponse(jsonResponse({ error: "Nope" }, 404))).rejects.toThrow("Nope"); }); it("falls back to the HTTP status when the body has no message", async () => { await expect(handleResponse(jsonResponse({ error: "" }, 500))).rejects.toThrow("HTTP 500"); }); it("falls back to Unknown error when the error body is not JSON", async () => { await expect(handleResponse(new Response("502", { status: 502 }))).rejects.toThrow( "Unknown error", ); }); }); describe("handleVoidResponse", () => { it("resolves on ok regardless of body", async () => { await expect( handleVoidResponse(new Response("ignored", { status: 200 })), ).resolves.toBeUndefined(); }); it("throws the server's error message", async () => { await expect(handleVoidResponse(jsonResponse({ error: "Denied" }, 403))).rejects.toThrow( "Denied", ); }); }); describe("getReadingProgress", () => { beforeEach(() => { localStorage.clear(); }); afterEach(() => { vi.unstubAllGlobals(); }); it("returns null on 404 (no progress yet)", async () => { vi.stubGlobal( "fetch", vi.fn(async () => jsonResponse({ error: "none" }, 404)), ); await expect(getReadingProgress("m1")).resolves.toBeNull(); }); it("returns the progress payload otherwise", async () => { vi.stubGlobal( "fetch", vi.fn(async () => jsonResponse({ current_page: 3, total_pages: 10, last_read_at: "2026-01-01T00:00:00Z" }), ), ); const progress = await getReadingProgress("m1"); expect(progress).toMatchObject({ current_page: 3 }); }); it("sends the auth header when a token is stored", async () => { localStorage.setItem("token", "tok-9"); const fetchMock = vi.fn(async (_url: string, _init?: RequestInit) => jsonResponse({ error: "none" }, 404), ); vi.stubGlobal("fetch", fetchMock); await getReadingProgress("m1"); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock.mock.calls[0]?.[1]?.headers).toMatchObject({ Authorization: "Bearer tok-9" }); }); });