import { describe, it, expect } from "vitest"; import fs from "node:fs"; import path from "node:path"; import vm from "node:vm"; import { fileURLToPath } from "node:url"; // pure.js est un script classique chargé par ')) .toBe("<script>alert("x")</script>"); }); it("échappe l'apostrophe (attribut délimité par des quotes simples)", () => { expect(P.escapeHtml("l'enfer")).toBe("l'enfer"); }); it("échappe l'esperluette en premier (pas de double échappement inversé)", () => { expect(P.escapeHtml("<")).toBe("&lt;"); }); it("null et undefined donnent une chaîne vide, pas 'null'", () => { expect(P.escapeHtml(null)).toBe(""); expect(P.escapeHtml(undefined)).toBe(""); }); it("laisse le texte normal intact", () => { expect(P.escapeHtml("Mötley Crüe")).toBe("Mötley Crüe"); }); }); describe("norm", () => { it.each([ [null, ""], [undefined, ""], ["", ""], [" Oslo ", "Oslo"], [42, "42"], ])("norm(%s) === %s", (input, expected) => { expect(P.norm(input)).toBe(expected); }); // 0 est une valeur légitime : ne doit pas être avalé comme falsy. it("conserve le zéro", () => { expect(P.norm(0)).toBe("0"); }); }); describe("parseYear", () => { it("accepte un nombre", () => { expect(P.parseYear(1991)).toBe(1991); }); it("extrait une année d'un texte libre", () => { expect(P.parseYear("formé en 1991")).toBe(1991); expect(P.parseYear("1984-1993")).toBe(1984); }); it("ne reconnaît que les siècles plausibles", () => { expect(P.parseYear("an 1234")).toBeNull(); expect(P.parseYear("2015")).toBe(2015); }); it("renvoie null pour null/undefined", () => { expect(P.parseYear(null)).toBeNull(); expect(P.parseYear(undefined)).toBeNull(); }); it("renvoie null pour un texte sans année", () => { expect(P.parseYear("inconnu")).toBeNull(); }); }); describe("splitThemes", () => { it("découpe sur virgule, point-virgule et slash", () => { expect(P.splitThemes("Death, Satan; War/Occult")) .toEqual(["Death", "Satan", "War", "Occult"]); }); it("accepte déjà un tableau", () => { expect(P.splitThemes([" a ", "", "b"])).toEqual(["a", "b"]); }); it("élimine les fragments vides", () => { expect(P.splitThemes("a,,;/b")).toEqual(["a", "b"]); }); it("valeur vide → tableau vide", () => { expect(P.splitThemes("")).toEqual([]); expect(P.splitThemes(null)).toEqual([]); }); }); describe("matchesQuery", () => { it("une requête vide laisse tout passer", () => { expect(P.matchesQuery(band(), "")).toBe(true); }); it.each([ ["nom", "mayhem"], ["genre", "black"], ["statut", "active"], ["pays", "no"], ["lieu", "oslo"], ])("cherche dans le %s", (_champ, q) => { expect(P.matchesQuery(band(), q)).toBe(true); }); it("cherche dans les thèmes", () => { expect(P.matchesQuery(band({ themes: ["Satanism", "Death"] }), "satan")).toBe(true); }); it("ne matche pas ce qui est absent", () => { expect(P.matchesQuery(band(), "burzum")).toBe(false); }); it("est insensible à la casse", () => { expect(P.matchesQuery(band({ name: "MAYHEM" }), "mayhem")).toBe(true); }); it("ne plante pas sur des champs manquants", () => { expect(P.matchesQuery({ name: "X" }, "x")).toBe(true); expect(P.matchesQuery({}, "zzz")).toBe(false); }); }); describe("matchesFacet — logique tri-état", () => { it("une facette absente de la Map passe (facette inconnue ≠ décochée)", () => { expect(P.matchesFacet("Active", new Map(), "Unknown")).toBe(true); }); it("une facette cochée passe", () => { expect(P.matchesFacet("Active", new Map([["Active", true]]), "Unknown")).toBe(true); }); it("une facette décochée ne passe pas", () => { expect(P.matchesFacet("Active", new Map([["Active", false]]), "Unknown")).toBe(false); }); it("une valeur vide retombe sur la clé de repli", () => { const map = new Map([["Unknown", false]]); expect(P.matchesFacet("", map, "Unknown")).toBe(false); expect(P.matchesFacet(null, map, "Unknown")).toBe(false); expect(P.matchesFacet(" ", map, "Unknown")).toBe(false); }); it("la clé de repli cochée laisse passer les valeurs vides", () => { expect(P.matchesFacet(null, new Map([["Unknown", true]]), "Unknown")).toBe(true); }); it("les espaces autour de la valeur sont ignorés", () => { expect(P.matchesFacet(" Active ", new Map([["Active", false]]), "Unknown")).toBe(false); }); }); describe("matchesMacroGenre", () => { const MACROS = [ { key: "Black", terms: ["black"] }, { key: "Doom", terms: ["doom", "stoner"] }, ]; it("aucun macro-genre actif → tout passe", () => { expect(P.matchesMacroGenre(band(), MACROS, new Map(), 0)).toBe(true); }); it("matche sur un terme du macro-genre actif", () => { const enabled = new Map([["Black", true]]); expect(P.matchesMacroGenre(band({ genre: "Black Metal" }), MACROS, enabled, 1)).toBe(true); }); it("matche n'importe lequel des termes d'un macro-genre", () => { const enabled = new Map([["Doom", true]]); expect(P.matchesMacroGenre(band({ genre: "Stoner Rock" }), MACROS, enabled, 1)).toBe(true); }); it("ne matche pas un macro-genre inactif", () => { const enabled = new Map([["Doom", true], ["Black", false]]); expect(P.matchesMacroGenre(band({ genre: "Black Metal" }), MACROS, enabled, 1)).toBe(false); }); // Quand un filtre de genre est actif, un groupe sans genre ne peut pas y // répondre : il doit disparaître, pas passer par défaut. it("un groupe sans genre est exclu dès qu'un macro-genre est actif", () => { const enabled = new Map([["Black", true]]); expect(P.matchesMacroGenre(band({ genre: "" }), MACROS, enabled, 1)).toBe(false); expect(P.matchesMacroGenre(band({ genre: null }), MACROS, enabled, 1)).toBe(false); }); it("est insensible à la casse du genre", () => { const enabled = new Map([["Black", true]]); expect(P.matchesMacroGenre(band({ genre: "BLACK METAL" }), MACROS, enabled, 1)).toBe(true); }); }); describe("matchesTheme", () => { it("aucun thème connu → tout passe", () => { expect(P.matchesTheme(band(), new Map())).toBe(true); }); it("tous les thèmes cochés → tout passe, même sans thème", () => { const all = new Map([["War", true], ["Death", true]]); expect(P.matchesTheme(band({ themes: [] }), all)).toBe(true); }); it("filtre actif : un groupe sans thème est exclu", () => { const partial = new Map([["War", true], ["Death", false]]); expect(P.matchesTheme(band({ themes: [] }), partial)).toBe(false); }); it("filtre actif : garde les groupes ayant au moins un thème coché", () => { const partial = new Map([["War", true], ["Death", false]]); expect(P.matchesTheme(band({ themes: ["War"] }), partial)).toBe(true); expect(P.matchesTheme(band({ themes: ["Death"] }), partial)).toBe(false); }); }); describe("matchesYear", () => { const range = { min: 1970, max: 2020 }; it("aucun filtre posé → tout passe", () => { expect(P.matchesYear(band(), { min: null, max: null }, range)).toBe(true); }); it("plage non initialisée → tout passe", () => { expect(P.matchesYear(band(), { min: 1990, max: 2000 }, { min: null, max: null })).toBe(true); }); // Plage complète = pas de filtrage : les groupes sans année restent visibles. it("plage complète → tout passe, y compris sans année", () => { expect(P.matchesYear(band({ formed_year: null }), { ...range }, range)).toBe(true); }); it("plage resserrée : garde ce qui est dedans", () => { const f = { min: 1980, max: 1990 }; expect(P.matchesYear(band({ formed_year: 1984 }), f, range)).toBe(true); expect(P.matchesYear(band({ formed_year: 1980 }), f, range)).toBe(true); expect(P.matchesYear(band({ formed_year: 1990 }), f, range)).toBe(true); }); it("plage resserrée : exclut ce qui est dehors", () => { const f = { min: 1980, max: 1990 }; expect(P.matchesYear(band({ formed_year: 1979 }), f, range)).toBe(false); expect(P.matchesYear(band({ formed_year: 1991 }), f, range)).toBe(false); }); it("plage resserrée : exclut les groupes sans année", () => { const f = { min: 1980, max: 1990 }; expect(P.matchesYear(band({ formed_year: null }), f, range)).toBe(false); expect(P.matchesYear(band({ formed_year: undefined }), f, range)).toBe(false); }); }); describe("keyFromLatLon", () => { it("produit la même clé pour des coordonnées identiques", () => { expect(P.keyFromLatLon(59.9139, 10.7522)).toBe(P.keyFromLatLon("59.9139", "10.7522")); }); it("distingue des coordonnées différentes", () => { expect(P.keyFromLatLon(59.9139, 10.7522)).not.toBe(P.keyFromLatLon(59.9140, 10.7522)); }); it("normalise à 6 décimales", () => { expect(P.keyFromLatLon(1, 2)).toBe("1.000000,2.000000"); expect(P.keyFromLatLon(1.00000049, 2)).toBe("1.000000,2.000000"); }); }); describe("sortBands", () => { const bands = [ band({ name: "Zyklon", genre: "Death", country: "NO", status: "Split-up", formed_year: 1998 }), band({ name: "abbath", genre: "Black", country: "DE", status: "Active", formed_year: 2005 }), band({ name: "Mayhem", genre: "Black", country: "NO", status: "Active", formed_year: 1984 }), ]; it("ne modifie pas le tableau d'entrée", () => { const copy = [...bands]; P.sortBands(bands, "az"); expect(bands).toEqual(copy); }); it("tri alphabétique insensible à la casse et aux accents", () => { expect(P.sortBands(bands, "az").map((b) => b.name)).toEqual(["abbath", "Mayhem", "Zyklon"]); }); it("mode inconnu retombe sur le tri alphabétique", () => { expect(P.sortBands(bands, "nimportequoi").map((b) => b.name)) .toEqual(["abbath", "Mayhem", "Zyklon"]); }); it("tri par année, décroissant", () => { expect(P.sortBands(bands, "year").map((b) => b.formed_year)).toEqual([2005, 1998, 1984]); }); it("les groupes sans année finissent en dernier", () => { const withNull = [...bands, band({ name: "Sans", formed_year: null })]; expect(P.sortBands(withNull, "year").at(-1).name).toBe("Sans"); }); it.each(["status", "genre", "country"])("tri par %s, départagé par le nom", (mode) => { const sorted = P.sortBands(bands, mode); for (let i = 1; i < sorted.length; i++) { const prev = (sorted[i - 1][mode] || "").toLowerCase(); const cur = (sorted[i][mode] || "").toLowerCase(); expect(prev <= cur).toBe(true); } }); it("départage bien à valeur égale", () => { // abbath et Mayhem sont tous deux Black/Active → départagés par nom expect(P.sortBands(bands, "genre").map((b) => b.name)).toEqual(["abbath", "Mayhem", "Zyklon"]); }); it("liste vide", () => { expect(P.sortBands([], "az")).toEqual([]); }); });