import { renderHook, act } from '@testing-library/react'; import { DEFAULT_PREFERENCES, writePreferences, usePreferences, useUpdatePreferences } from '@/lib/preferences'; const STORAGE_KEY = 'doormilePreferences'; /** * Operator display preferences. * * Two properties matter: unknown/corrupt storage must never take a page down, * and a change must reach EVERY mounted consumer including the one that made it * — `storage` alone fires only in other tabs, which left the table that * triggered the change unchanged. */ describe('preferences', () => { describe('defaults', () => { it('should default to comfortable density and a 25-row page', () => { expect(DEFAULT_PREFERENCES).toEqual({ compactDensity: false, defaultPageSize: 25 }); }); it('should read the defaults when nothing is stored', () => { const { result } = renderHook(() => usePreferences()); expect(result.current).toEqual(DEFAULT_PREFERENCES); }); it('should merge a partial stored record over the defaults', () => { // A record written by an older build must not leave newer keys undefined. localStorage.setItem(STORAGE_KEY, JSON.stringify({ compactDensity: true })); const { result } = renderHook(() => usePreferences()); expect(result.current).toEqual({ compactDensity: true, defaultPageSize: 25 }); }); it('should fall back to the defaults for a corrupt stored record', () => { localStorage.setItem(STORAGE_KEY, '{not json'); const { result } = renderHook(() => usePreferences()); expect(result.current).toEqual(DEFAULT_PREFERENCES); }); it('should not hand out the shared defaults object itself', () => { // A consumer mutating what it reads would silently rewrite the defaults // for every other consumer. const { result } = renderHook(() => usePreferences()); expect(result.current).not.toBe(DEFAULT_PREFERENCES); }); }); describe('writePreferences', () => { it('should persist a patch merged over what is already stored', () => { writePreferences({ compactDensity: true }); writePreferences({ defaultPageSize: 100 }); expect(JSON.parse(localStorage.getItem(STORAGE_KEY))).toEqual({ compactDensity: true, defaultPageSize: 100 }); }); it('should return the merged result to the caller', () => { expect(writePreferences({ defaultPageSize: 50 })).toEqual({ compactDensity: false, defaultPageSize: 50 }); }); it('should broadcast the change so the triggering component re-renders too', () => { const listener = jest.fn(); window.addEventListener('doormile:preferences', listener); writePreferences({ compactDensity: true }); window.removeEventListener('doormile:preferences', listener); expect(listener).toHaveBeenCalledTimes(1); expect(listener.mock.calls[0][0].detail).toEqual({ compactDensity: true, defaultPageSize: 25 }); }); it('should still broadcast when the write itself is refused', () => { // Private mode / quota: the change applies for this session either way, // so the UI must not be left showing the old value. const setItem = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('QuotaExceededError'); }); const listener = jest.fn(); window.addEventListener('doormile:preferences', listener); expect(() => writePreferences({ compactDensity: true })).not.toThrow(); expect(listener).toHaveBeenCalledTimes(1); window.removeEventListener('doormile:preferences', listener); setItem.mockRestore(); }); it('should accept an empty patch as a no-op that still returns the current values', () => { expect(writePreferences({})).toEqual(DEFAULT_PREFERENCES); }); }); describe('usePreferences', () => { it('should re-render with the new value when a preference changes', () => { const { result } = renderHook(() => usePreferences()); expect(result.current.compactDensity).toBe(false); act(() => { writePreferences({ compactDensity: true }); }); expect(result.current.compactDensity).toBe(true); }); it('should pick up a change made in another tab', () => { const { result } = renderHook(() => usePreferences()); act(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify({ defaultPageSize: 100 })); window.dispatchEvent(new Event('storage')); }); expect(result.current.defaultPageSize).toBe(100); }); it('should keep every mounted consumer in agreement', () => { const a = renderHook(() => usePreferences()); const b = renderHook(() => usePreferences()); act(() => { writePreferences({ defaultPageSize: 50 }); }); expect(a.result.current).toEqual(b.result.current); expect(a.result.current.defaultPageSize).toBe(50); }); it('should stop listening once unmounted', () => { const removeSpy = jest.spyOn(window, 'removeEventListener'); const { unmount } = renderHook(() => usePreferences()); unmount(); expect(removeSpy).toHaveBeenCalledWith('doormile:preferences', expect.any(Function)); expect(removeSpy).toHaveBeenCalledWith('storage', expect.any(Function)); }); }); describe('useUpdatePreferences', () => { it('should write and broadcast through the same path as writePreferences', () => { const update = renderHook(() => useUpdatePreferences()); const read = renderHook(() => usePreferences()); act(() => { update.result.current({ compactDensity: true }); }); expect(read.result.current.compactDensity).toBe(true); expect(JSON.parse(localStorage.getItem(STORAGE_KEY)).compactDensity).toBe(true); }); it('should keep a stable identity across renders so consumers do not re-run effects', () => { const { result, rerender } = renderHook(() => useUpdatePreferences()); const first = result.current; rerender(); expect(result.current).toBe(first); }); }); });