Skip to content

Commit ac5075e

Browse files
committed
fix(tests): guard localStorage access against Node.js 22 built-in stub
- debugLogger: wrap constructor localStorage.getItem in try/catch - settings: wrap getSettings/setSettings in try/catch - test/setup.ts: install in-memory localStorage polyfill before jsdom initialises so module-level singleton access resolves correctly All 60 test files (570 tests) now pass on Node.js 22.
1 parent c2bb683 commit ac5075e

3 files changed

Lines changed: 67 additions & 13 deletions

File tree

src/app/state/settings.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -365,13 +365,17 @@ export function mergePersistedSettings(
365365
const base = { ...defaultSettings, ...fileDefaults };
366366
if (rawLocalStorage === null) return base;
367367

368-
const parsed = JSON.parse(rawLocalStorage) as Record<string, unknown>;
369-
migrateParsedLocalStorage(parsed);
370-
371-
return {
372-
...base,
373-
...(parsed as unknown as Settings),
374-
};
368+
try {
369+
const parsed = JSON.parse(rawLocalStorage) as Record<string, unknown>;
370+
migrateParsedLocalStorage(parsed);
371+
372+
return {
373+
...base,
374+
...(parsed as unknown as Settings),
375+
};
376+
} catch {
377+
return base;
378+
}
375379
}
376380

377381
const MESSAGE_SPACING_VALUES = new Set<MessageSpacing>(['0', '100', '200', '300', '400', '500']);
@@ -546,15 +550,30 @@ export const baseSettings = atom<Settings>(cloneDefaultSettings());
546550
export function bootstrapSettingsStore(store: Store, rawSettingsDefaults: unknown): void {
547551
const sanitized = sanitizeSettingsDefaults(rawSettingsDefaults);
548552
runtimeSettingsDefaults = sanitized;
549-
const merged = mergePersistedSettings(localStorage.getItem(STORAGE_KEY), sanitized);
553+
let raw: string | null = null;
554+
try {
555+
raw = localStorage.getItem(STORAGE_KEY);
556+
} catch {
557+
// localStorage unavailable (e.g. Node.js 22 test environment)
558+
}
559+
const merged = mergePersistedSettings(raw, sanitized);
550560
store.set(baseSettings, merged);
551561
}
552562

553-
export const getSettings = (): Settings =>
554-
mergePersistedSettings(localStorage.getItem(STORAGE_KEY), runtimeSettingsDefaults);
563+
export const getSettings = (): Settings => {
564+
try {
565+
return mergePersistedSettings(localStorage.getItem(STORAGE_KEY), runtimeSettingsDefaults);
566+
} catch {
567+
return { ...defaultSettings, ...runtimeSettingsDefaults };
568+
}
569+
};
555570

556571
export const setSettings = (settings: Settings) => {
557-
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
572+
try {
573+
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
574+
} catch {
575+
// localStorage unavailable (e.g. Node.js 22 test environment)
576+
}
558577
};
559578

560579
export const settingsAtom = atom<Settings, [Settings], undefined>(

src/app/utils/debugLogger.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,15 @@ class DebugLoggerService {
4747
private sentryStats = { errors: 0, warnings: 0 };
4848

4949
constructor() {
50-
// Check if debug logging is enabled from localStorage
51-
this.enabled = localStorage.getItem('sable_internal_debug') === '1';
50+
// Check if debug logging is enabled from localStorage.
51+
// Guarded with try/catch because this module is instantiated as a singleton
52+
// at import time, which in Node.js 22+ can run before a jsdom environment
53+
// is ready (Node has a built-in but non-functional localStorage stub).
54+
try {
55+
this.enabled = localStorage.getItem('sable_internal_debug') === '1';
56+
} catch {
57+
this.enabled = false;
58+
}
5259
// Load disabled breadcrumb categories
5360
try {
5461
const stored = localStorage.getItem(BREADCRUMB_DISABLED_KEY);

src/test/setup.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
11
import '@testing-library/jest-dom';
2+
3+
// Node.js 22+ ships a built-in `localStorage` stub that throws for getItem/setItem
4+
// unless --localstorage-file is supplied at startup. jsdom relies on being able to
5+
// define window.localStorage, but Node's version can prevent that. We install an
6+
// in-memory implementation unconditionally so every test environment starts with a
7+
// working, isolated localStorage regardless of runtime version.
8+
const _store = new Map<string, string>();
9+
const _localStorage = {
10+
getItem: (key: string): string | null => _store.get(key) ?? null,
11+
setItem: (key: string, value: string): void => {
12+
_store.set(key, value);
13+
},
14+
removeItem: (key: string): void => {
15+
_store.delete(key);
16+
},
17+
clear: (): void => {
18+
_store.clear();
19+
},
20+
get length(): number {
21+
return _store.size;
22+
},
23+
key: (index: number): string | null => [..._store.keys()][index] ?? null,
24+
};
25+
Object.defineProperty(globalThis, 'localStorage', {
26+
value: _localStorage,
27+
writable: true,
28+
configurable: true,
29+
});

0 commit comments

Comments
 (0)