76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
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<string, unknown>)[key],
|
|
]),
|
|
);
|
|
|
|
beforeEach(() => {
|
|
for (const key of ["window", "document", "location", "CustomEvent", "FormData", "fetch"]) {
|
|
delete (globalThis as Record<string, unknown>)[key];
|
|
}
|
|
});
|
|
afterEach(() => {
|
|
for (const [key, value] of originalGlobals) {
|
|
if (value === undefined) delete (globalThis as Record<string, unknown>)[key];
|
|
else (globalThis as Record<string, unknown>)[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 = `<form data-wrn-action="createUser"><input name="name" value="Ada"></form>`;
|
|
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<string, unknown>;
|
|
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;
|
|
}
|
|
});
|