Files
rxminder/hooks/__tests__/useLocalStorage.test.ts
William Valentin 2556250f2c feat: add comprehensive test coverage and fix lint issues
- Add comprehensive tests for MailgunService (439 lines)
  * Email sending functionality with template generation
  * Configuration status validation
  * Error handling and edge cases
  * Mock setup for fetch API and FormData

- Add DatabaseService tests (451 lines)
  * Strategy pattern testing (Mock vs Production)
  * All CRUD operations for users, medications, settings
  * Legacy compatibility method testing
  * Proper TypeScript typing

- Add MockDatabaseStrategy tests (434 lines)
  * Complete coverage of mock database implementation
  * User operations, medication management
  * Settings and custom reminders functionality
  * Data persistence and error handling

- Add React hooks tests
  * useLocalStorage hook with comprehensive edge cases (340 lines)
  * useSettings hook with fetch operations and error handling (78 lines)

- Fix auth integration tests
  * Update mocking to use new database service instead of legacy couchdb.factory
  * Fix service variable references and expectations

- Simplify mailgun config tests
  * Remove redundant edge case testing
  * Focus on core functionality validation

- Fix all TypeScript and ESLint issues
  * Proper FormData mock typing
  * Correct database entity type usage
  * Remove non-existent property references

Test Results:
- 184 total tests passing
- Comprehensive coverage of core services
- Zero TypeScript compilation errors
- Full ESLint compliance
2025-09-08 10:13:50 -07:00

379 lines
11 KiB
TypeScript

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<string | null>('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<string | undefined>('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();
});
});
});