import { afterEach, beforeEach, expect, test } from "bun:test"; import { Window } from "happy-dom"; import { ACTION_RUNTIME } from "../src/action-runtime.ts"; import { createActionClient } from "../src/actions.ts"; const originalGlobals = new Map( ["window", "document", "location", "CustomEvent", "FormData", "fetch"].map((key) => [ key, (globalThis as Record)[key], ]), ); beforeEach(() => { for (const key of ["window", "document", "location", "CustomEvent", "FormData", "fetch"]) { delete (globalThis as Record)[key]; } }); afterEach(() => { for (const [key, value] of originalGlobals) { if (value === undefined) delete (globalThis as Record)[key]; else (globalThis as Record)[key] = value; } }); test("action runtime exposes pending, optimistic, success, and invalidation state", async () => { const win = new Window({ url: "https://example.test/users" }); win.document.cookie = "wrn-csrf=token"; win.document.body.innerHTML = `
`; const phases: string[] = []; ["optimistic", "pending", "success"].forEach((phase) => win.document.addEventListener(`wrnexus:action:${phase}`, () => phases.push(phase)), ); let invalidated: unknown; win.addEventListener("wrnexus:cache:invalidate", (event) => { invalidated = (event as unknown as CustomEvent).detail.tags; }); const g = globalThis as Record; Object.assign(g, { window: win, document: win.document, location: win.location, CustomEvent: win.CustomEvent, FormData: win.FormData, fetch: async () => Response.json({ data: { id: 1 }, invalidated: ["users"] }), }); (0, eval)(ACTION_RUNTIME); win.document .querySelector("form")! .dispatchEvent(new win.Event("submit", { bubbles: true, cancelable: true })); await new Promise((resolve) => setTimeout(resolve, 0)); expect(phases).toEqual(["optimistic", "pending", "success"]); expect(invalidated).toEqual(["users"]); expect(win.document.querySelector("form")?.getAttribute("data-wrn-action-state")).toBe("success"); }); test("typed action client returns invalidations and structured validation errors", async () => { const original = globalThis.fetch; try { globalThis.fetch = (async () => Response.json({ data: { id: 1 }, invalidated: ["users"] })) as unknown as typeof fetch; const call = createActionClient<{ name: string }, { id: number }>("/users", "createUser"); expect(await call({ name: "Ada" }, { csrfToken: "token" })).toEqual({ data: { id: 1 }, invalidated: ["users"], }); globalThis.fetch = (async () => Response.json({ errors: { name: "Required" } }, { status: 422 })) as unknown as typeof fetch; await expect(call({ name: "" })).rejects.toMatchObject({ status: 422, errors: { name: "Required" }, }); } finally { globalThis.fetch = original; } });