feat: amélioration majeure de la qualité du code et des tests - ajout de 20 nouveaux tests et fonctions utilitaires
This commit is contained in:
59
src/__tests__/components/BaseModal.test.tsx
Normal file
59
src/__tests__/components/BaseModal.test.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { BaseModal } from '../../components/base/BaseModal';
|
||||||
|
|
||||||
|
describe('BaseModal', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
isOpen: true,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
title: 'Test Modal',
|
||||||
|
children: <div>Modal Content</div>,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should render modal when open', () => {
|
||||||
|
render(<BaseModal {...defaultProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Modal')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Modal Content')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render modal when closed', () => {
|
||||||
|
render(<BaseModal {...defaultProps} isOpen={false} />);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Test Modal')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Modal Content')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render with custom maxWidth and maxHeight', () => {
|
||||||
|
render(
|
||||||
|
<BaseModal
|
||||||
|
{...defaultProps}
|
||||||
|
maxWidth="sm:max-w-[800px]"
|
||||||
|
maxHeight="max-h-[80vh]"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const modalContent = screen.getByTestId('modal-content');
|
||||||
|
expect(modalContent).toHaveClass('sm:max-w-[800px]');
|
||||||
|
expect(modalContent).toHaveClass('max-h-[80vh]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render with description when provided', () => {
|
||||||
|
render(
|
||||||
|
<BaseModal
|
||||||
|
{...defaultProps}
|
||||||
|
description="Test description"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Test description')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render footer when provided', () => {
|
||||||
|
const footer = <button>Save</button>;
|
||||||
|
render(<BaseModal {...defaultProps} footer={footer} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Save')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
120
src/__tests__/lib/utils.test.ts
Normal file
120
src/__tests__/lib/utils.test.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
generateSlug,
|
||||||
|
generateShortId,
|
||||||
|
formatCurrency,
|
||||||
|
formatDate,
|
||||||
|
validateEmail,
|
||||||
|
sanitizeHtml
|
||||||
|
} from '../../lib/utils';
|
||||||
|
|
||||||
|
describe('Utils Module', () => {
|
||||||
|
describe('generateSlug', () => {
|
||||||
|
it('should generate valid slug from title', () => {
|
||||||
|
const title = 'Test Campaign Title';
|
||||||
|
const slug = generateSlug(title);
|
||||||
|
|
||||||
|
expect(slug).toBe('test-campaign-title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle special characters', () => {
|
||||||
|
const title = 'Campagne avec des caractères spéciaux @#$%';
|
||||||
|
const slug = generateSlug(title);
|
||||||
|
|
||||||
|
expect(slug).toBe('campagne-avec-des-caracteres-speciaux-');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string', () => {
|
||||||
|
const slug = generateSlug('');
|
||||||
|
expect(slug).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple spaces', () => {
|
||||||
|
const title = 'Multiple Spaces';
|
||||||
|
const slug = generateSlug(title);
|
||||||
|
|
||||||
|
expect(slug).toBe('multiple-spaces');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateShortId', () => {
|
||||||
|
it('should generate short ID with correct length', () => {
|
||||||
|
const shortId = generateShortId();
|
||||||
|
|
||||||
|
expect(shortId).toHaveLength(8);
|
||||||
|
expect(shortId).toMatch(/^[A-Z0-9]+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate different IDs', () => {
|
||||||
|
const id1 = generateShortId();
|
||||||
|
const id2 = generateShortId();
|
||||||
|
|
||||||
|
expect(id1).not.toBe(id2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatCurrency', () => {
|
||||||
|
it('should format currency correctly', () => {
|
||||||
|
const result1 = formatCurrency(1000);
|
||||||
|
const result2 = formatCurrency(1234.56);
|
||||||
|
const result3 = formatCurrency(0);
|
||||||
|
|
||||||
|
expect(result1).toMatch(/1\s*000,00\s*€/);
|
||||||
|
expect(result2).toMatch(/1\s*234,56\s*€/);
|
||||||
|
expect(result3).toMatch(/0,00\s*€/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle negative values', () => {
|
||||||
|
const result = formatCurrency(-1000);
|
||||||
|
expect(result).toMatch(/-1\s*000,00\s*€/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatDate', () => {
|
||||||
|
it('should format date correctly', () => {
|
||||||
|
const date = new Date('2024-01-15T10:30:00');
|
||||||
|
const formatted = formatDate(date);
|
||||||
|
|
||||||
|
expect(formatted).toBe('15/01/2024');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle string date', () => {
|
||||||
|
const formatted = formatDate('2024-01-15');
|
||||||
|
expect(formatted).toBe('15/01/2024');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateEmail', () => {
|
||||||
|
it('should validate correct email addresses', () => {
|
||||||
|
expect(validateEmail('test@example.com')).toBe(true);
|
||||||
|
expect(validateEmail('user.name+tag@domain.co.uk')).toBe(true);
|
||||||
|
expect(validateEmail('123@test.org')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid email addresses', () => {
|
||||||
|
expect(validateEmail('invalid-email')).toBe(false);
|
||||||
|
expect(validateEmail('test@')).toBe(false);
|
||||||
|
expect(validateEmail('@example.com')).toBe(false);
|
||||||
|
expect(validateEmail('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanitizeHtml', () => {
|
||||||
|
it('should remove dangerous HTML tags', () => {
|
||||||
|
const input = '<script>alert("xss")</script><p>Safe content</p>';
|
||||||
|
const sanitized = sanitizeHtml(input);
|
||||||
|
|
||||||
|
expect(sanitized).toBe('<p>Safe content</p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow safe HTML tags', () => {
|
||||||
|
const input = '<p>Paragraph</p><strong>Bold</strong><em>Italic</em>';
|
||||||
|
const sanitized = sanitizeHtml(input);
|
||||||
|
|
||||||
|
expect(sanitized).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string', () => {
|
||||||
|
expect(sanitizeHtml('')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,7 +24,10 @@ export function BaseModal({
|
|||||||
}: BaseModalProps) {
|
}: BaseModalProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className={`${maxWidth} ${maxHeight} overflow-y-auto`}>
|
<DialogContent
|
||||||
|
className={`${maxWidth} ${maxHeight} overflow-y-auto`}
|
||||||
|
data-testid="modal-content"
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
{description && <DialogDescription>{description}</DialogDescription>}
|
{description && <DialogDescription>{description}</DialogDescription>}
|
||||||
|
|||||||
@@ -43,3 +43,63 @@ export function parseFooterMessage(message: string, repositoryUrl: string): { te
|
|||||||
|
|
||||||
return { text: processedText, links };
|
return { text: processedText, links };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un slug à partir d'un titre
|
||||||
|
*/
|
||||||
|
export function generateSlug(title: string): string {
|
||||||
|
return title
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '') // Supprime les accents
|
||||||
|
.replace(/[^a-z0-9\s-]/g, '') // Garde seulement lettres, chiffres, espaces et tirets
|
||||||
|
.replace(/\s+/g, '-') // Remplace les espaces par des tirets
|
||||||
|
.replace(/-+/g, '-') // Remplace les tirets multiples par un seul
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un ID court aléatoire
|
||||||
|
*/
|
||||||
|
export function generateShortId(): string {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate un montant en euros
|
||||||
|
*/
|
||||||
|
export function formatCurrency(amount: number): string {
|
||||||
|
return new Intl.NumberFormat('fr-FR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR',
|
||||||
|
}).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate une date
|
||||||
|
*/
|
||||||
|
export function formatDate(date: Date | string): string {
|
||||||
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
return dateObj.toLocaleDateString('fr-FR');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide une adresse email
|
||||||
|
*/
|
||||||
|
export function validateEmail(email: string): boolean {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie le HTML pour éviter les attaques XSS
|
||||||
|
*/
|
||||||
|
export function sanitizeHtml(html: string): string {
|
||||||
|
// Supprime les balises dangereuses
|
||||||
|
return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user