From af464cfcb766d728640ac30ecf95e16a9db44585 Mon Sep 17 00:00:00 2001 From: Anthony Coyaud Date: Sat, 15 Aug 2026 15:02:00 +0200 Subject: [PATCH] =?UTF-8?q?test(front):=20=C3=A9tend=20la=20couverture=20d?= =?UTF-8?q?e=208=20=C3=A0=2041=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les fonctionnalités ajoutées récemment n'étaient couvertes par aucun test, et deux utilitaires historiques ne l'avaient jamais été. - statutMonitoring : bornes du seuil testées de part et d'autre, là où une confusion entre < et <= se verrait. - dates : protège la correction du décalage de fuseau (toISOString basculait en UTC, le 1er janvier local devenait le 31 décembre). Les helpers sont sortis de Historique.jsx vers utils/dates.js, un fichier de composant ne pouvant pas exporter autre chose sans casser le rafraîchissement à chaud. - exportToCSV : échappement des virgules, guillemets et sauts de ligne, et conservation du zéro, qui est falsy alors que « 0 erreur » est justement l'information utile. Cet utilitaire sert quatre écrans et n'avait aucun test. - Dashboard : les menus ne proposent que les valeurs ayant des résultats, les catégories se restreignent au service choisi, et une catégorie devenue sans résultat est réinitialisée. Vérifié par mutation : casser la cascade fait bien échouer deux tests. - Admin : création, modification pré-remplie, confirmation avant suppression, remontée du refus 409 de l'API, désactivation et non suppression d'un monitoring, affichage des libellés à la place des identifiants. Corrige un défaut révélé par ces tests : referentielsConfig capturait les fonctions de l'API à l'import, figeant l'implémentation. Elles sont désormais résolues à l'appel, ce qui découple la configuration du module d'API. --- DataSentinel/src/__tests__/Admin.test.jsx | 174 ++++++++++++++++++ DataSentinel/src/__tests__/Dashboard.test.jsx | 108 +++++++++++ DataSentinel/src/__tests__/dates.test.js | 50 +++++ .../src/__tests__/exportToCSV.test.js | 83 +++++++++ .../src/__tests__/statutMonitoring.test.js | 33 ++++ .../src/pages/Admin/referentielsConfig.js | 34 ++-- .../src/pages/Historique/Historique.jsx | 20 +- DataSentinel/src/utils/dates.js | 17 ++ 8 files changed, 482 insertions(+), 37 deletions(-) create mode 100644 DataSentinel/src/__tests__/Admin.test.jsx create mode 100644 DataSentinel/src/__tests__/Dashboard.test.jsx create mode 100644 DataSentinel/src/__tests__/dates.test.js create mode 100644 DataSentinel/src/__tests__/exportToCSV.test.js create mode 100644 DataSentinel/src/__tests__/statutMonitoring.test.js create mode 100644 DataSentinel/src/utils/dates.js diff --git a/DataSentinel/src/__tests__/Admin.test.jsx b/DataSentinel/src/__tests__/Admin.test.jsx new file mode 100644 index 0000000..b8865b8 --- /dev/null +++ b/DataSentinel/src/__tests__/Admin.test.jsx @@ -0,0 +1,174 @@ +/* Écran d'administration des référentiels. + + Avant correction, les boutons Modifier / Désactiver n'appelaient rien + (console.log) et « Enregistrer » se contentait de fermer la modale. Ces + tests vérifient que chaque action atteint réellement l'API et que les + erreurs métier remontent à l'utilisateur. */ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Admin from '../pages/Admin/Admin'; +import * as api from '../services/api'; + +const SERVICES = [ + { id_service: 1, nom_service: 'Contrat' }, + { id_service: 2, nom_service: 'Fournisseur' }, +]; +const CATEGORIES = [{ id_categorie: 1, intitule_categorie: 'Tiers-payeurs' }]; +const MONITORINGS = [{ + id_monito: 1, monito_intitule: 'Erreurs tiers payeurs', id_service: 1, + id_categorie: 1, table_source: 'MONITO_TIERS_PAYEURS', bdd_source: 'Sage', +}]; + +function stubLectures() { + vi.spyOn(api, 'getServices').mockResolvedValue(SERVICES); + vi.spyOn(api, 'getCategories').mockResolvedValue(CATEGORIES); + vi.spyOn(api, 'getContacts').mockResolvedValue([]); + vi.spyOn(api, 'getMonitorings').mockResolvedValue(MONITORINGS); +} + +async function ouvrirOnglet(utilisateur, nom) { + await utilisateur.click(screen.getByRole('button', { name: nom })); +} + +describe('Admin — CRUD des référentiels', () => { + beforeEach(() => { + vi.restoreAllMocks(); + stubLectures(); + vi.spyOn(window, 'confirm').mockReturnValue(true); + }); + + it('crée un service via l’API et recharge la liste', async () => { + const creation = vi.spyOn(api, 'createService').mockResolvedValue({ id_service: 3 }); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + await utilisateur.click(screen.getByRole('button', { name: /Ajouter/ })); + + await utilisateur.type(screen.getByLabelText('Nom du service'), 'Comptabilité'); + await utilisateur.click(screen.getByRole('button', { name: 'Enregistrer' })); + + await waitFor(() => { + expect(creation).toHaveBeenCalledWith({ nom_service: 'Comptabilité' }); + }); + /* Rechargement après écriture : la liste doit être redemandée. */ + expect(api.getServices).toHaveBeenCalledTimes(2); + }); + + it('pré-remplit le formulaire de modification avec la ligne choisie', async () => { + vi.spyOn(api, 'updateService').mockResolvedValue({}); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + + const ligne = screen.getByText('Fournisseur').closest('tr'); + await utilisateur.click(within(ligne).getByRole('button', { name: /Modifier/ })); + + expect(screen.getByLabelText('Nom du service')).toHaveValue('Fournisseur'); + }); + + it('envoie l’identifiant de la ligne lors d’une modification', async () => { + const modification = vi.spyOn(api, 'updateService').mockResolvedValue({}); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + + const ligne = screen.getByText('Fournisseur').closest('tr'); + await utilisateur.click(within(ligne).getByRole('button', { name: /Modifier/ })); + await utilisateur.click(screen.getByRole('button', { name: 'Enregistrer' })); + + await waitFor(() => { + expect(modification).toHaveBeenCalledWith(2, { nom_service: 'Fournisseur' }); + }); + }); + + it('demande confirmation avant de supprimer, et n’appelle rien si on refuse', async () => { + window.confirm.mockReturnValue(false); + const suppression = vi.spyOn(api, 'deleteService').mockResolvedValue({}); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + + const ligne = screen.getByText('Contrat').closest('tr'); + await utilisateur.click(within(ligne).getByRole('button', { name: /Supprimer/ })); + + expect(window.confirm).toHaveBeenCalled(); + expect(suppression).not.toHaveBeenCalled(); + }); + + it('affiche le refus de l’API quand des enregistrements sont rattachés', async () => { + /* Cas réel : l'API renvoie 409 avec le décompte des rattachements. */ + vi.spyOn(api, 'deleteService').mockRejectedValue( + new Error('Erreur 409: Suppression impossible : 5 monitoring(s) rattachés.'), + ); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + await ouvrirOnglet(utilisateur, 'Services'); + + const ligne = screen.getByText('Contrat').closest('tr'); + await utilisateur.click(within(ligne).getByRole('button', { name: /Supprimer/ })); + + expect(await screen.findByText(/5 monitoring\(s\) rattachés/)).toBeInTheDocument(); + }); + + it('parle de désactivation, pas de suppression, pour un monitoring', async () => { + /* TABLE_FINAL référence le monitoring : il n'est jamais supprimé. */ + const desactivation = vi.spyOn(api, 'desactiverMonitoring').mockResolvedValue({}); + const utilisateur = userEvent.setup(); + render(); + + await screen.findByText('Administration'); + + const ligne = screen.getByText('Erreurs tiers payeurs').closest('tr'); + expect(within(ligne).getByRole('button', { name: /Désactiver/ })).toBeInTheDocument(); + + await utilisateur.click(within(ligne).getByRole('button', { name: /Désactiver/ })); + await waitFor(() => expect(desactivation).toHaveBeenCalledWith(1)); + }); + + it('affiche le libellé du service plutôt que son identifiant brut', async () => { + render(); + await screen.findByText('Administration'); + + const ligne = screen.getByText('Erreurs tiers payeurs').closest('tr'); + /* id_service = 1 doit s'afficher « Contrat ». */ + expect(within(ligne).getByText('Contrat')).toBeInTheDocument(); + }); + + it('convertit en entier les identifiants issus des listes déroulantes', async () => { + /* Un (YYYY-MM-DD). - On lit les composantes en heure locale plutôt que d'utiliser toISOString(), - qui bascule en UTC : un 1er janvier saisi depuis Paris (UTC+1) y devient le - 31 décembre de l'année précédente. */ -function formatDateISO(date) { - const mois = String(date.getMonth() + 1).padStart(2, '0'); - const jour = String(date.getDate()).padStart(2, '0'); - return `${date.getFullYear()}-${mois}-${jour}`; -} - -/* Période affichée par défaut : du 1er janvier de l'année courante à aujourd'hui. */ -function periodeParDefaut() { - const aujourdhui = new Date(); - return { - debut: formatDateISO(new Date(aujourdhui.getFullYear(), 0, 1)), - fin: formatDateISO(aujourdhui), - }; -} - /* Récupère le tableau d'historique et la série du graphique pour les filtres donnés. Sans monitoring sélectionné, on prend la courbe globale. Fonction pure : elle ne touche à aucun état, ce qui permet de l'appeler diff --git a/DataSentinel/src/utils/dates.js b/DataSentinel/src/utils/dates.js new file mode 100644 index 0000000..8e9d12f --- /dev/null +++ b/DataSentinel/src/utils/dates.js @@ -0,0 +1,17 @@ +/* Formate une date au format attendu par (YYYY-MM-DD). + On lit les composantes en heure locale plutôt que d'utiliser toISOString(), + qui bascule en UTC : un 1er janvier saisi depuis Paris (UTC+1) y devient le + 31 décembre de l'année précédente. */ +export function formatDateISO(date) { + const mois = String(date.getMonth() + 1).padStart(2, '0'); + const jour = String(date.getDate()).padStart(2, '0'); + return `${date.getFullYear()}-${mois}-${jour}`; +} + +/* Période affichée par défaut : du 1er janvier de l'année courante à aujourd'hui. */ +export function periodeParDefaut(aujourdhui = new Date()) { + return { + debut: formatDateISO(new Date(aujourdhui.getFullYear(), 0, 1)), + fin: formatDateISO(aujourdhui), + }; +}