import { afterEach, describe, expect, it, vi } from "vitest";import { z } from "zod";import { ApiFetchError, fetchJson } from "./api-client";import { createRuntimeServices } from "./runtime-services"; describe("api client", () => { afterEach(() => { vi.unstubAllGlobals(); }); it("preserves ApiFetchError and status across the Promise boundary", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response(JSON.stringify({ message: "Rate limited" }), { status: 429, }), ), ); await expect( fetchJson( "/api/test", undefined, z.object({ ok: z.boolean() }), "Failed", ), ).rejects.toMatchObject({ _tag: "ApiFetchError", message: "Rate limited", status: 429, }); await expect( fetchJson( "/api/test", undefined, z.object({ ok: z.boolean() }), "Failed", ), ).rejects.toBeInstanceOf(ApiFetchError); }); it("preserves AbortError so route hooks can ignore stale requests", async () => { const abortError = new DOMException( "The operation was aborted.", "AbortError", ); vi.stubGlobal( "fetch", vi.fn(async () => { throw abortError; }), ); await expect( fetchJson( "/api/test", undefined, z.object({ ok: z.boolean() }), "Failed", ), ).rejects.toBe(abortError); }); it("accepts an injected fetch service", async () => { const fetch = vi.fn(async () => Response.json({ ok: true })); await expect( fetchJson( "/api/test", undefined, z.object({ ok: z.boolean() }), "Failed", createRuntimeServices({ fetch }), ), ).resolves.toEqual({ ok: true }); expect(fetch).toHaveBeenCalledWith("/api/test"); }); it("rejects malformed successful responses at the shared boundary", async () => { const fetch = vi.fn(async () => Response.json({ ok: "yes" })); await expect( fetchJson( "/api/test", undefined, z.object({ ok: z.boolean() }), "Malformed response", createRuntimeServices({ fetch }), ), ).rejects.toMatchObject({ _tag: "ApiFetchError", message: "Malformed response", }); });});
import { afterEach, describe, expect, it, vi } from "vitest";
import { z } from "zod";
import { ApiFetchError, fetchJson } from "./api-client";
import { createRuntimeServices } from "./runtime-services";
describe("api client", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("preserves ApiFetchError and status across the Promise boundary", async () => {
vi.stubGlobal(
"fetch",
vi.fn(
async () =>
new Response(JSON.stringify({ message: "Rate limited" }), {
status: 429,
}),
),
);
await expect(
fetchJson(
"/api/test",
undefined,
z.object({ ok: z.boolean() }),
"Failed",
),
).rejects.toMatchObject({
_tag: "ApiFetchError",
message: "Rate limited",
status: 429,
});
await expect(
fetchJson(
"/api/test",
undefined,
z.object({ ok: z.boolean() }),
"Failed",
),
).rejects.toBeInstanceOf(ApiFetchError);
});
it("preserves AbortError so route hooks can ignore stale requests", async () => {
const abortError = new DOMException(
"The operation was aborted.",
"AbortError",
);
vi.stubGlobal(
"fetch",
vi.fn(async () => {
throw abortError;
}),
);
await expect(
fetchJson(
"/api/test",
undefined,
z.object({ ok: z.boolean() }),
"Failed",
),
).rejects.toBe(abortError);
});
it("accepts an injected fetch service", async () => {
const fetch = vi.fn(async () => Response.json({ ok: true }));
await expect(
fetchJson(
"/api/test",
undefined,
z.object({ ok: z.boolean() }),
"Failed",
createRuntimeServices({ fetch }),
),
).resolves.toEqual({ ok: true });
expect(fetch).toHaveBeenCalledWith("/api/test");
});
it("rejects malformed successful responses at the shared boundary", async () => {
const fetch = vi.fn(async () => Response.json({ ok: "yes" }));
await expect(
fetchJson(
"/api/test",
undefined,
z.object({ ok: z.boolean() }),
"Malformed response",
createRuntimeServices({ fetch }),
),
).rejects.toMatchObject({
_tag: "ApiFetchError",
message: "Malformed response",
});
});
});