/** * Tests for utils/helpers.ts */ import { storage, setCookie, getCookie, removeCookie, getToken, setToken, removeToken, formatDate, formatDateTime, formatCurrency, formatPercentage, truncate, capitalize, isValidEmail, isValidPhone, cn, delay, deepClone, generateId, debounce, throttle, } from '../../utils/helpers'; // Mock localStorage const localStorageMock = (() => { let store: Record = {}; return { getItem: jest.fn((key: string) => store[key] || null), setItem: jest.fn((key: string, value: string) => { store[key] = value; }), removeItem: jest.fn((key: string) => { delete store[key]; }), clear: jest.fn(() => { store = {}; }), }; })(); Object.defineProperty(window, 'localStorage', { value: localStorageMock }); // Mock document.cookie let cookieStore: Record = {}; Object.defineProperty(document, 'cookie', { get: () => Object.entries(cookieStore).map(([k, v]) => `${k}=${v}`).join('; '), set: (value: string) => { const [cookiePart] = value.split(';'); const [key, val] = cookiePart.split('='); if (val === '' || value.includes('expires=Thu, 01 Jan 1970')) { delete cookieStore[key]; } else { cookieStore[key] = val; } }, }); describe('Storage Helpers', () => { beforeEach(() => { localStorageMock.clear(); jest.clearAllMocks(); }); describe('storage.get', () => { it('should get item from localStorage', () => { localStorageMock.getItem.mockReturnValueOnce('test-value'); expect(storage.get('test-key')).toBe('test-value'); }); it('should return null for non-existent key', () => { expect(storage.get('non-existent')).toBeNull(); }); it('should return null and log error when localStorage throws', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); localStorageMock.getItem.mockImplementationOnce(() => { throw new Error('Storage error'); }); expect(storage.get('test-key')).toBeNull(); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); }); describe('storage.set', () => { it('should set item in localStorage', () => { storage.set('test-key', 'test-value'); expect(localStorageMock.setItem).toHaveBeenCalledWith('test-key', 'test-value'); }); it('should log error when localStorage throws', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); localStorageMock.setItem.mockImplementationOnce(() => { throw new Error('Storage error'); }); storage.set('test-key', 'test-value'); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); }); describe('storage.remove', () => { it('should remove item from localStorage', () => { storage.remove('test-key'); expect(localStorageMock.removeItem).toHaveBeenCalledWith('test-key'); }); it('should log error when localStorage throws', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); localStorageMock.removeItem.mockImplementationOnce(() => { throw new Error('Storage error'); }); storage.remove('test-key'); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); }); describe('storage.clear', () => { it('should clear localStorage', () => { storage.clear(); expect(localStorageMock.clear).toHaveBeenCalled(); }); it('should log error when localStorage throws', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); localStorageMock.clear.mockImplementationOnce(() => { throw new Error('Storage error'); }); storage.clear(); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); }); }); describe('Cookie Helpers', () => { beforeEach(() => { cookieStore = {}; }); describe('setCookie', () => { it('should set a cookie', () => { setCookie('test-cookie', 'test-value', 7); expect(cookieStore['test-cookie']).toBe('test-value'); }); }); describe('getCookie', () => { it('should get a cookie value', () => { cookieStore['test-cookie'] = 'test-value'; expect(getCookie('test-cookie')).toBe('test-value'); }); it('should return null for non-existent cookie', () => { expect(getCookie('non-existent')).toBeNull(); }); }); describe('removeCookie', () => { it('should remove a cookie', () => { cookieStore['test-cookie'] = 'test-value'; removeCookie('test-cookie'); expect(cookieStore['test-cookie']).toBeUndefined(); }); }); }); describe('Token Helpers', () => { beforeEach(() => { localStorageMock.clear(); cookieStore = {}; jest.clearAllMocks(); }); describe('getToken', () => { it('should get token from localStorage', () => { localStorageMock.getItem.mockReturnValueOnce('stored-token'); expect(getToken()).toBe('stored-token'); }); it('should fall back to cookie if localStorage is empty', () => { localStorageMock.getItem.mockReturnValueOnce(null); cookieStore['auth_token'] = 'cookie-token'; expect(getToken()).toBe('cookie-token'); }); it('should return null if no token exists', () => { localStorageMock.getItem.mockReturnValueOnce(null); expect(getToken()).toBeNull(); }); }); describe('setToken', () => { it('should set token in localStorage and cookie', () => { setToken('my-token'); expect(localStorageMock.setItem).toHaveBeenCalled(); expect(cookieStore['auth_token']).toBe('my-token'); }); }); describe('removeToken', () => { it('should remove token from localStorage and cookie', () => { cookieStore['auth_token'] = 'my-token'; removeToken(); expect(localStorageMock.removeItem).toHaveBeenCalled(); expect(cookieStore['auth_token']).toBeUndefined(); }); }); }); describe('Date Formatting', () => { describe('formatDate', () => { it('should format a valid date', () => { const result = formatDate('2025-01-15'); expect(result).toContain('2025'); }); it('should return dash for empty date', () => { expect(formatDate('')).toBe('-'); }); it('should return dash for invalid date', () => { expect(formatDate('invalid')).toBe('-'); }); it('should format Date object', () => { const result = formatDate(new Date('2025-01-15')); expect(result).toContain('2025'); }); }); describe('formatDateTime', () => { it('should format a valid datetime', () => { const result = formatDateTime('2025-01-15T10:30:00'); expect(result).toContain('2025'); }); it('should return dash for empty date', () => { expect(formatDateTime('')).toBe('-'); }); it('should return dash for invalid date', () => { expect(formatDateTime('invalid')).toBe('-'); }); }); }); describe('Number Formatting', () => { describe('formatCurrency', () => { it('should format currency in EUR', () => { const result = formatCurrency(100); expect(result).toContain('100'); }); it('should handle decimals', () => { const result = formatCurrency(99.99); expect(result).toContain('99'); }); }); describe('formatPercentage', () => { it('should format percentage', () => { const result = formatPercentage(0.75); expect(result).toContain('75'); }); }); }); describe('String Helpers', () => { describe('truncate', () => { it('should truncate long strings', () => { expect(truncate('Hello World', 5)).toBe('Hello...'); }); it('should not truncate short strings', () => { expect(truncate('Hi', 10)).toBe('Hi'); }); it('should handle exact length', () => { expect(truncate('Hello', 5)).toBe('Hello'); }); }); describe('capitalize', () => { it('should capitalize first letter', () => { expect(capitalize('hello')).toBe('Hello'); }); it('should lowercase the rest', () => { expect(capitalize('HELLO')).toBe('Hello'); }); it('should handle single character', () => { expect(capitalize('h')).toBe('H'); }); }); }); describe('Validation Helpers', () => { describe('isValidEmail', () => { it('should return true for valid emails', () => { expect(isValidEmail('test@example.com')).toBe(true); expect(isValidEmail('user.name@domain.fr')).toBe(true); }); it('should return false for invalid emails', () => { expect(isValidEmail('invalid')).toBe(false); expect(isValidEmail('')).toBe(false); }); it('should return false for emails exceeding 254 characters', () => { const longEmail = 'a'.repeat(250) + '@test.com'; expect(isValidEmail(longEmail)).toBe(false); }); }); describe('isValidPhone', () => { it('should return true for valid French phone numbers', () => { expect(isValidPhone('0612345678')).toBe(true); expect(isValidPhone('+33612345678')).toBe(true); }); it('should return false for invalid phone numbers', () => { expect(isValidPhone('123')).toBe(false); expect(isValidPhone('')).toBe(false); }); }); }); describe('Class Name Helper', () => { describe('cn', () => { it('should join class names', () => { expect(cn('class1', 'class2')).toBe('class1 class2'); }); it('should filter out falsy values', () => { expect(cn('class1', false, 'class2', null, undefined)).toBe('class1 class2'); }); it('should handle empty input', () => { expect(cn()).toBe(''); }); }); }); describe('Async Helpers', () => { describe('delay', () => { it('should resolve after specified time', async () => { const start = Date.now(); await delay(50); const elapsed = Date.now() - start; expect(elapsed).toBeGreaterThanOrEqual(40); }); }); }); describe('Object Helpers', () => { describe('deepClone', () => { it('should create a deep copy of an object', () => { const original = { a: 1, b: { c: 2 } }; const cloned = deepClone(original); expect(cloned).toEqual(original); expect(cloned).not.toBe(original); expect(cloned.b).not.toBe(original.b); }); it('should clone arrays', () => { const original = [1, 2, { a: 3 }]; const cloned = deepClone(original); expect(cloned).toEqual(original); expect(cloned).not.toBe(original); }); }); }); describe('ID Generation', () => { describe('generateId', () => { it('should generate unique IDs', () => { const id1 = generateId(); const id2 = generateId(); expect(id1).not.toBe(id2); }); it('should generate non-empty string', () => { const id = generateId(); expect(typeof id).toBe('string'); expect(id.length).toBeGreaterThan(0); }); it('should use window.crypto when available', () => { const id = generateId(); expect(id).toBeDefined(); expect(id.length).toBeGreaterThan(5); }); it('should fallback to globalThis.crypto when window.crypto is unavailable', () => { const originalWindow = global.window; // @ts-ignore delete global.window; const id = generateId(); expect(id).toBeDefined(); expect(typeof id).toBe('string'); global.window = originalWindow; }); it('should fallback to timestamp when no crypto is available', () => { const originalWindow = global.window; const originalGlobalThis = global.globalThis; const originalCrypto = globalThis.crypto; // @ts-ignore delete global.window; // @ts-ignore delete globalThis.crypto; const id = generateId(); expect(id).toBeDefined(); expect(typeof id).toBe('string'); expect(id.length).toBeGreaterThan(0); global.window = originalWindow; globalThis.crypto = originalCrypto; }); }); }); describe('Function Utilities', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); describe('debounce', () => { it('should debounce function calls', () => { const fn = jest.fn(); const debouncedFn = debounce(fn, 100); debouncedFn(); debouncedFn(); debouncedFn(); expect(fn).not.toHaveBeenCalled(); jest.advanceTimersByTime(100); expect(fn).toHaveBeenCalledTimes(1); }); }); describe('throttle', () => { it('should throttle function calls', () => { const fn = jest.fn(); const throttledFn = throttle(fn, 100); throttledFn(); throttledFn(); throttledFn(); expect(fn).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(100); throttledFn(); expect(fn).toHaveBeenCalledTimes(2); }); }); });