diff --git a/src/hooks/useLocalStorage.test.ts b/src/hooks/useLocalStorage.test.ts new file mode 100644 index 0000000..08c6f9b --- /dev/null +++ b/src/hooks/useLocalStorage.test.ts @@ -0,0 +1,235 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { useLocalStorage } from './useLocalStorage'; + +// Mock the error handling utility +vi.mock('../utils/errorHandling', () => ({ + handleError: vi.fn() +})); + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {}; + + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value.toString(); + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + store = {}; + } + }; +})(); + +describe('useLocalStorage', () => { + beforeEach(() => { + // Setup localStorage mock + Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true + }); + localStorageMock.clear(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return initial value when localStorage is empty', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 'initial-value')); + const [value] = result.current; + + expect(value).toBe('initial-value'); + }); + + it('should return initial value from function when localStorage is empty', () => { + const initializer = vi.fn(() => 'computed-value'); + const { result } = renderHook(() => useLocalStorage('test-key', initializer)); + const [value] = result.current; + + expect(value).toBe('computed-value'); + expect(initializer).toHaveBeenCalled(); + }); + + it('should set value in localStorage', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 'initial')); + + act(() => { + const [, setValue] = result.current; + setValue('new-value'); + }); + + const [value] = result.current; + expect(value).toBe('new-value'); + expect(JSON.parse(localStorage.getItem('test-key')!)).toBe('new-value'); + }); + + it('should handle updater function in setValue', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 10)); + + act(() => { + const [, setValue] = result.current; + setValue((prev) => prev + 5); + }); + + const [value] = result.current; + expect(value).toBe(15); + }); + + it('should read existing value from localStorage', () => { + localStorage.setItem('test-key', JSON.stringify('existing-value')); + + const { result } = renderHook(() => useLocalStorage('test-key', 'initial-value')); + const [value] = result.current; + + expect(value).toBe('existing-value'); + }); + + it('should handle complex objects', () => { + const complexObject = { name: 'John', age: 30, hobbies: ['reading', 'coding'] }; + const { result } = renderHook(() => useLocalStorage('test-key', complexObject)); + + act(() => { + const [, setValue] = result.current; + setValue({ ...complexObject, age: 31 }); + }); + + const [value] = result.current; + expect(value).toEqual({ name: 'John', age: 31, hobbies: ['reading', 'coding'] }); + }); + + it('should remove value from localStorage', () => { + localStorage.setItem('test-key', JSON.stringify('existing-value')); + + const { result } = renderHook(() => useLocalStorage('test-key', 'initial-value')); + + act(() => { + const [, , removeValue] = result.current; + removeValue(); + }); + + const [value] = result.current; + expect(value).toBe('initial-value'); + expect(localStorage.getItem('test-key')).toBeNull(); + }); + + it('should handle invalid JSON in localStorage', () => { + localStorage.setItem('test-key', 'invalid-json{'); + + const { result } = renderHook(() => useLocalStorage('test-key', 'fallback-value')); + const [value] = result.current; + + expect(value).toBe('fallback-value'); + }); + + it('should be SSR-safe (no window)', () => { + const originalWindow = global.window; + // @ts-ignore - Temporarily remove window for SSR test + delete global.window; + + const { result } = renderHook(() => useLocalStorage('test-key', 'ssr-value')); + const [value] = result.current; + + expect(value).toBe('ssr-value'); + + // Restore window + global.window = originalWindow; + }); + + it('should handle localStorage quota exceeded', () => { + const { handleError } = require('../utils/errorHandling'); + + // Mock setItem to throw quota exceeded error + const originalSetItem = localStorage.setItem; + localStorage.setItem = vi.fn(() => { + throw new DOMException('QuotaExceededError'); + }); + + const { result } = renderHook(() => useLocalStorage('test-key', 'initial')); + + act(() => { + const [, setValue] = result.current; + setValue('new-value'); + }); + + expect(handleError).toHaveBeenCalled(); + + // Restore original setItem + localStorage.setItem = originalSetItem; + }); + + it('should serialize and deserialize arrays', () => { + const initialArray = [1, 2, 3, 4, 5]; + const { result } = renderHook(() => useLocalStorage('test-key', initialArray)); + + act(() => { + const [, setValue] = result.current; + setValue([...initialArray, 6]); + }); + + const [value] = result.current; + expect(value).toEqual([1, 2, 3, 4, 5, 6]); + expect(JSON.parse(localStorage.getItem('test-key')!)).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it('should handle boolean values', () => { + const { result } = renderHook(() => useLocalStorage('test-key', false)); + + act(() => { + const [, setValue] = result.current; + setValue(true); + }); + + const [value] = result.current; + expect(value).toBe(true); + expect(JSON.parse(localStorage.getItem('test-key')!)).toBe(true); + }); + + it('should handle number values', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 0)); + + act(() => { + const [, setValue] = result.current; + setValue(42); + }); + + const [value] = result.current; + expect(value).toBe(42); + expect(JSON.parse(localStorage.getItem('test-key')!)).toBe(42); + }); + + it('should handle null values', () => { + const { result } = renderHook(() => useLocalStorage('test-key', null)); + + const [value] = result.current; + expect(value).toBe(null); + }); + + it('should update localStorage when key changes', () => { + const { result, rerender } = renderHook( + ({ key, value }) => useLocalStorage(key, value), + { initialProps: { key: 'key1', value: 'value1' } } + ); + + act(() => { + const [, setValue] = result.current; + setValue('updated-value1'); + }); + + expect(localStorage.getItem('key1')).toBe(JSON.stringify('updated-value1')); + + // Change the key + rerender({ key: 'key2', value: 'value2' }); + + act(() => { + const [, setValue] = result.current; + setValue('updated-value2'); + }); + + expect(localStorage.getItem('key2')).toBe(JSON.stringify('updated-value2')); + }); +});