From 74189ac037b70aa40d4271cc61114c81bb09c153 Mon Sep 17 00:00:00 2001 From: Yannick Le Duc Date: Thu, 28 Aug 2025 20:53:53 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20am=C3=A9lioration=20majeure=20de=20la?= =?UTF-8?q?=20qualit=C3=A9=20du=20code=20et=20des=20tests=20-=20ajout=20de?= =?UTF-8?q?=2020=20nouveaux=20tests=20et=20fonctions=20utilitaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/__tests__/components/BaseModal.test.tsx | 59 ++++++++++ src/__tests__/lib/utils.test.ts | 120 ++++++++++++++++++++ src/components/base/BaseModal.tsx | 5 +- src/lib/utils.ts | 60 ++++++++++ 4 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/components/BaseModal.test.tsx create mode 100644 src/__tests__/lib/utils.test.ts diff --git a/src/__tests__/components/BaseModal.test.tsx b/src/__tests__/components/BaseModal.test.tsx new file mode 100644 index 0000000..714940c --- /dev/null +++ b/src/__tests__/components/BaseModal.test.tsx @@ -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:
Modal Content
, + }; + + it('should render modal when open', () => { + render(); + + expect(screen.getByText('Test Modal')).toBeInTheDocument(); + expect(screen.getByText('Modal Content')).toBeInTheDocument(); + }); + + it('should not render modal when closed', () => { + render(); + + expect(screen.queryByText('Test Modal')).not.toBeInTheDocument(); + expect(screen.queryByText('Modal Content')).not.toBeInTheDocument(); + }); + + it('should render with custom maxWidth and maxHeight', () => { + render( + + ); + + 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( + + ); + + expect(screen.getByText('Test description')).toBeInTheDocument(); + }); + + it('should render footer when provided', () => { + const footer = ; + render(); + + expect(screen.getByText('Save')).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/lib/utils.test.ts b/src/__tests__/lib/utils.test.ts new file mode 100644 index 0000000..373abff --- /dev/null +++ b/src/__tests__/lib/utils.test.ts @@ -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 = '

Safe content

'; + const sanitized = sanitizeHtml(input); + + expect(sanitized).toBe('

Safe content

'); + }); + + it('should allow safe HTML tags', () => { + const input = '

Paragraph

BoldItalic'; + const sanitized = sanitizeHtml(input); + + expect(sanitized).toBe(input); + }); + + it('should handle empty string', () => { + expect(sanitizeHtml('')).toBe(''); + }); + }); +}); diff --git a/src/components/base/BaseModal.tsx b/src/components/base/BaseModal.tsx index 0d59e77..286c875 100644 --- a/src/components/base/BaseModal.tsx +++ b/src/components/base/BaseModal.tsx @@ -24,7 +24,10 @@ export function BaseModal({ }: BaseModalProps) { return ( - + {title} {description && {description}} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 43ccc09..b25b6bf 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -43,3 +43,63 @@ export function parseFooterMessage(message: string, repositoryUrl: string): { te 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>/gi, ''); +}