diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index b1b744c1f..3958c1c21 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -369,13 +369,17 @@ export function mergePersistedSettings( const base = { ...defaultSettings, ...fileDefaults }; if (rawLocalStorage === null) return base; - const parsed = JSON.parse(rawLocalStorage) as Record; - migrateParsedLocalStorage(parsed); - - return { - ...base, - ...(parsed as unknown as Settings), - }; + try { + const parsed = JSON.parse(rawLocalStorage) as Record; + migrateParsedLocalStorage(parsed); + + return { + ...base, + ...(parsed as unknown as Settings), + }; + } catch { + return base; + } } const MESSAGE_SPACING_VALUES = new Set(['0', '100', '200', '300', '400', '500']); @@ -550,15 +554,30 @@ export const baseSettings = atom(cloneDefaultSettings()); export function bootstrapSettingsStore(store: Store, rawSettingsDefaults: unknown): void { const sanitized = sanitizeSettingsDefaults(rawSettingsDefaults); runtimeSettingsDefaults = sanitized; - const merged = mergePersistedSettings(localStorage.getItem(STORAGE_KEY), sanitized); + let raw: string | null = null; + try { + raw = localStorage.getItem(STORAGE_KEY); + } catch { + // localStorage unavailable (e.g. Node.js 22 test environment) + } + const merged = mergePersistedSettings(raw, sanitized); store.set(baseSettings, merged); } -export const getSettings = (): Settings => - mergePersistedSettings(localStorage.getItem(STORAGE_KEY), runtimeSettingsDefaults); +export const getSettings = (): Settings => { + try { + return mergePersistedSettings(localStorage.getItem(STORAGE_KEY), runtimeSettingsDefaults); + } catch { + return { ...defaultSettings, ...runtimeSettingsDefaults }; + } +}; export const setSettings = (settings: Settings) => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } catch { + // localStorage unavailable (e.g. Node.js 22 test environment) + } }; export const settingsAtom = atom( diff --git a/src/app/utils/debugLogger.ts b/src/app/utils/debugLogger.ts index 88f787328..e727fa5ab 100644 --- a/src/app/utils/debugLogger.ts +++ b/src/app/utils/debugLogger.ts @@ -47,8 +47,15 @@ class DebugLoggerService { private sentryStats = { errors: 0, warnings: 0 }; constructor() { - // Check if debug logging is enabled from localStorage - this.enabled = localStorage.getItem('sable_internal_debug') === '1'; + // Check if debug logging is enabled from localStorage. + // Guarded with try/catch because this module is instantiated as a singleton + // at import time, which in Node.js 22+ can run before a jsdom environment + // is ready (Node has a built-in but non-functional localStorage stub). + try { + this.enabled = localStorage.getItem('sable_internal_debug') === '1'; + } catch { + this.enabled = false; + } // Load disabled breadcrumb categories try { const stored = localStorage.getItem(BREADCRUMB_DISABLED_KEY); diff --git a/src/test/setup.ts b/src/test/setup.ts index 7b0828bfa..28c93a12c 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1 +1,29 @@ import '@testing-library/jest-dom'; + +// Node.js 22+ ships a built-in `localStorage` stub that throws for getItem/setItem +// unless --localstorage-file is supplied at startup. jsdom relies on being able to +// define window.localStorage, but Node's version can prevent that. We install an +// in-memory implementation unconditionally so every test environment starts with a +// working, isolated localStorage regardless of runtime version. +const _store = new Map(); +const _localStorage = { + getItem: (key: string): string | null => _store.get(key) ?? null, + setItem: (key: string, value: string): void => { + _store.set(key, value); + }, + removeItem: (key: string): void => { + _store.delete(key); + }, + clear: (): void => { + _store.clear(); + }, + get length(): number { + return _store.size; + }, + key: (index: number): string | null => [..._store.keys()][index] ?? null, +}; +Object.defineProperty(globalThis, 'localStorage', { + value: _localStorage, + writable: true, + configurable: true, +});