import { expect, test, beforeEach } from "bun:test"; import { Window } from "happy-dom"; import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts"; import { restoreGlobalsAfterAll } from "./global-restore.ts"; const REPLACED_GLOBALS = ["window", "document", "location", "fetch", "NodeFilter"]; restoreGlobalsAfterAll(REPLACED_GLOBALS); beforeEach(() => { for (const name of REPLACED_GLOBALS) delete (globalThis as Record)[name]; }); interface Call { url: string; init: RequestInit; } /** Mount the runtime with a recording fetch and return its callApi plus the calls made. */ function harness(response: { status: number; payload: unknown }) { const win = new Window() as unknown as Window & Record; win.document.body.innerHTML = `
`; const calls: Call[] = []; (globalThis as Record).window = win; (globalThis as Record).document = win.document; (globalThis as Record).location = win.location; (globalThis as Record).NodeFilter = ( win as unknown as { NodeFilter: unknown } ).NodeFilter; (globalThis as Record).fetch = (url: string, init: RequestInit) => { calls.push({ url, init }); return Promise.resolve({ ok: response.status >= 200 && response.status < 300, status: response.status, json: () => Promise.resolve(response.payload), }); }; (0, eval)(REACTIVE_RUNTIME); const callApi = (win as unknown as { __wrnexusCallApi: (...args: any[]) => Promise }) .__wrnexusCallApi; return { callApi, calls, win }; } test("GET builds a query string and omits undefined fields", async () => { const { callApi, calls } = harness({ status: 200, payload: { users: [] } }); await callApi("/api/users", "GET", { name: "Ajay", age: undefined }); expect(calls[0]!.url).toBe("/api/users?name=Ajay"); expect(calls[0]!.init.method).toBe("GET"); expect(calls[0]!.init.body).toBeUndefined(); }); test("POST sends a JSON body", async () => { const { callApi, calls } = harness({ status: 200, payload: { ok: true } }); await callApi("/api/users", "POST", { name: "Ajay" }); expect(calls[0]!.url).toBe("/api/users"); expect(calls[0]!.init.body).toBe(JSON.stringify({ name: "Ajay" })); expect((calls[0]!.init.headers as Record)["content-type"]).toBe( "application/json", ); }); test("a non-GET request carries the CSRF token from the cookie", async () => { const { callApi, calls, win } = harness({ status: 200, payload: {} }); win.document.cookie = "wrn-csrf=token-123"; await callApi("/api/users", "POST", {}); expect((calls[0]!.init.headers as Record)["x-csrf-token"]).toBe("token-123"); }); test("a 2xx resolves to the parsed payload", async () => { const { callApi } = harness({ status: 200, payload: { users: [{ name: "Ajay" }] } }); expect(await callApi("/api/users", "GET", {})).toEqual({ users: [{ name: "Ajay" }] }); }); test("a 204 with no body resolves to undefined instead of rejecting", async () => { const win = new Window() as unknown as Window & Record; win.document.body.innerHTML = `
`; (globalThis as Record).window = win; (globalThis as Record).document = win.document; (globalThis as Record).location = win.location; (globalThis as Record).NodeFilter = ( win as unknown as { NodeFilter: unknown } ).NodeFilter; (globalThis as Record).fetch = () => Promise.resolve({ ok: true, status: 204, json: () => Promise.reject(new SyntaxError("Unexpected end of JSON input")), }); (0, eval)(REACTIVE_RUNTIME); const callApi = (win as unknown as { __wrnexusCallApi: (...args: any[]) => Promise }) .__wrnexusCallApi; await expect(callApi("/api/users", "DELETE", {})).resolves.toBeUndefined(); }); test("a 2xx with an empty/unparseable body resolves to undefined", async () => { const { callApi } = harness({ status: 200, payload: undefined }); (globalThis as Record).fetch = () => Promise.resolve({ ok: true, status: 200, json: () => Promise.reject(new SyntaxError("Unexpected end of JSON input")), }); await expect(callApi("/api/users", "GET", {})).resolves.toBeUndefined(); }); test("a non-2xx rejects with status, message and data", async () => { const { callApi } = harness({ status: 400, payload: { error: "Bad filter" } }); const failure = await callApi("/api/users", "GET", {}).catch( (error: Error & { status?: number; data?: unknown }) => error, ); expect(failure.status).toBe(400); expect(failure.message).toContain("Bad filter"); expect(failure.data).toEqual({ error: "Bad filter" }); });