Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions src/app/state/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,13 +365,17 @@ export function mergePersistedSettings(
const base = { ...defaultSettings, ...fileDefaults };
if (rawLocalStorage === null) return base;

const parsed = JSON.parse(rawLocalStorage) as Record<string, unknown>;
migrateParsedLocalStorage(parsed);

return {
...base,
...(parsed as unknown as Settings),
};
try {
const parsed = JSON.parse(rawLocalStorage) as Record<string, unknown>;
migrateParsedLocalStorage(parsed);

return {
...base,
...(parsed as unknown as Settings),
};
} catch {
return base;
}
}

const MESSAGE_SPACING_VALUES = new Set<MessageSpacing>(['0', '100', '200', '300', '400', '500']);
Expand Down Expand Up @@ -546,15 +550,30 @@ export const baseSettings = atom<Settings>(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<Settings, [Settings], undefined>(
Expand Down
11 changes: 9 additions & 2 deletions src/app/utils/debugLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();
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,
});
Loading