@qazuor/react-hooks
Version:
A comprehensive collection of production-ready React hooks for modern web applications. Features type-safe implementations, extensive testing, and zero dependencies. Includes hooks for state management, browser APIs, user interactions, and development uti
216 lines (168 loc) • 7.56 kB
text/typescript
import { type RenderHookResult, act, renderHook } from '@testing-library/react';
import { useLocalStorage } from '../src/hooks/useLocalStorage';
interface TestObject {
name: string;
value: number;
}
describe('useLocalStorage', () => {
let hook: RenderHookResult<readonly [string, (value: string | ((val: string) => string)) => void], unknown>;
const testKey = 'test-key';
const initialValue = 'initial';
const mockError = new Error('Storage error');
beforeEach(() => {
localStorage.clear();
});
afterEach(() => {
localStorage.clear();
jest.restoreAllMocks();
});
it('should initialize with default value when no stored value exists', () => {
const hook = renderHook(() => useLocalStorage(testKey, initialValue));
expect(hook.result.current[0]).toBe(initialValue);
expect(localStorage.getItem(testKey)).toBe(JSON.stringify(initialValue));
});
it('should initialize with stored value when it exists', () => {
const storedValue = 'stored';
localStorage.setItem(testKey, JSON.stringify(storedValue));
const storedHook = renderHook(() => useLocalStorage(testKey, initialValue));
expect(storedHook.result.current[0]).toBe(storedValue);
});
it('should update value and localStorage when setValue is called', () => {
const hook = renderHook(() => useLocalStorage(testKey, initialValue));
const newValue = 'updated';
act(() => {
hook.result.current[1](newValue);
});
expect(hook.result.current[0]).toBe(newValue);
expect(localStorage.getItem(testKey)).toBe(JSON.stringify(newValue));
});
it('should handle function updates correctly', () => {
const hook = renderHook(() => useLocalStorage(testKey, initialValue));
act(() => {
hook.result.current[1]((prev) => `${prev}-updated`);
});
expect(hook.result.current[0]).toBe('initial-updated');
expect(localStorage.getItem(testKey)).toBe(JSON.stringify('initial-updated'));
});
it('should handle complex objects', () => {
const complexHook = renderHook(() => useLocalStorage<TestObject>('complex-key', { name: 'test', value: 42 }));
const updatedValue = { name: 'updated', value: 100 };
act(() => {
complexHook.result.current[1](updatedValue);
});
expect(complexHook.result.current[0]).toEqual(updatedValue);
expect(JSON.parse(localStorage.getItem('complex-key')!)).toEqual(updatedValue);
});
it('should handle errors during initialization', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
localStorage.setItem(testKey, 'invalid json');
const errorHook = renderHook(() => useLocalStorage(testKey, initialValue));
expect(errorHook.result.current[0]).toBe(initialValue);
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});
it('should handle errors during updates', () => {
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw mockError;
});
const errorHandler = jest.fn();
const errorHook = renderHook(() => useLocalStorage(testKey, initialValue, { onError: errorHandler }));
act(() => {
errorHook.result.current[1]('will-fail');
});
expect(errorHandler).toHaveBeenCalledWith(mockError);
setItemSpy.mockRestore();
});
it('should sync across tabs when enabled', () => {
const syncHook = renderHook(() => useLocalStorage(testKey, initialValue, { syncTabs: true }));
const newValue = 'synced-value';
const storageEvent = new StorageEvent('storage', {
key: testKey,
newValue: JSON.stringify('synced-value'),
oldValue: JSON.stringify(initialValue),
storageArea: localStorage
});
act(() => {
window.dispatchEvent(storageEvent);
});
expect(syncHook.result.current[0]).toBe(newValue);
});
it('should use custom serializer and deserializer', () => {
const serializer = (value: string) => `serialized:${value}`;
const deserializer = (value: string) => value.split(':')[1];
const customHook = renderHook(() => useLocalStorage(testKey, initialValue, { serializer, deserializer }));
act(() => {
customHook.result.current[1]('custom');
});
expect(localStorage.getItem(testKey)).toBe('serialized:custom');
expect(customHook.result.current[0]).toBe('custom');
});
it('should ignore storage events for different keys', () => {
const syncHook = renderHook(() => useLocalStorage(testKey, initialValue, { syncTabs: true }));
act(() => {
window.dispatchEvent(
new StorageEvent('storage', {
key: 'different-key',
newValue: JSON.stringify('different-value'),
storageArea: localStorage
})
);
});
expect(syncHook.result.current[0]).toBe(initialValue);
});
it('should handle null values correctly', () => {
const nullHook = renderHook(() => useLocalStorage<string | null>(testKey, null));
expect(nullHook.result.current[0]).toBeNull();
act(() => {
nullHook.result.current[1]('not-null');
});
expect(nullHook.result.current[0]).toBe('not-null');
});
it('should handle invalid JSON during sync', () => {
const errorHandler = jest.fn();
const syncHook = renderHook(() =>
useLocalStorage(testKey, initialValue, { syncTabs: true, onError: errorHandler })
);
act(() => {
window.dispatchEvent(
new StorageEvent('storage', {
key: testKey,
newValue: 'invalid-json',
storageArea: localStorage
})
);
});
expect(errorHandler).toHaveBeenCalled();
expect(syncHook.result.current[0]).toBe(initialValue);
});
it('should handle storage events with null newValue', () => {
const syncHook = renderHook(() => useLocalStorage(testKey, initialValue, { syncTabs: true }));
act(() => {
window.dispatchEvent(
new StorageEvent('storage', {
key: testKey,
newValue: null,
storageArea: localStorage
})
);
});
expect(syncHook.result.current[0]).toBe(initialValue);
});
it('should cleanup event listeners on unmount', () => {
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
const syncHook = renderHook(() => useLocalStorage(testKey, initialValue, { syncTabs: true }));
syncHook.unmount();
expect(removeEventListenerSpy).toHaveBeenCalled();
removeEventListenerSpy.mockRestore();
});
it('should handle rapid state updates', () => {
const hook = renderHook(() => useLocalStorage(testKey, initialValue));
act(() => {
hook.result.current[1]('value1');
hook.result.current[1]('value2');
hook.result.current[1]('value3');
});
expect(hook.result.current[0]).toBe('value3');
expect(localStorage.getItem(testKey)).toBe(JSON.stringify('value3'));
});
});