/** * Tests unitaires pour les routers catalogue et planFormation * Ces tests vérifient la logique de scoring des suggestions et les validations de base. */ import { describe, it, expect } from "vitest"; // ─── Tests logique de scoring des suggestions ───────────────────────────────── describe("Catalogue - logique de suggestion", () => { // Reproduire la logique de scoring du router catalogue function scoreFormation( f: { intitule: string; theme?: string | null; motsCles?: string | null; description?: string | null; validateeItinova: boolean }, searchTerms: string[] ): number { let score = 0; const haystack = [ f.intitule, f.theme ?? "", f.motsCles ?? "", f.description ?? "", ].join(" ").toLowerCase(); for (const term of searchTerms) { if (haystack.includes(term)) score += 2; if (term.length >= 4) { const words = haystack.split(/\s+/); for (const word of words) { if (word.startsWith(term.substring(0, 4))) score += 1; } } } if (f.validateeItinova) score += 5; return score; } it("doit donner un score plus élevé aux formations validées Itinova", () => { const f1 = { intitule: "Gestes et postures", theme: "Sécurité", motsCles: null, description: null, validateeItinova: true }; const f2 = { intitule: "Gestes et postures", theme: "Sécurité", motsCles: null, description: null, validateeItinova: false }; const terms = ["gestes"]; expect(scoreFormation(f1, terms)).toBeGreaterThan(scoreFormation(f2, terms)); }); it("doit retourner 0 pour une formation sans correspondance", () => { const f = { intitule: "Formation cuisine", theme: "Alimentation", motsCles: null, description: null, validateeItinova: false }; const terms = ["sécurité", "incendie"]; expect(scoreFormation(f, terms)).toBe(0); }); it("doit scorer les correspondances partielles (4+ caractères)", () => { const f = { intitule: "Prévention des risques", theme: null, motsCles: null, description: null, validateeItinova: false }; const terms = ["préven"]; const score = scoreFormation(f, terms); expect(score).toBeGreaterThan(0); }); it("doit scorer les correspondances dans les mots-clés", () => { const f = { intitule: "Formation X", theme: null, motsCles: "manutention, ergonomie, postures", description: null, validateeItinova: false }; const terms = ["manutention"]; expect(scoreFormation(f, terms)).toBeGreaterThan(0); }); it("doit trier par score décroissant", () => { const formations = [ { intitule: "Sécurité incendie", theme: "Sécurité", motsCles: "incendie, évacuation", description: null, validateeItinova: false }, { intitule: "Gestes et postures", theme: "Sécurité", motsCles: null, description: null, validateeItinova: true }, { intitule: "Cuisine équilibrée", theme: "Alimentation", motsCles: null, description: null, validateeItinova: false }, ]; const terms = ["sécurité"]; const scored = formations .map(f => ({ ...f, _score: scoreFormation(f, terms) })) .filter(f => f._score > 0) .sort((a, b) => b._score - a._score); expect(scored.length).toBe(2); expect(scored[0]._score).toBeGreaterThanOrEqual(scored[1]._score); }); }); // ─── Tests validation des données de plan ──────────────────────────────────── describe("PlanFormation - validation des données", () => { it("doit valider qu'une année est dans une plage raisonnable", () => { const anneeMin = 2020; const anneeMax = 2100; const validAnnees = [2024, 2025, 2026, 2030]; const invalidAnnees = [1999, 2101, 0, -1]; validAnnees.forEach(a => { expect(a >= anneeMin && a <= anneeMax).toBe(true); }); invalidAnnees.forEach(a => { expect(a >= anneeMin && a <= anneeMax).toBe(false); }); }); it("doit valider les statuts possibles d'un plan", () => { const statutsValides = ["brouillon", "soumis", "valide", "rejete"]; expect(statutsValides).toContain("brouillon"); expect(statutsValides).toContain("soumis"); expect(statutsValides).not.toContain("archive"); expect(statutsValides).not.toContain("en_cours"); }); it("doit valider les statuts possibles d'un item", () => { const statutsValides = ["en_attente", "valide", "refuse", "suggere"]; expect(statutsValides).toContain("en_attente"); expect(statutsValides).toContain("suggere"); expect(statutsValides).not.toContain("brouillon"); }); it("doit valider les priorités (1=haute, 2=moyenne, 3=basse)", () => { const prioritesValides = [1, 2, 3]; expect(prioritesValides).toContain(1); expect(prioritesValides).toContain(2); expect(prioritesValides).toContain(3); expect(prioritesValides).not.toContain(0); expect(prioritesValides).not.toContain(4); }); }); // ─── Tests parsing CSV ──────────────────────────────────────────────────────── describe("CatalogueFormation - parsing CSV", () => { function parseCsvLine(line: string): string[] { const result: string[] = []; let current = ""; let inQuotes = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (ch === '"') { inQuotes = !inQuotes; } else if ((ch === "," || ch === ";") && !inQuotes) { result.push(current.trim()); current = ""; } else { current += ch; } } result.push(current.trim()); return result; } it("doit parser une ligne CSV simple avec virgule", () => { const line = "Formation A,Sécurité,2 jours,Organisme X"; const result = parseCsvLine(line); expect(result).toHaveLength(4); expect(result[0]).toBe("Formation A"); expect(result[1]).toBe("Sécurité"); }); it("doit parser une ligne CSV avec point-virgule", () => { const line = "Formation B;Soins;1 jour;Organisme Y"; const result = parseCsvLine(line); expect(result).toHaveLength(4); expect(result[0]).toBe("Formation B"); }); it("doit gérer les valeurs entre guillemets", () => { const line = '"Formation, avec virgule",Thème,3 jours'; const result = parseCsvLine(line); expect(result[0]).toBe("Formation, avec virgule"); expect(result[1]).toBe("Thème"); }); it("doit détecter l'éligibilité OPCO", () => { const opcoValues = ["oui", "true", "1", "x", "OUI", "TRUE"]; const nonOpcoValues = ["non", "false", "0", "", "no"]; opcoValues.forEach(v => { expect(["oui", "true", "1", "x"].includes(v.toLowerCase())).toBe(true); }); nonOpcoValues.forEach(v => { expect(["oui", "true", "1", "x"].includes(v.toLowerCase())).toBe(false); }); }); });