Adds real-time updates when lore syncs new GitLab data by watching the lore.db file for changes. React hook (src/hooks/useTauriEvents.ts): - useTauriEvent(): Subscribe to a single Tauri event with auto-cleanup - useTauriEvents(): Subscribe to multiple events with a handler map - Typed payloads for each event type: - global-shortcut-triggered: toggle-window | quick-capture - lore-data-changed: void (refresh trigger) - sync-status: started | completed | failed - error-notification: code + message Rust watcher (src-tauri/src/watcher.rs): - Watches lore's data directory for lore.db modifications - Uses notify crate with 2-second poll interval - Emits "lore-data-changed" event to frontend on file change - Handles atomic writes by watching parent directory - Gracefully handles missing lore.db (logs warning, skips watcher) Test coverage: - Hook subscription and cleanup behavior - Focus store test fix: clear localStorage before each test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
225 lines
6.7 KiB
TypeScript
225 lines
6.7 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { useFocusStore } from "@/stores/focus-store";
|
|
import { makeFocusItem } from "../helpers/fixtures";
|
|
|
|
describe("useFocusStore", () => {
|
|
beforeEach(() => {
|
|
// Clear persisted state before each test
|
|
localStorage.clear();
|
|
// Reset store between tests
|
|
useFocusStore.setState({
|
|
current: null,
|
|
queue: [],
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
describe("setItems", () => {
|
|
it("sets first item as current and rest as queue", () => {
|
|
const items = [
|
|
makeFocusItem({ id: "a", title: "First" }),
|
|
makeFocusItem({ id: "b", title: "Second" }),
|
|
makeFocusItem({ id: "c", title: "Third" }),
|
|
];
|
|
|
|
useFocusStore.getState().setItems(items);
|
|
|
|
const state = useFocusStore.getState();
|
|
expect(state.current?.id).toBe("a");
|
|
expect(state.queue).toHaveLength(2);
|
|
expect(state.queue[0].id).toBe("b");
|
|
expect(state.queue[1].id).toBe("c");
|
|
});
|
|
|
|
it("sets current to null when empty", () => {
|
|
useFocusStore.getState().setItems([]);
|
|
|
|
const state = useFocusStore.getState();
|
|
expect(state.current).toBeNull();
|
|
expect(state.queue).toHaveLength(0);
|
|
});
|
|
|
|
it("clears loading and error on setItems", () => {
|
|
useFocusStore.setState({ isLoading: true, error: "old error" });
|
|
|
|
useFocusStore.getState().setItems([makeFocusItem()]);
|
|
|
|
const state = useFocusStore.getState();
|
|
expect(state.isLoading).toBe(false);
|
|
expect(state.error).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("act", () => {
|
|
it("advances to next item in queue", () => {
|
|
useFocusStore.getState().setItems([
|
|
makeFocusItem({ id: "a" }),
|
|
makeFocusItem({ id: "b" }),
|
|
makeFocusItem({ id: "c" }),
|
|
]);
|
|
|
|
const next = useFocusStore.getState().act("start");
|
|
|
|
expect(next?.id).toBe("b");
|
|
expect(useFocusStore.getState().current?.id).toBe("b");
|
|
expect(useFocusStore.getState().queue).toHaveLength(1);
|
|
});
|
|
|
|
it("returns null when queue is empty", () => {
|
|
useFocusStore.getState().setItems([makeFocusItem({ id: "only" })]);
|
|
|
|
const next = useFocusStore.getState().act("skip");
|
|
|
|
expect(next).toBeNull();
|
|
expect(useFocusStore.getState().current).toBeNull();
|
|
expect(useFocusStore.getState().queue).toHaveLength(0);
|
|
});
|
|
|
|
it("works with defer_1h action", () => {
|
|
useFocusStore.getState().setItems([
|
|
makeFocusItem({ id: "a" }),
|
|
makeFocusItem({ id: "b" }),
|
|
]);
|
|
|
|
useFocusStore.getState().act("defer_1h", "in a meeting");
|
|
|
|
expect(useFocusStore.getState().current?.id).toBe("b");
|
|
});
|
|
|
|
it("works with defer_tomorrow action", () => {
|
|
useFocusStore.getState().setItems([
|
|
makeFocusItem({ id: "a" }),
|
|
makeFocusItem({ id: "b" }),
|
|
]);
|
|
|
|
useFocusStore.getState().act("defer_tomorrow");
|
|
|
|
expect(useFocusStore.getState().current?.id).toBe("b");
|
|
});
|
|
});
|
|
|
|
describe("setFocus", () => {
|
|
it("promotes a queue item to current", () => {
|
|
useFocusStore.getState().setItems([
|
|
makeFocusItem({ id: "a", title: "First" }),
|
|
makeFocusItem({ id: "b", title: "Second" }),
|
|
makeFocusItem({ id: "c", title: "Third" }),
|
|
]);
|
|
|
|
useFocusStore.getState().setFocus("c");
|
|
|
|
const state = useFocusStore.getState();
|
|
expect(state.current?.id).toBe("c");
|
|
// Previous current and other queue items are in queue
|
|
expect(state.queue.map((i) => i.id)).toEqual(
|
|
expect.arrayContaining(["a", "b"])
|
|
);
|
|
expect(state.queue).toHaveLength(2);
|
|
});
|
|
|
|
it("does nothing for unknown item ID", () => {
|
|
useFocusStore.getState().setItems([makeFocusItem({ id: "a" })]);
|
|
|
|
useFocusStore.getState().setFocus("nonexistent");
|
|
|
|
expect(useFocusStore.getState().current?.id).toBe("a");
|
|
});
|
|
});
|
|
|
|
describe("reorderQueue", () => {
|
|
it("moves an item from one position to another", () => {
|
|
useFocusStore.getState().setItems([
|
|
makeFocusItem({ id: "focus" }),
|
|
makeFocusItem({ id: "a" }),
|
|
makeFocusItem({ id: "b" }),
|
|
makeFocusItem({ id: "c" }),
|
|
]);
|
|
|
|
useFocusStore.getState().reorderQueue(2, 0);
|
|
|
|
const ids = useFocusStore.getState().queue.map((i) => i.id);
|
|
expect(ids).toEqual(["c", "a", "b"]);
|
|
});
|
|
|
|
it("does nothing for same from/to index", () => {
|
|
useFocusStore.getState().setItems([
|
|
makeFocusItem({ id: "focus" }),
|
|
makeFocusItem({ id: "a" }),
|
|
makeFocusItem({ id: "b" }),
|
|
]);
|
|
|
|
useFocusStore.getState().reorderQueue(0, 0);
|
|
|
|
const ids = useFocusStore.getState().queue.map((i) => i.id);
|
|
expect(ids).toEqual(["a", "b"]);
|
|
});
|
|
|
|
it("does nothing for out-of-bounds indices", () => {
|
|
useFocusStore.getState().setItems([
|
|
makeFocusItem({ id: "focus" }),
|
|
makeFocusItem({ id: "a" }),
|
|
]);
|
|
|
|
useFocusStore.getState().reorderQueue(-1, 0);
|
|
useFocusStore.getState().reorderQueue(0, 5);
|
|
|
|
expect(useFocusStore.getState().queue.map((i) => i.id)).toEqual(["a"]);
|
|
});
|
|
|
|
it("does not affect current focus", () => {
|
|
useFocusStore.getState().setItems([
|
|
makeFocusItem({ id: "focus" }),
|
|
makeFocusItem({ id: "a" }),
|
|
makeFocusItem({ id: "b" }),
|
|
]);
|
|
|
|
useFocusStore.getState().reorderQueue(1, 0);
|
|
|
|
expect(useFocusStore.getState().current?.id).toBe("focus");
|
|
});
|
|
});
|
|
|
|
describe("setLoading / setError", () => {
|
|
it("sets loading state", () => {
|
|
useFocusStore.getState().setLoading(true);
|
|
expect(useFocusStore.getState().isLoading).toBe(true);
|
|
});
|
|
|
|
it("sets error state", () => {
|
|
useFocusStore.getState().setError("something broke");
|
|
expect(useFocusStore.getState().error).toBe("something broke");
|
|
});
|
|
});
|
|
|
|
describe("persistence", () => {
|
|
it("persists current and queue to localStorage", () => {
|
|
const items = [
|
|
makeFocusItem({ id: "a", title: "First" }),
|
|
makeFocusItem({ id: "b", title: "Second" }),
|
|
];
|
|
|
|
useFocusStore.getState().setItems(items);
|
|
|
|
const stored = localStorage.getItem("mc-focus-store");
|
|
expect(stored).not.toBeNull();
|
|
|
|
const parsed = JSON.parse(stored!);
|
|
expect(parsed.state.current.id).toBe("a");
|
|
expect(parsed.state.queue).toHaveLength(1);
|
|
expect(parsed.state.queue[0].id).toBe("b");
|
|
});
|
|
|
|
it("does not persist isLoading or error", () => {
|
|
useFocusStore.setState({ isLoading: true, error: "oops" });
|
|
|
|
const stored = localStorage.getItem("mc-focus-store");
|
|
expect(stored).not.toBeNull();
|
|
|
|
const parsed = JSON.parse(stored!);
|
|
expect(parsed.state).not.toHaveProperty("isLoading");
|
|
expect(parsed.state).not.toHaveProperty("error");
|
|
});
|
|
});
|
|
});
|