114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
// @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);
|
|
});
|
|
});
|