From 41942a13167d07205cddc0ce62321d01431619a7 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 18:31:27 +0530 Subject: [PATCH] feat(renderer): useSettingsStore (persisted, validated, reset-able) --- src/renderer/stores/settings-store.ts | 38 ++++++++++++++++++++ tests/unit/stores/settings-store.test.ts | 44 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/renderer/stores/settings-store.ts create mode 100644 tests/unit/stores/settings-store.test.ts diff --git a/src/renderer/stores/settings-store.ts b/src/renderer/stores/settings-store.ts new file mode 100644 index 0000000..a0cdf87 --- /dev/null +++ b/src/renderer/stores/settings-store.ts @@ -0,0 +1,38 @@ +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import { settingsSchema, type Settings } from '@/lib/validators'; + +type SettingsLeaf = keyof Omit; + +interface SettingsState extends Settings { + setSetting: (key: K, value: Settings[K]) => void; + resetToDefaults: () => void; +} + +const DEFAULTS = settingsSchema.parse({}); + +export const useSettingsStore = create()( + persist( + (set) => ({ + ...DEFAULTS, + setSetting: (key, value) => set((s) => ({ ...s, [key]: value }) as Partial), + resetToDefaults: () => set(() => ({ ...DEFAULTS })), + }), + { + name: 'mc-settings-store', + storage: createJSONStorage(() => localStorage), + partialize: (state) => { + const { setSetting, resetToDefaults, ...rest } = state; + return rest; + }, + onRehydrateStorage: () => (state) => { + if (!state) return; + const result = settingsSchema.safeParse(state); + if (!result.success) { + console.warn('[settings-store] invalid persisted state, resetting to defaults', result.error); + useSettingsStore.setState({ ...DEFAULTS } as any); + } + }, + } + ) +); \ No newline at end of file diff --git a/tests/unit/stores/settings-store.test.ts b/tests/unit/stores/settings-store.test.ts new file mode 100644 index 0000000..8b77156 --- /dev/null +++ b/tests/unit/stores/settings-store.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useSettingsStore } from '@/stores/settings-store'; + +describe('useSettingsStore', () => { + beforeEach(() => { + localStorage.clear(); + useSettingsStore.setState(useSettingsStore.getInitialState()); + }); + + it('has sensible defaults', () => { + const s = useSettingsStore.getState(); + expect(s.fontSize).toBe(14); + expect(s.theme).toBe('auto'); + expect(s.pdfFormat).toBe('a4'); + expect(s.docxTemplate).toBe('standard'); + expect(s.renderTablesAsAscii).toBe(false); + expect(s.welcomeDismissed).toBe(false); + }); + + it('setSetting updates a leaf field', () => { + useSettingsStore.getState().setSetting('fontSize', 18); + expect(useSettingsStore.getState().fontSize).toBe(18); + }); + + it('resetToDefaults restores all defaults', () => { + useSettingsStore.getState().setSetting('fontSize', 22); + useSettingsStore.getState().setSetting('theme', 'dark'); + useSettingsStore.getState().resetToDefaults(); + expect(useSettingsStore.getState().fontSize).toBe(14); + expect(useSettingsStore.getState().theme).toBe('auto'); + }); + + it('persists only leaf settings (partialize), not actions', () => { + useSettingsStore.getState().setSetting('fontSize', 16); + useSettingsStore.getState().setSetting('docxTemplate', 'modern'); + const raw = localStorage.getItem('mc-settings-store'); + expect(raw).toBeTruthy(); + const parsed = JSON.parse(raw!); + expect(parsed.state.fontSize).toBe(16); + expect(parsed.state.docxTemplate).toBe('modern'); + expect(parsed.state.setSetting).toBeUndefined(); + expect(parsed.state.resetToDefaults).toBeUndefined(); + }); +}); \ No newline at end of file