// @vitest-environment jsdom // src/lib/offline/queue.test.ts import { describe, it, expect, beforeEach } from "vitest"; import "fake-indexeddb/auto"; import { enqueueAction, dequeueAction, getQueuedActions, markSynced, markConflict, markInFlight, markFailed, } from "./queue"; beforeEach(async () => { // Clear the queue between tests const { getOfflineDB } = await import("./db"); const db = await getOfflineDB(); await db.clear("queue"); }); describe("offline queue", () => { it("enqueueAction stores an action with a generated clientActionId", async () => { const action = await enqueueAction("markOrderReady", { orderId: "abc" }); expect(action.id).toMatch(/^[0-9a-f-]{36}$/i); expect(action.actionName).toBe("markOrderReady"); expect(action.status).toBe("pending"); expect(action.attempts).toBe(0); }); it("enqueueAction is idempotent on the same clientActionId", async () => { const a = await enqueueAction("markOrderReady", { orderId: "abc" }, { id: "fixed-id" }); const b = await enqueueAction("markOrderReady", { orderId: "abc" }, { id: "fixed-id" }); expect(a.id).toBe(b.id); expect(a.id).toBe("fixed-id"); const all = await getQueuedActions(); expect(all).toHaveLength(1); }); it("getQueuedActions returns pending actions in createdAt order", async () => { const a = await enqueueAction("a", {}, { id: "a" }); // Small delay to ensure ordering await new Promise((r) => setTimeout(r, 5)); const b = await enqueueAction("b", {}, { id: "b" }); const all = await getQueuedActions(); expect(all.map((x) => x.id)).toEqual([a.id, b.id]); }); it("dequeueAction removes the action and returns its data", async () => { const a = await enqueueAction("markOrderReady", { orderId: "abc" }); const removed = await dequeueAction(a.id); expect(removed?.id).toBe(a.id); const all = await getQueuedActions(); expect(all).toHaveLength(0); }); it("markSynced removes the action", async () => { const a = await enqueueAction("markOrderReady", { orderId: "abc" }); await markSynced(a.id); const all = await getQueuedActions(); expect(all).toHaveLength(0); }); it("markConflict moves the action to conflict status", async () => { const a = await enqueueAction("markOrderReady", { orderId: "abc" }); await markConflict(a.id, "stale updated_at"); const all = await getQueuedActions(); expect(all).toHaveLength(1); expect(all[0].status).toBe("conflict"); expect(all[0].error).toBe("stale updated_at"); }); it("markInFlight transitions the action to in-flight and records an attempt", async () => { const a = await enqueueAction("markOrderReady", { orderId: "abc" }); const before = await getQueuedActions(); expect(before[0].attempts).toBe(0); expect(before[0].status).toBe("pending"); expect(before[0].lastAttemptAt).toBeNull(); const beforeMs = Date.now(); await markInFlight(a.id); const afterMs = Date.now(); const all = await getQueuedActions(); expect(all).toHaveLength(1); expect(all[0].status).toBe("in-flight"); expect(all[0].attempts).toBe(1); expect(typeof all[0].lastAttemptAt).toBe("number"); expect(all[0].lastAttemptAt).not.toBeNull(); expect(all[0].lastAttemptAt!).toBeGreaterThanOrEqual(beforeMs); expect(all[0].lastAttemptAt!).toBeLessThanOrEqual(afterMs); }); it("markFailed transitions the action to failed and increments attempts", async () => { const a = await enqueueAction("markOrderReady", { orderId: "abc" }); // Pretend it has already been tried once await markInFlight(a.id); const beforeMs = Date.now(); await markFailed(a.id, "server returned 500"); const afterMs = Date.now(); const all = await getQueuedActions(); expect(all).toHaveLength(1); expect(all[0].status).toBe("failed"); expect(all[0].attempts).toBe(2); // markInFlight set it to 1, markFailed adds 1 expect(all[0].error).toBe("server returned 500"); expect(typeof all[0].lastAttemptAt).toBe("number"); expect(all[0].lastAttemptAt).not.toBeNull(); expect(all[0].lastAttemptAt!).toBeGreaterThanOrEqual(beforeMs); expect(all[0].lastAttemptAt!).toBeLessThanOrEqual(afterMs); }); });