feat: add Tauri event hook and lore.db file watcher
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>
This commit is contained in:
112
tests/hooks/useTauriEvents.test.ts
Normal file
112
tests/hooks/useTauriEvents.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useTauriEvent, useTauriEvents } from "@/hooks/useTauriEvents";
|
||||
|
||||
// Mock the listen function
|
||||
const mockUnlisten = vi.fn();
|
||||
const mockListen = vi.fn().mockResolvedValue(mockUnlisten);
|
||||
|
||||
vi.mock("@tauri-apps/api/event", () => ({
|
||||
listen: (...args: unknown[]) => mockListen(...args),
|
||||
}));
|
||||
|
||||
describe("useTauriEvent", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("subscribes to the specified event on mount", async () => {
|
||||
const handler = vi.fn();
|
||||
renderHook(() => useTauriEvent("lore-data-changed", handler));
|
||||
|
||||
// Wait for the listen promise to resolve
|
||||
await vi.waitFor(() => {
|
||||
expect(mockListen).toHaveBeenCalledWith(
|
||||
"lore-data-changed",
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("calls the handler when event is received", async () => {
|
||||
const handler = vi.fn();
|
||||
renderHook(() => useTauriEvent("global-shortcut-triggered", handler));
|
||||
|
||||
// Get the callback that was passed to listen
|
||||
await vi.waitFor(() => {
|
||||
expect(mockListen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const eventCallback = mockListen.mock.calls[0][1];
|
||||
|
||||
// Simulate receiving an event
|
||||
act(() => {
|
||||
eventCallback({ payload: "quick-capture" });
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledWith("quick-capture");
|
||||
});
|
||||
|
||||
it("calls unlisten on unmount", async () => {
|
||||
const handler = vi.fn();
|
||||
const { unmount } = renderHook(() =>
|
||||
useTauriEvent("lore-data-changed", handler)
|
||||
);
|
||||
|
||||
// Wait for subscription to be set up
|
||||
await vi.waitFor(() => {
|
||||
expect(mockListen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockUnlisten).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTauriEvents", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("subscribes to multiple events", async () => {
|
||||
const handlers = {
|
||||
"lore-data-changed": vi.fn(),
|
||||
"sync-status": vi.fn(),
|
||||
};
|
||||
|
||||
renderHook(() => useTauriEvents(handlers));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockListen).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
expect(mockListen).toHaveBeenCalledWith(
|
||||
"lore-data-changed",
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(mockListen).toHaveBeenCalledWith("sync-status", expect.any(Function));
|
||||
});
|
||||
|
||||
it("cleans up all subscriptions on unmount", async () => {
|
||||
const handlers = {
|
||||
"lore-data-changed": vi.fn(),
|
||||
"sync-status": vi.fn(),
|
||||
};
|
||||
|
||||
const { unmount } = renderHook(() => useTauriEvents(handlers));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockListen).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
// Should call unlisten for each subscription
|
||||
expect(mockUnlisten).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import { makeFocusItem } from "../helpers/fixtures";
|
||||
|
||||
describe("useFocusStore", () => {
|
||||
beforeEach(() => {
|
||||
// Clear persisted state before each test
|
||||
localStorage.clear();
|
||||
// Reset store between tests
|
||||
useFocusStore.setState({
|
||||
current: null,
|
||||
@@ -189,4 +191,34 @@ describe("useFocusStore", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user