diff --git a/packages/ui/src/stores/utils/safeStorage.test.ts b/packages/ui/src/stores/utils/safeStorage.test.ts new file mode 100644 index 00000000..31301261 --- /dev/null +++ b/packages/ui/src/stores/utils/safeStorage.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from 'bun:test'; + +const importSafeStorage = async () => { + return await import(`./safeStorage.ts?test=${Date.now()}-${Math.random()}`) as typeof import('./safeStorage'); +}; + +describe('safeStorage', () => { + test('falls back to memory when storage getters throw', async () => { + const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); + const throwingWindow = {}; + + Object.defineProperties(throwingWindow, { + localStorage: { + get() { + throw new Error('localStorage blocked'); + }, + }, + sessionStorage: { + get() { + throw new Error('sessionStorage blocked'); + }, + }, + }); + + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: throwingWindow, + }); + + try { + const { getSafeSessionStorage, getSafeStorage } = await importSafeStorage(); + const storage = getSafeStorage(); + const sessionStorage = getSafeSessionStorage(); + + storage.setItem('local-key', 'local-value'); + sessionStorage.setItem('session-key', 'session-value'); + + expect(storage.getItem('local-key')).toBe('local-value'); + expect(sessionStorage.getItem('session-key')).toBe('session-value'); + } finally { + if (previousWindow) { + Object.defineProperty(globalThis, 'window', previousWindow); + } else { + delete (globalThis as { window?: unknown }).window; + } + } + }); +}); diff --git a/packages/ui/src/stores/utils/safeStorage.ts b/packages/ui/src/stores/utils/safeStorage.ts index d7dd962c..3a568aa8 100644 --- a/packages/ui/src/stores/utils/safeStorage.ts +++ b/packages/ui/src/stores/utils/safeStorage.ts @@ -1,6 +1,18 @@ let safeStorageInstance: Storage | null = null; let safeSessionStorageInstance: Storage | null = null; +const getWindowStorage = (key: 'localStorage' | 'sessionStorage'): Storage | null => { + if (typeof window === 'undefined') { + return null; + } + + try { + return window[key] ?? null; + } catch { + return null; + } +}; + const createInMemoryStorage = (): Storage => { const store = new Map(); return { @@ -22,11 +34,12 @@ const createInMemoryStorage = (): Storage => { }; const createSafeStorage = (): Storage => { - if (typeof window === 'undefined' || !window.localStorage) { + const baseStorage = getWindowStorage('localStorage'); + + if (!baseStorage) { return createInMemoryStorage(); } - const baseStorage = window.localStorage; const fallback = createInMemoryStorage(); let storageAvailable = true; @@ -123,11 +136,12 @@ export const getSafeStorage = (): Storage => { }; const createSafeSessionStorage = (): Storage => { - if (typeof window === 'undefined' || !window.sessionStorage) { + const baseStorage = getWindowStorage('sessionStorage'); + + if (!baseStorage) { return createInMemoryStorage(); } - const baseStorage = window.sessionStorage; const fallback = createInMemoryStorage(); let storageAvailable = true;