test: ajoute des tests automatisés frontend (Vitest) et mobile (Jest)

- Frontend Next.js : Vitest + Testing Library
  - Button (rendu, clic, état loading)
  - auth-store (setAuth, déconnexion, persistance token)
  - script `npm test`, config jsdom + alias @/
- Mobile Expo : Jest (preset jest-expo)
  - getApiError (erreur Axios, validation, erreur générique, sans réponse)
  - script `npm test`, transformIgnorePatterns adapté
- Cahier de tests : section « Outils de test automatisés » (PHPUnit + Vitest + Jest)
  et entrée 6.4 (PV de recette) au sommaire

Suites vertes : backend 15, frontend 6, mobile 4. tsc/lint/build OK, expo-doctor 18/18.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Azmog
2026-06-18 22:17:20 +02:00
parent 8904e6127a
commit 0e07f5a57e
11 changed files with 4713 additions and 32 deletions
Binary file not shown.
+1878 -28
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -6,7 +6,8 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint",
"test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
@@ -22,12 +23,18 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"jsdom": "^29.1.1",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5",
"vitest": "^4.1.9"
} }
} }
+30
View File
@@ -0,0 +1,30 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '@/components/ui/Button';
describe('Button', () => {
it('affiche son libellé', () => {
render(<Button>Enregistrer</Button>);
expect(screen.getByRole('button', { name: 'Enregistrer' })).toBeInTheDocument();
});
it('déclenche onClick au clic', () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Valider</Button>);
fireEvent.click(screen.getByRole('button', { name: 'Valider' }));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('est désactivé pendant le chargement et ne déclenche pas onClick', () => {
const onClick = vi.fn();
render(
<Button loading onClick={onClick}>
Envoi
</Button>
);
const btn = screen.getByRole('button');
expect(btn).toBeDisabled();
fireEvent.click(btn);
expect(onClick).not.toHaveBeenCalled();
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { useAuthStore } from '@/lib/auth-store';
import type { Utilisateur } from '@/types';
const fakeUser = {
id: 1,
nom: 'Martin',
prenom: 'Claire',
email: 'claire@email.fr',
role: { id: 2, nom: 'membre' },
} as unknown as Utilisateur;
describe('auth-store', () => {
beforeEach(() => {
localStorage.clear();
useAuthStore.setState({ user: null, token: null, isAuthenticated: false });
});
it('démarre déconnecté', () => {
const state = useAuthStore.getState();
expect(state.isAuthenticated).toBe(false);
expect(state.token).toBeNull();
});
it('setAuth stocke le token et authentifie l\'utilisateur', () => {
useAuthStore.getState().setAuth(fakeUser, 'jwt-123');
const state = useAuthStore.getState();
expect(state.isAuthenticated).toBe(true);
expect(state.token).toBe('jwt-123');
expect(state.user?.email).toBe('claire@email.fr');
expect(localStorage.getItem('cesizen_token')).toBe('jwt-123');
});
it('setAuth(null) déconnecte et purge le token', () => {
useAuthStore.getState().setAuth(fakeUser, 'jwt-123');
useAuthStore.getState().setAuth(null, null);
const state = useAuthStore.getState();
expect(state.isAuthenticated).toBe(false);
expect(state.token).toBeNull();
expect(localStorage.getItem('cesizen_token')).toBeNull();
});
});
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'node:path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
},
});
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
+11 -1
View File
@@ -209,6 +209,7 @@ def generate_pdf():
(' 6.1', 'Plan de validation', 12), (' 6.1', 'Plan de validation', 12),
(' 6.2', 'Checklist de validation', 13), (' 6.2', 'Checklist de validation', 13),
(' 6.3', 'Critères d\'acceptation', 13), (' 6.3', 'Critères d\'acceptation', 13),
(' 6.4', 'Modèle de procès-verbal de recette', 14),
] ]
pdf.set_font('Helvetica', '', 10) pdf.set_font('Helvetica', '', 10)
for num, title, page in toc: for num, title, page in toc:
@@ -247,7 +248,16 @@ def generate_pdf():
[45, 25, 85, 35] [45, 25, 85, 35]
) )
pdf.ln(5) pdf.ln(2)
pdf.subsection_title('Outils de test automatisés')
pdf.body_text(
'Les tests sont automatisés sur les trois briques du projet et exécutables en une commande :'
)
pdf.bullet('Backend (API Laravel) : PHPUnit - 15 tests (authentification, tracker, admin, RGPD). Commande : php artisan test.')
pdf.bullet('Frontend Web (Next.js) : Vitest + Testing Library - composants UI et store d\'authentification. Commande : npm test.')
pdf.bullet('Mobile (React Native / Expo) : Jest (preset jest-expo) - utilitaires d\'appel API. Commande : npm test.')
pdf.ln(4)
pdf.section_title('1.2 Tests fonctionnels - Authentification') pdf.section_title('1.2 Tests fonctionnels - Authentification')
pdf.colored_table( pdf.colored_table(
['ID', 'Cas de test', 'Entrée', 'Résultat attendu', 'Statut', 'Responsable'], ['ID', 'Cas de test', 'Entrée', 'Résultat attendu', 'Statut', 'Responsable'],
+36
View File
@@ -0,0 +1,36 @@
import axios from 'axios';
import { getApiError } from '../lib/api';
// expo-secure-store touche un module natif : on le mocke pour les tests unitaires.
jest.mock('expo-secure-store', () => ({
getItemAsync: jest.fn(),
setItemAsync: jest.fn(),
deleteItemAsync: jest.fn(),
}));
describe('getApiError', () => {
it("extrait le message d'une erreur Axios", () => {
const err = new axios.AxiosError('échec');
err.response = {
data: { message: 'Identifiants incorrects' },
} as never;
expect(getApiError(err).message).toBe('Identifiants incorrects');
});
it('extrait les erreurs de validation par champ', () => {
const err = new axios.AxiosError('422');
err.response = {
data: { errors: { email: ['Cet email est déjà utilisé'] } },
} as never;
expect(getApiError(err).errors?.email).toEqual(['Cet email est déjà utilisé']);
});
it('retourne un objet vide pour une erreur non-Axios', () => {
expect(getApiError(new Error('boom'))).toEqual({});
});
it('retourne un objet vide quand la réponse Axios est absente', () => {
const err = new axios.AxiosError('réseau');
expect(getApiError(err)).toEqual({});
});
});
+2677
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -6,7 +6,14 @@
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web" "web": "expo start --web",
"test": "jest"
},
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|axios))"
]
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.0.0", "@expo/vector-icons": "^14.0.0",
@@ -30,7 +37,10 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.0", "@babel/core": "^7.25.0",
"@types/jest": "^30.0.0",
"@types/react": "~19.0.0", "@types/react": "~19.0.0",
"jest": "^29.7.0",
"jest-expo": "~53.0.0",
"typescript": "~5.8.0" "typescript": "~5.8.0"
}, },
"private": true "private": true