test: add unit tests for utils/helpers.ts with 87% coverage
- Install Jest and testing dependencies - Configure Jest with coverage reporting (lcov for SonarQube) - Add comprehensive tests for all helper functions - Update package.json test script to run with coverage Test coverage: - 42 tests passing - 87.87% line coverage on helpers.ts - 84.78% statement coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
de643c17d0
commit
a31999a037
378
__tests__/utils/helpers.test.ts
Normal file
378
__tests__/utils/helpers.test.ts
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
/**
|
||||||
|
* Tests for utils/helpers.ts
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
storage,
|
||||||
|
setCookie,
|
||||||
|
getCookie,
|
||||||
|
removeCookie,
|
||||||
|
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<string, string> = {};
|
||||||
|
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<string, string> = {};
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('storage.set', () => {
|
||||||
|
it('should set item in localStorage', () => {
|
||||||
|
storage.set('test-key', 'test-value');
|
||||||
|
expect(localStorageMock.setItem).toHaveBeenCalledWith('test-key', 'test-value');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('storage.remove', () => {
|
||||||
|
it('should remove item from localStorage', () => {
|
||||||
|
storage.remove('test-key');
|
||||||
|
expect(localStorageMock.removeItem).toHaveBeenCalledWith('test-key');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('storage.clear', () => {
|
||||||
|
it('should clear localStorage', () => {
|
||||||
|
storage.clear();
|
||||||
|
expect(localStorageMock.clear).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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('setToken', () => {
|
||||||
|
it('should set token in localStorage and cookie', () => {
|
||||||
|
setToken('my-token');
|
||||||
|
expect(localStorageMock.setItem).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeToken', () => {
|
||||||
|
it('should remove token from localStorage and cookie', () => {
|
||||||
|
removeToken();
|
||||||
|
expect(localStorageMock.removeItem).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -28,6 +28,8 @@ const customJestConfig = {
|
||||||
'!**/.next/**',
|
'!**/.next/**',
|
||||||
'!**/coverage/**',
|
'!**/coverage/**',
|
||||||
],
|
],
|
||||||
|
coverageReporters: ['text', 'lcov', 'html'],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
};
|
};
|
||||||
|
|
||||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
|
|
||||||
4247
package-lock.json
generated
4247
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,7 @@
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "echo \"No tests configured\" && exit 0",
|
"test": "jest --coverage",
|
||||||
"sonar": "sonar-scanner"
|
"sonar": "sonar-scanner"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -28,14 +28,20 @@
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "24.9.2",
|
"@types/node": "24.9.2",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-next": "14.2.4",
|
"eslint-config-next": "14.2.4",
|
||||||
|
"jest": "^30.2.0",
|
||||||
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
|
"ts-jest": "^29.4.5",
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user