/** * Tests for settings-store. * * Verifies: * 1. Default values are set correctly * 2. hydrate() loads settings from Tauri backend * 3. hydrate() handles missing/null state gracefully * 4. hydrate() handles backend errors gracefully * 5. hydrate() validates types before applying * 6. update() persists changes to backend and updates store * 7. update() merges with existing state file data * 8. update() handles write errors gracefully (no partial state) * 9. extractSettings excludes methods from persisted data */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { act } from "@testing-library/react"; // Mock Tauri bindings (readState/writeState are re-exports from bindings) const mockReadState = vi.fn(); const mockWriteState = vi.fn(); vi.mock("@/lib/tauri", () => ({ readState: (...args: unknown[]) => mockReadState(...args), writeState: (...args: unknown[]) => mockWriteState(...args), })); // Import after mocking import { useSettingsStore } from "@/stores/settings-store"; describe("useSettingsStore", () => { beforeEach(() => { vi.clearAllMocks(); // Reset store to defaults useSettingsStore.setState({ syncInterval: 15, notificationsEnabled: true, quickCaptureShortcut: "CommandOrControl+Shift+C", }); }); describe("defaults", () => { it("has correct default values", () => { const state = useSettingsStore.getState(); expect(state.syncInterval).toBe(15); expect(state.notificationsEnabled).toBe(true); expect(state.quickCaptureShortcut).toBe("CommandOrControl+Shift+C"); }); }); describe("hydrate", () => { it("loads settings from Tauri backend", async () => { mockReadState.mockResolvedValue({ status: "ok", data: { settings: { syncInterval: 30, notificationsEnabled: false, quickCaptureShortcut: "Meta+Shift+X", }, }, }); await act(async () => { await useSettingsStore.getState().hydrate(); }); const state = useSettingsStore.getState(); expect(state.syncInterval).toBe(30); expect(state.notificationsEnabled).toBe(false); expect(state.quickCaptureShortcut).toBe("Meta+Shift+X"); }); it("keeps defaults when state is null (first run)", async () => { mockReadState.mockResolvedValue({ status: "ok", data: null, }); await act(async () => { await useSettingsStore.getState().hydrate(); }); const state = useSettingsStore.getState(); expect(state.syncInterval).toBe(15); expect(state.notificationsEnabled).toBe(true); }); it("keeps defaults when state has no settings key", async () => { mockReadState.mockResolvedValue({ status: "ok", data: { otherStoreData: "value" }, }); await act(async () => { await useSettingsStore.getState().hydrate(); }); const state = useSettingsStore.getState(); expect(state.syncInterval).toBe(15); }); it("handles backend read errors gracefully", async () => { mockReadState.mockResolvedValue({ status: "error", error: { code: "IO_ERROR", message: "File not found", recoverable: true }, }); await act(async () => { await useSettingsStore.getState().hydrate(); }); // Should keep defaults, not crash const state = useSettingsStore.getState(); expect(state.syncInterval).toBe(15); }); it("validates syncInterval before applying", async () => { mockReadState.mockResolvedValue({ status: "ok", data: { settings: { syncInterval: 42, // Invalid - not 5, 15, or 30 notificationsEnabled: false, }, }, }); await act(async () => { await useSettingsStore.getState().hydrate(); }); const state = useSettingsStore.getState(); // Invalid syncInterval should be ignored, keep default expect(state.syncInterval).toBe(15); // Valid field should still be applied expect(state.notificationsEnabled).toBe(false); }); it("ignores non-boolean notificationsEnabled", async () => { mockReadState.mockResolvedValue({ status: "ok", data: { settings: { notificationsEnabled: "yes", // Wrong type }, }, }); await act(async () => { await useSettingsStore.getState().hydrate(); }); expect(useSettingsStore.getState().notificationsEnabled).toBe(true); }); it("ignores non-string quickCaptureShortcut", async () => { mockReadState.mockResolvedValue({ status: "ok", data: { settings: { quickCaptureShortcut: 123, // Wrong type }, }, }); await act(async () => { await useSettingsStore.getState().hydrate(); }); expect(useSettingsStore.getState().quickCaptureShortcut).toBe( "CommandOrControl+Shift+C" ); }); }); describe("update", () => { it("persists changes to backend and updates store", async () => { // Existing state in backend mockReadState.mockResolvedValue({ status: "ok", data: { otherData: "preserved" }, }); mockWriteState.mockResolvedValue({ status: "ok", data: null }); await act(async () => { await useSettingsStore.getState().update({ syncInterval: 5 }); }); // Store should be updated expect(useSettingsStore.getState().syncInterval).toBe(5); // Backend should receive merged state expect(mockWriteState).toHaveBeenCalledWith({ otherData: "preserved", settings: { syncInterval: 5, notificationsEnabled: true, quickCaptureShortcut: "CommandOrControl+Shift+C", }, }); }); it("merges with existing backend state", async () => { mockReadState.mockResolvedValue({ status: "ok", data: { "mc-focus-store": { current: null, queue: [] }, settings: { syncInterval: 30 }, }, }); mockWriteState.mockResolvedValue({ status: "ok", data: null }); await act(async () => { await useSettingsStore.getState().update({ notificationsEnabled: false }); }); // Should preserve other keys in state file expect(mockWriteState).toHaveBeenCalledWith( expect.objectContaining({ "mc-focus-store": { current: null, queue: [] }, }) ); }); it("does not update store on write failure", async () => { mockReadState.mockResolvedValue({ status: "ok", data: {} }); mockWriteState.mockResolvedValue({ status: "error", error: { code: "IO_ERROR", message: "Disk full", recoverable: false }, }); await act(async () => { await useSettingsStore.getState().update({ syncInterval: 30 }); }); // Store should NOT be updated since write failed expect(useSettingsStore.getState().syncInterval).toBe(15); }); it("handles null existing state on update", async () => { mockReadState.mockResolvedValue({ status: "ok", data: null }); mockWriteState.mockResolvedValue({ status: "ok", data: null }); await act(async () => { await useSettingsStore.getState().update({ quickCaptureShortcut: "Meta+K", }); }); expect(useSettingsStore.getState().quickCaptureShortcut).toBe("Meta+K"); expect(mockWriteState).toHaveBeenCalledWith({ settings: { syncInterval: 15, notificationsEnabled: true, quickCaptureShortcut: "Meta+K", }, }); }); it("persisted data does not include methods", async () => { mockReadState.mockResolvedValue({ status: "ok", data: {} }); mockWriteState.mockResolvedValue({ status: "ok", data: null }); await act(async () => { await useSettingsStore.getState().update({ syncInterval: 5 }); }); const writtenState = mockWriteState.mock.calls[0][0] as Record; const writtenSettings = writtenState.settings as Record; expect(writtenSettings).not.toHaveProperty("hydrate"); expect(writtenSettings).not.toHaveProperty("update"); expect(Object.keys(writtenSettings)).toEqual([ "syncInterval", "notificationsEnabled", "quickCaptureShortcut", ]); }); }); });