import { renderHook, act } from '@testing-library/react'; import { useLocalStorage } from '../useLocalStorage'; // Mock localStorage const localStorageMock = { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn(), }; Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true, }); describe('useLocalStorage', () => { beforeEach(() => { jest.clearAllMocks(); localStorageMock.getItem.mockReturnValue(null); }); describe('initialization', () => { test('should return default value when localStorage is empty', () => { localStorageMock.getItem.mockReturnValue(null); const { result } = renderHook(() => useLocalStorage('test-key', 'default-value') ); expect(result.current[0]).toBe('default-value'); expect(localStorageMock.getItem).toHaveBeenCalledWith('test-key'); }); test('should return stored value when localStorage has data', () => { localStorageMock.getItem.mockReturnValue(JSON.stringify('stored-value')); const { result } = renderHook(() => useLocalStorage('test-key', 'default-value') ); expect(result.current[0]).toBe('stored-value'); expect(localStorageMock.getItem).toHaveBeenCalledWith('test-key'); }); test('should handle complex objects', () => { const storedObject = { name: 'John', age: 30 }; localStorageMock.getItem.mockReturnValue(JSON.stringify(storedObject)); const { result } = renderHook(() => useLocalStorage('user', { name: '', age: 0 }) ); expect(result.current[0]).toEqual(storedObject); }); test('should handle arrays', () => { const storedArray = ['item1', 'item2', 'item3']; localStorageMock.getItem.mockReturnValue(JSON.stringify(storedArray)); const { result } = renderHook(() => useLocalStorage('items', [])); expect(result.current[0]).toEqual(storedArray); }); test('should handle boolean values', () => { localStorageMock.getItem.mockReturnValue(JSON.stringify(true)); const { result } = renderHook(() => useLocalStorage('isEnabled', false)); expect(result.current[0]).toBe(true); }); test('should handle number values', () => { localStorageMock.getItem.mockReturnValue(JSON.stringify(42)); const { result } = renderHook(() => useLocalStorage('count', 0)); expect(result.current[0]).toBe(42); }); test('should handle null values', () => { localStorageMock.getItem.mockReturnValue(JSON.stringify(null)); const { result } = renderHook(() => useLocalStorage('nullable', 'default') ); expect(result.current[0]).toBe(null); }); }); describe('error handling', () => { test('should return default value when JSON parsing fails', () => { localStorageMock.getItem.mockReturnValue('invalid-json'); const { result } = renderHook(() => useLocalStorage('test-key', 'default-value') ); expect(result.current[0]).toBe('default-value'); }); test('should return default value when localStorage throws error', () => { localStorageMock.getItem.mockImplementation(() => { throw new Error('Storage error'); }); const { result } = renderHook(() => useLocalStorage('test-key', 'default-value') ); expect(result.current[0]).toBe('default-value'); }); test('should handle corrupted data gracefully', () => { localStorageMock.getItem.mockReturnValue('{"incomplete": json'); const { result } = renderHook(() => useLocalStorage('test-key', { complete: 'data' }) ); expect(result.current[0]).toEqual({ complete: 'data' }); }); }); describe('setValue functionality', () => { test('should update value and store in localStorage', () => { const { result } = renderHook(() => useLocalStorage('test-key', 'initial') ); act(() => { result.current[1]('updated-value'); }); expect(result.current[0]).toBe('updated-value'); expect(localStorageMock.setItem).toHaveBeenCalledWith( 'test-key', JSON.stringify('updated-value') ); }); test('should handle function updates', () => { localStorageMock.getItem.mockReturnValue(JSON.stringify(5)); const { result } = renderHook(() => useLocalStorage('counter', 0)); act(() => { result.current[1]((prev: number) => prev + 1); }); expect(result.current[0]).toBe(6); expect(localStorageMock.setItem).toHaveBeenCalledWith( 'counter', JSON.stringify(6) ); }); test('should update complex objects', () => { const initialUser = { name: 'John', age: 30 }; localStorageMock.getItem.mockReturnValue(JSON.stringify(initialUser)); const { result } = renderHook(() => useLocalStorage('user', { name: '', age: 0 }) ); const updatedUser = { name: 'Jane', age: 25 }; act(() => { result.current[1](updatedUser); }); expect(result.current[0]).toEqual(updatedUser); expect(localStorageMock.setItem).toHaveBeenCalledWith( 'user', JSON.stringify(updatedUser) ); }); test('should update arrays', () => { const initialItems = ['item1', 'item2']; localStorageMock.getItem.mockReturnValue(JSON.stringify(initialItems)); const { result } = renderHook(() => useLocalStorage('items', [])); act(() => { result.current[1]((prev: string[]) => [...prev, 'item3']); }); expect(result.current[0]).toEqual(['item1', 'item2', 'item3']); expect(localStorageMock.setItem).toHaveBeenCalledWith( 'items', JSON.stringify(['item1', 'item2', 'item3']) ); }); test('should handle setting null values', () => { const { result } = renderHook(() => useLocalStorage('test-key', 'initial') ); act(() => { result.current[1](null); }); expect(result.current[0]).toBe(null); expect(localStorageMock.setItem).toHaveBeenCalledWith( 'test-key', JSON.stringify(null) ); }); test('should handle setting undefined values', () => { const { result } = renderHook(() => useLocalStorage('test-key', 'initial') ); act(() => { result.current[1](undefined); }); expect(result.current[0]).toBe(undefined); expect(localStorageMock.setItem).toHaveBeenCalledWith( 'test-key', JSON.stringify(undefined) ); }); }); describe('server-side rendering (SSR) support', () => { test('should return default value when window is undefined', () => { const originalWindow = global.window; // @ts-ignore delete global.window; const { result } = renderHook(() => useLocalStorage('test-key', 'default-value') ); expect(result.current[0]).toBe('default-value'); // Restore window global.window = originalWindow; }); }); describe('persistence across rerenders', () => { test('should maintain state across rerenders', () => { const { result, rerender } = renderHook(() => useLocalStorage('test-key', 'initial') ); act(() => { result.current[1]('updated'); }); rerender(); expect(result.current[0]).toBe('updated'); }); test('should sync with localStorage changes', () => { localStorageMock.getItem.mockReturnValue(JSON.stringify('stored-value')); const { result } = renderHook(() => useLocalStorage('test-key', 'default') ); expect(result.current[0]).toBe('stored-value'); // Simulate external localStorage change localStorageMock.getItem.mockReturnValue( JSON.stringify('externally-changed') ); // Value should remain the same until component re-mounts expect(result.current[0]).toBe('stored-value'); }); }); describe('type safety', () => { test('should maintain type safety with strings', () => { const { result } = renderHook(() => useLocalStorage('string-key', 'default') ); // TypeScript should enforce string type act(() => { result.current[1]('new-string'); }); expect(typeof result.current[0]).toBe('string'); }); test('should maintain type safety with numbers', () => { const { result } = renderHook(() => useLocalStorage('number-key', 42)); act(() => { result.current[1](100); }); expect(typeof result.current[0]).toBe('number'); }); test('should maintain type safety with objects', () => { interface TestObject { id: number; name: string; } const defaultObj: TestObject = { id: 1, name: 'test' }; const { result } = renderHook(() => useLocalStorage('object-key', defaultObj) ); act(() => { result.current[1]({ id: 2, name: 'updated' }); }); expect(result.current[0]).toHaveProperty('id'); expect(result.current[0]).toHaveProperty('name'); }); }); describe('edge cases', () => { test('should handle empty string key', () => { const { result } = renderHook(() => useLocalStorage('', 'default')); expect(result.current[0]).toBe('default'); expect(localStorageMock.getItem).toHaveBeenCalledWith(''); }); test('should handle very long keys', () => { const longKey = 'a'.repeat(1000); const { result } = renderHook(() => useLocalStorage(longKey, 'default')); expect(result.current[0]).toBe('default'); expect(localStorageMock.getItem).toHaveBeenCalledWith(longKey); }); test('should handle special characters in key', () => { const specialKey = 'key-with-!@#$%^&*()_+-={}[]|\\:";\'<>?,./'; const { result } = renderHook(() => useLocalStorage(specialKey, 'default') ); expect(result.current[0]).toBe('default'); expect(localStorageMock.getItem).toHaveBeenCalledWith(specialKey); }); test('should handle rapid state updates', () => { const { result } = renderHook(() => useLocalStorage('rapid-key', 0)); act(() => { for (let i = 1; i <= 10; i++) { result.current[1](i); } }); expect(result.current[0]).toBe(10); expect(localStorageMock.setItem).toHaveBeenCalledTimes(10); }); }); describe('memory cleanup', () => { test('should not leak memory on unmount', () => { const { unmount } = renderHook(() => useLocalStorage('cleanup-key', 'default') ); unmount(); // Hook should not continue to interact with localStorage after unmount expect(localStorageMock.setItem).not.toHaveBeenCalled(); }); }); });