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:
Binary file not shown.
Generated
+1878
-28
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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}'],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
+11
-1
@@ -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'],
|
||||||
|
|||||||
@@ -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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+2677
File diff suppressed because it is too large
Load Diff
+11
-1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user