// Store unit tests — run with `bun test src/lib`. Expo/native modules are // mocked so the local-first mutations run in plain Bun. import { beforeEach, describe, expect, mock, test } from "bun:test"; const storage = new Map(); mock.module("@react-native-async-storage/async-storage", () => ({ default: { getItem: async (k: string) => storage.get(k) ?? null, setItem: async (k: string, v: string) => void storage.set(k, v), }, })); let seq = 0; mock.module("expo-crypto", () => ({ randomUUID: () => `00000000-0000-4000-8000-${String(++seq).padStart(12, "0")}`, })); mock.module("./api", () => ({ api: {} })); mock.module("./config", () => ({ getConfig: async () => ({ token: "" }) })); const store = await import("./store"); beforeEach(async () => { storage.clear(); await store.hydrate(); store.setArea("personal"); }); const find = (id: string) => store.getState().tasks.find((t) => t.id === id)!; describe("completeTask", () => { test("marks done and undo restores the original status in place", () => { const a = store.createTask({ title: "a", status: "waiting", sort_order: 5 }); const undo = store.completeTask(a.id); expect(find(a.id).status).toBe("done"); expect(find(a.id).completed_at).not.toBeNull(); undo!(); expect(find(a.id).status).toBe("waiting"); expect(find(a.id).completed_at).toBeNull(); expect(find(a.id).sort_order).toBe(5); }); test("revertTo overrides the status undo returns to", () => { const a = store.createTask({ title: "a", status: "next" }); store.completeTask(a.id, "someday")!(); expect(find(a.id).status).toBe("someday"); }); test("undo of a repeating task trashes the spawned occurrence", () => { const a = store.createTask({ title: "rent", repeat: "monthly", due_date: "2026-09-01" }); const known = new Set(store.getState().tasks.map((t) => t.id)); const undo = store.completeTask(a.id)!; const spawned = store.getState().tasks.filter((t) => !known.has(t.id)); expect(spawned).toHaveLength(1); expect(spawned[0].status).toBe("next"); undo(); expect(find(a.id).status).toBe("next"); expect(find(spawned[0].id).status).toBe("trashed"); }); test("returns null for an already-done or unknown task", () => { const a = store.createTask({ title: "a", status: "done" }); expect(store.completeTask(a.id)).toBeNull(); expect(store.completeTask("nope")).toBeNull(); }); }); describe("area lens", () => { test("new rows are born into the active area and selectors filter by it", () => { const home = store.createTask({ title: "home" }); store.setArea("work"); const office = store.createTask({ title: "office" }); const proj = store.createProject({ name: "Q4" }); expect(home.area).toBe("personal"); expect(office.area).toBe("work"); expect(proj.area).toBe("work"); // The store is module state shared across tests, so assert on membership // and on every visible row's area rather than on exact lists. const ids = (s: ReturnType) => store.tasksByStatus(s, "next").map((t) => t.id); let s = store.getState(); expect(ids(s)).toContain(office.id); expect(ids(s)).not.toContain(home.id); expect(store.tasksByStatus(s, "next").every((t) => t.area === "work")).toBe(true); expect(store.activeProjects(s).map((p) => p.id)).toEqual([proj.id]); store.setArea("personal"); s = store.getState(); expect(ids(s)).toContain(home.id); expect(ids(s)).not.toContain(office.id); expect(store.activeProjects(s).some((p) => p.id === proj.id)).toBe(false); }); test("area persists across hydrate", async () => { store.setArea("work"); expect(storage.get("area_v1")).toBe("work"); }); test("rows cached without an area count as personal", () => { // Simulate a pre-migration cache row. const legacy = store.createTask({ title: "old" }); delete (legacy as { area?: string }).area; store.setArea("work"); expect(store.todayTasks(store.getState()).overdue).toEqual([]); store.setArea("personal"); expect(store.tasksByStatus(store.getState(), "next").some((t) => t.id === legacy.id)).toBe(true); }); test("today and contexts respect the lens", () => { store.createTask({ title: "p", due_date: "2000-01-01", context: "@home" }); store.setArea("work"); store.createTask({ title: "w", due_date: "2000-01-01", context: "@office" }); const s = store.getState(); expect(store.todayTasks(s).overdue.every((t) => t.area === "work")).toBe(true); expect(store.todayTasks(s).overdue.map((t) => t.title)).toContain("w"); expect(store.todayTasks(s).overdue.map((t) => t.title)).not.toContain("p"); expect(store.knownContexts(s)).toEqual(["@office"]); }); test("completing a repeating work task spawns into work", () => { store.setArea("work"); const t = store.createTask({ title: "standup", repeat: "daily", due_date: "2000-01-01" }); store.setArea("personal"); store.completeTask(t.id); const spawned = store.getState().tasks.find((x) => x.title === "standup" && x.status === "next")!; expect(spawned.area).toBe("work"); }); });