278 lines
14 KiB
JavaScript
278 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ObsiGate — Alertes unifiées (Fusion #69, chantier 2 + chantier 5).
|
|
*
|
|
* Tests du module partagé frontend/draco-alertes.js (logique pure,
|
|
* sans DOM) + vérification du gabarit livré frontend/index.html.
|
|
* Copie vendangée byte-identique de Projets/dracodev-commons
|
|
* ui/alertes/ (chantier 5) — seul le chargement change (ESM ici).
|
|
* Le payload console n'est qu'un EXEMPLE DE FORMAT (voir README
|
|
* commons) : aucun endpoint console n'est publié sur agent-commun
|
|
* main au moment du chantier.
|
|
*
|
|
* Usage: node tests/frontend/draco-alertes.test.mjs
|
|
*/
|
|
import test from "node:test";
|
|
import { strict as assert } from "node:assert";
|
|
import { readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
const require = createRequire(import.meta.url);
|
|
const t = require(path.join(REPO_ROOT, "frontend", "draco-alertes.js"));
|
|
|
|
function sampleResults() {
|
|
return {
|
|
flowdeck: { items: t.normalizeFlowdeck("https://flowdeck.dracodev.net/", {
|
|
notifications: [
|
|
{ id: 1, ntype: "mention", title: "Mention budget", message: "voir page", url: "/collections/3", is_read: 0, created_at: "2026-10-28T10:00:00" },
|
|
{ id: 2, ntype: "reminder", title: "Rappel lu", message: "", url: "", is_read: 1, created_at: "2026-10-27T09:00:00" },
|
|
], unread: 1,
|
|
}) },
|
|
tiktak: { items: t.normalizeTiktak("https://tiktak.dracodev.net/", [
|
|
{ id: 7, kind: "reminder", title: "Rappel tâche", body: "tache", task_id: 42, is_read: 0, created_at: "2026-10-28T08:00:00" },
|
|
{ id: 8, kind: "share", title: "Partage lu", body: "", task_id: null, is_read: 1, created_at: "2026-10-26T08:00:00" },
|
|
]) },
|
|
kanbante: { items: t.normalizeKanbante("https://kanbante.dracodev.net/", {
|
|
events: [
|
|
{ id: 9, repo_full_name: "Projets/fusion-trois-apps", issue_number: 69, event_type: "commented", from_column: "", to_column: "", created_at: "2026-10-28T11:50:00" },
|
|
{ id: 10, repo_full_name: "Projets/fusion-trois-apps", issue_number: 69, event_type: "moved", from_column: "todo", to_column: "review", created_at: "2026-10-28T12:00:00" },
|
|
],
|
|
}) },
|
|
obsigate: { items: [] },
|
|
console: { items: [], sourceMissing: true },
|
|
};
|
|
}
|
|
function allOf(results) {
|
|
const all = [];
|
|
for (const app of ["obsigate", "flowdeck", "tiktak", "kanbante", "console"]) {
|
|
(results[app] && results[app].items || []).forEach((a) => all.push(a));
|
|
}
|
|
return all;
|
|
}
|
|
|
|
test("ordre des onglets : Tout, apps, Console Spark, Historique", () => {
|
|
assert.deepEqual(t.TABS, ["tout", "obsigate", "flowdeck", "tiktak", "kanbante", "console", "historique"]);
|
|
assert.equal(t.TAB_LABELS.console, "Console Spark");
|
|
});
|
|
|
|
test("resolveUrls: config explicite > hote -dev > href > defaut prod, agent inclus", () => {
|
|
const prod = t.resolveUrls({ hostname: "tiktak.dracodev.net", tabHrefs: {} });
|
|
assert.equal(prod.tiktak, "https://tiktak.dracodev.net/");
|
|
assert.equal(prod.agent, "https://agent.dracodev.net/");
|
|
const dev = t.resolveUrls({ hostname: "tiktak-dev.dracodev.net", tabHrefs: {} });
|
|
assert.equal(dev.flowdeck, "https://flowdeck-dev.dracodev.net/");
|
|
assert.equal(dev.obsigate, "https://og-dev.dracodev.net/");
|
|
assert.equal(dev.agent, "https://agent-dev.dracodev.net/");
|
|
const explicit = t.resolveUrls({
|
|
hostname: "tiktak-dev.dracodev.net",
|
|
configUrls: { flowdeck: "https://flowdeck.dracodev.net/" },
|
|
});
|
|
assert.equal(explicit.flowdeck, "https://flowdeck.dracodev.net/");
|
|
const href = t.resolveUrls({
|
|
hostname: "example.org",
|
|
tabHrefs: { kanbante: "https://kanbante-dev.dracodev.net/", agent: "https://agent.dracodev.net/" },
|
|
});
|
|
assert.equal(href.kanbante, "https://kanbante-dev.dracodev.net/");
|
|
assert.equal(href.agent, "https://agent.dracodev.net/");
|
|
const token = t.resolveUrls({ hostname: "x", tabHrefs: { tiktak: "{{URL_TIKTAK}}" } });
|
|
assert.equal(token.tiktak, "https://tiktak.dracodev.net/");
|
|
assert.equal(t.isDevHost("tiktak-dev.dracodev.net.evil.com"), false);
|
|
assert.equal(t.isDevHost("x-dev.dracodev.net"), true);
|
|
});
|
|
|
|
test("normalizeFlowdeck: non lues actives, lien = base + url", () => {
|
|
const items = t.normalizeFlowdeck("https://flowdeck.dracodev.net/", {
|
|
notifications: [
|
|
{ id: 1, ntype: "mention", title: "Mention", message: "voir page", url: "/collections/3", is_read: 0, created_at: "2026-10-28T10:00:00" },
|
|
{ id: 2, ntype: "reminder", title: "Rappel", message: "", url: "", is_read: 1, created_at: "2026-10-27T09:00:00" },
|
|
], unread: 1,
|
|
});
|
|
assert.equal(items.length, 2);
|
|
assert.equal(items[0].active, true);
|
|
assert.equal(items[0].state, "active");
|
|
assert.equal(items[0].url, "https://flowdeck.dracodev.net/collections/3");
|
|
assert.equal(items[1].active, false);
|
|
assert.equal(items[1].url, "https://flowdeck.dracodev.net/");
|
|
});
|
|
|
|
test("normalizeTiktak: lien tache, sinon page notifications", () => {
|
|
const items = t.normalizeTiktak("https://tiktak.dracodev.net/", [
|
|
{ id: 7, kind: "reminder", title: "Rappel", body: "tache", task_id: 42, is_read: 0, created_at: "2026-10-28T08:00:00" },
|
|
{ id: 8, kind: "share", title: "Partage", body: "", task_id: null, is_read: 1, created_at: "2026-10-26T08:00:00" },
|
|
]);
|
|
assert.equal(items[0].url, "https://tiktak.dracodev.net/?task=42");
|
|
assert.equal(items[1].url, "https://tiktak.dracodev.net/notifications");
|
|
assert.equal(items[0].active, true);
|
|
});
|
|
|
|
test("normalizeKanbante: jamais actif (pas d'etat lu/non lu), lien board", () => {
|
|
const items = t.normalizeKanbante("https://kanbante.dracodev.net/", {
|
|
events: [
|
|
{ id: 9, repo_full_name: "Projets/fusion-trois-apps", issue_number: 69, event_type: "commented", from_column: "", to_column: "", created_at: "2026-10-28T11:50:00" },
|
|
{ id: 10, repo_full_name: "Projets/fusion-trois-apps", issue_number: 69, event_type: "moved", from_column: "todo", to_column: "review", created_at: "2026-10-28T12:00:00" },
|
|
],
|
|
});
|
|
assert.equal(items.length, 2);
|
|
assert.ok(items.every((i) => i.active === false));
|
|
assert.equal(items[0].url, "https://kanbante.dracodev.net/board/Projets/fusion-trois-apps");
|
|
assert.match(items[0].title, /#69/);
|
|
});
|
|
|
|
test("sortAlerts + countActive: actives d'abord, date decroissante", () => {
|
|
const list = [
|
|
{ app: "tiktak", active: false, date: "2026-10-28T12:00:00" },
|
|
{ app: "flowdeck", active: true, date: "2026-10-26T12:00:00" },
|
|
{ app: "flowdeck", active: true, date: "2026-10-28T12:00:00" },
|
|
{ app: "kanbante", active: false, date: "" },
|
|
];
|
|
const sorted = t.sortAlerts(list);
|
|
assert.equal(sorted[0].date, "2026-10-28T12:00:00");
|
|
assert.equal(sorted[0].active, true);
|
|
assert.equal(sorted[1].date, "2026-10-26T12:00:00");
|
|
assert.equal(sorted[3].date, "");
|
|
assert.equal(t.countActive(list), 2);
|
|
});
|
|
|
|
test("scopeAlerts: Tout et onglets apps = actives seulement ; Historique = fermees seulement", () => {
|
|
const r = sampleResults();
|
|
const all = allOf(r);
|
|
const tout = t.scopeAlerts(all, "tout", r);
|
|
assert.equal(tout.items.length, 2);
|
|
assert.ok(tout.items.every((a) => a.active));
|
|
assert.equal(tout.items[0].date, "2026-10-28T10:00:00"); // date decroissante
|
|
const fd = t.scopeAlerts(all, "flowdeck", r);
|
|
assert.equal(fd.items.length, 1);
|
|
assert.equal(fd.items[0].app, "flowdeck");
|
|
const hist = t.scopeAlerts(all, "historique", r);
|
|
assert.equal(hist.items.length, 4); // 1 FD lue + 1 TK lue + 2 KB
|
|
assert.ok(hist.items.every((a) => !a.active));
|
|
const og = t.scopeAlerts(all, "obsigate", r);
|
|
assert.equal(og.items.length, 0);
|
|
const cs = t.scopeAlerts(all, "console", r);
|
|
assert.equal(cs.items.length, 0);
|
|
assert.equal(cs.consoleMissing, true);
|
|
});
|
|
|
|
test("scopeAlerts: une section en echec remonte aussi dans Historique (pas avalee)", () => {
|
|
const r = sampleResults();
|
|
r.flowdeck = { items: [], error: "Application injoignable depuis cet hôte (TypeError)." };
|
|
const hist = t.scopeAlerts(allOf(r), "historique", r);
|
|
assert.equal(hist.errors.length, 1);
|
|
assert.equal(hist.errors[0].app, "flowdeck");
|
|
const tout = t.scopeAlerts(allOf(r), "tout", r);
|
|
assert.equal(tout.errors.length, 1);
|
|
});
|
|
|
|
test("countByTab: compteurs par onglet sur l'echantillon", () => {
|
|
const r = sampleResults();
|
|
const c = t.countByTab(allOf(r), r);
|
|
assert.deepEqual(c, {
|
|
tout: 2, obsigate: 0, flowdeck: 1, tiktak: 1,
|
|
kanbante: 0, console: 0, historique: 4,
|
|
});
|
|
});
|
|
|
|
test("filterAlerts: titre + contexte, insensible a la casse, sans melanger actifs/historique", () => {
|
|
const r = sampleResults();
|
|
const all = allOf(r);
|
|
const tout = t.scopeAlerts(all, "tout", r).items;
|
|
assert.equal(t.filterAlerts(tout, "budget").length, 1);
|
|
assert.equal(t.filterAlerts(tout, "BUDGET").length, 1);
|
|
assert.equal(t.filterAlerts(tout, "voir page").length, 1); // contexte (detail)
|
|
assert.equal(t.filterAlerts(tout, "tache").length, 1);
|
|
// « Rappel lu » est une alerte fermee : absente de Tout meme filtree.
|
|
assert.equal(t.filterAlerts(tout, "Rappel lu").length, 0);
|
|
const hist = t.scopeAlerts(all, "historique", r).items;
|
|
assert.equal(t.filterAlerts(hist, "Rappel lu").length, 1);
|
|
assert.equal(t.filterAlerts(tout, "").length, tout.length);
|
|
});
|
|
|
|
test("presentation identique: meta Kanbante sans type brut anglais ; date illisible = sans date", () => {
|
|
const kb = t.normalizeKanbante("https://kanbante.dracodev.net/", {
|
|
events: [{ id: 1, repo_full_name: "R", issue_number: 1, event_type: "moved", from_column: "todo", to_column: "review", created_at: "2026-10-28T12:00:00" }],
|
|
})[0];
|
|
assert.ok(!t.metaLine(kb).includes("moved"));
|
|
assert.match(t.metaLine(kb), /Kanbante · .* · Déplacée/);
|
|
assert.equal(t.displayState({ active: true }), "Active");
|
|
assert.equal(t.displayState({ active: false, app: "flowdeck" }), "Lue");
|
|
const sansDate = { app: "tiktak", active: true, title: "x", detail: "", date: "nimporte-quoi", kind: "", url: "https://x/" };
|
|
assert.match(t.metaLine(sansDate), /sans date/);
|
|
});
|
|
|
|
test("hash URL: parse + round-trip, onglet inconnu rejete", () => {
|
|
assert.equal(t.parseAlertTab("#alertes=historique"), "historique");
|
|
assert.equal(t.parseAlertTab("#alertes=console"), "console");
|
|
assert.equal(t.parseAlertTab("#alertes"), "tout");
|
|
assert.equal(t.parseAlertTab("#autre"), null);
|
|
assert.equal(t.parseAlertTab("#alertes=nimporte"), null);
|
|
assert.equal(t.parseAlertTab(""), null);
|
|
for (const tab of t.TABS) assert.equal(t.parseAlertTab(t.hashForTab(tab)), tab);
|
|
assert.equal(t.hashForTab("nimporte"), "#alertes=tout");
|
|
});
|
|
|
|
test("normalizeConsole (FORMAT EXEMPLE): statuts reels -> actif/ferme, lien console par defaut", () => {
|
|
const items = t.normalizeConsole("https://agent.dracodev.net/", {
|
|
alerts: [
|
|
{ id: "m1", title: "Mission en pause budget", detail: "budget épuisé", status: "pause_budget", created_at: "2026-10-28T09:00:00" },
|
|
{ id: "m2", title: "Accès requis", detail: "jeton manquant", status: "acces_requis", created_at: "2026-10-28T09:30:00" },
|
|
{ id: "a1", title: "Approbation en attente", status: "en_attente", url: "/approbations", created_at: "2026-10-28T10:00:00" },
|
|
{ id: "m3", title: "Mission terminée", status: "terminee", created_at: "2026-10-27T10:00:00" },
|
|
{ id: "m4", title: "Sans statut", created_at: "2026-10-27T11:00:00" },
|
|
],
|
|
});
|
|
assert.equal(items.length, 5);
|
|
assert.equal(items[0].active, true);
|
|
assert.equal(items[1].active, true);
|
|
assert.equal(items[2].active, true);
|
|
assert.equal(items[2].url, "https://agent.dracodev.net/approbations");
|
|
assert.equal(items[3].active, false);
|
|
assert.equal(items[4].active, true); // statut inconnu : expose en active, jamais masque
|
|
assert.equal(items[0].url, "https://agent.dracodev.net/");
|
|
const r = sampleResults();
|
|
r.console = { items };
|
|
const c = t.countByTab(allOf(r), r);
|
|
assert.equal(c.console, 4);
|
|
assert.equal(c.tout, 6);
|
|
assert.equal(c.historique, 5);
|
|
});
|
|
|
|
test("consoleSourceUrl: null sans configuration (aucune URL devineee)", () => {
|
|
assert.equal(t.consoleSourceUrl({}), null);
|
|
assert.equal(t.consoleSourceUrl({ consoleUrl: "https://agent-dev.dracodev.net/api/alertes" }),
|
|
"https://agent-dev.dracodev.net/api/alertes");
|
|
});
|
|
|
|
test("index.html: placeholder Alertes disparu, module charge", () => {
|
|
const html = readFileSync(path.join(REPO_ROOT, "frontend", "index.html"), "utf8");
|
|
assert.equal(html.includes("Alertes unifiées : arrive avec la fédération"), false);
|
|
assert.equal(html.includes("/static/draco-alertes.js"), true);
|
|
assert.equal(html.includes("/static/draco-alertes.css"), true);
|
|
assert.equal(html.includes("DRACO_ALERTES.open()"), true);
|
|
});
|
|
|
|
test("index.html chantier 5: onglet Console Spark en dernier, apres Kanbante", () => {
|
|
const html = readFileSync(path.join(REPO_ROOT, "frontend", "index.html"), "utf8");
|
|
const nav = /<nav class="draco-apptabs"[^>]*>([\s\S]*?)<\/nav>/.exec(html)[1];
|
|
const apps = [...nav.matchAll(/data-app="([^"]+)"/g)].map((m) => m[1]);
|
|
assert.deepEqual(apps, ["obsigate", "flowdeck", "tiktak", "kanbante", "agent"]);
|
|
assert.ok(nav.includes('data-app="agent" href="https://agent.dracodev.net/"'));
|
|
assert.ok(nav.includes("<strong>Console Spark</strong>"));
|
|
// La réécriture DRACO_URLS couvre la clé "agent" (même boucle que les autres onglets).
|
|
assert.ok(html.includes('window.DRACO_URLS[a.dataset.app]'));
|
|
});
|
|
|
|
test("draco-alertes.js/.css chantier 5: onglets, filtre, etat console honnete", () => {
|
|
const js = readFileSync(path.join(REPO_ROOT, "frontend", "draco-alertes.js"), "utf8");
|
|
assert.ok(js.includes("draco-alertes-tab"));
|
|
assert.ok(js.includes("historique"));
|
|
assert.ok(js.includes("source indisponible"));
|
|
assert.ok(js.includes("DRACO_ALERTES_CONSOLE_URL"));
|
|
const css = readFileSync(path.join(REPO_ROOT, "frontend", "draco-alertes.css"), "utf8");
|
|
assert.ok(css.includes(".draco-alertes-tab"));
|
|
assert.ok(css.includes(".draco-alertes-filtre"));
|
|
});
|