feat(task): undo toast for completing — mis-taps are one tap to take back
completeTask returns an undo closure that restores status + sort slot and trashes the occurrence a repeat spawned. One global snackbar (5s, replace-on-new) mounted in the root layout; every checkbox and Clarify's save-as-done go through completeWithUndo. bun tests for the store. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YE5FmDZmFnWxH6RXCZLcjj
This commit is contained in:
63
app/src/lib/store.test.ts
Normal file
63
app/src/lib/store.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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<string, string>();
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user