472 lines
23 KiB
JavaScript
472 lines
23 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ObsiGate — JSDOM tests for the per-file chat panel (#169):
|
|
* `frontend/js/filechat.js`.
|
|
*
|
|
* Covers:
|
|
* - `renderChatMessages()`: chronological render + empty state + escaping
|
|
* - `onChatMessage()`: live append when open on the same file, dedupe by id,
|
|
* no duplicate append when the panel shows another file
|
|
*
|
|
* Usage: node tests/frontend/filechat.test.mjs
|
|
*/
|
|
|
|
import { strict as assert } from "node:assert";
|
|
import { JSDOM } from "jsdom";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import path from "node:path";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const dom = new JSDOM(
|
|
`<!DOCTYPE html><html><body><div id="toast-container"></div>
|
|
<div class="sidebar-tab-panel active" id="sidebar-panel-chat">
|
|
<div id="sidebar-panel-chat-list" class="file-chat-list"></div>
|
|
<div id="sidebar-panel-chat-pending"></div>
|
|
<form id="sidebar-panel-chat-form"><textarea id="sidebar-panel-chat-input"></textarea></form>
|
|
</div>
|
|
<button id="sidebar-tab-chat"></button>
|
|
</body></html>`,
|
|
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
);
|
|
|
|
const w = dom.window;
|
|
globalThis.window = w;
|
|
globalThis.document = w.document;
|
|
globalThis.HTMLElement = w.HTMLElement;
|
|
globalThis.CSS = w.CSS || { escape: (s) => s };
|
|
globalThis.localStorage = w.localStorage;
|
|
globalThis.requestAnimationFrame = w.requestAnimationFrame || ((cb) => setTimeout(cb, 16));
|
|
globalThis.cancelAnimationFrame = w.cancelAnimationFrame || clearTimeout;
|
|
globalThis.sessionStorage = w.sessionStorage;
|
|
const JS_DIR = path.resolve(__dirname, "..", "..", "frontend", "js");
|
|
const LOCALES_DIR = path.resolve(__dirname, "..", "..", "frontend", "locales");
|
|
|
|
// Route fetch: locales from disk, auth 401, API chat endpoints from a stub map.
|
|
const apiStubs = new Map();
|
|
const fetchCalls = []; // #110 — journal des appels {url, method}
|
|
globalThis.fetch = (url, opts = {}) => {
|
|
const u = String(url);
|
|
fetchCalls.push({ url: u, method: (opts.method || "GET").toUpperCase() });
|
|
if (u.includes("/static/locales/")) {
|
|
const name = u.split("/").pop();
|
|
try {
|
|
const body = readFileSync(path.join(LOCALES_DIR, name), "utf-8");
|
|
return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(JSON.parse(body)) });
|
|
} catch (_) {
|
|
return Promise.resolve({ ok: false, status: 404, json: () => Promise.resolve({}) });
|
|
}
|
|
}
|
|
if (u.includes("/api/auth/me")) {
|
|
return Promise.resolve({ ok: false, status: 401, json: () => Promise.resolve({}) });
|
|
}
|
|
if (apiStubs.has(u)) {
|
|
return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(apiStubs.get(u)) });
|
|
}
|
|
return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({ messages: [] }) });
|
|
};
|
|
|
|
const { initI18n } = await import(pathToFileURL(path.join(JS_DIR, "i18n.js")).href);
|
|
await initI18n();
|
|
|
|
// #191 — the delete right needs a logged-in identity
|
|
const { AuthManager } = await import(pathToFileURL(path.join(JS_DIR, "auth.js")).href);
|
|
AuthManager.getUser = () => ({ username: "bruno", role: "user" });
|
|
|
|
const mod = await import("../../frontend/js/filechat.js");
|
|
const {
|
|
renderChatMessages, onChatMessage, openFileChat, closeFileChat, toggleFileChat,
|
|
followFileChat, openSidebarChat, filterChatMessages, refreshUnreadBadge,
|
|
onChatDeleted, onChatRead, _deleteMessage,
|
|
} = mod;
|
|
|
|
let pass = 0;
|
|
const cases = [];
|
|
function test(name, fn) {
|
|
cases.push([name, fn]);
|
|
}
|
|
|
|
function makeList() {
|
|
const list = document.createElement("div");
|
|
list.className = "file-chat-list";
|
|
return list;
|
|
}
|
|
|
|
test("renderChatMessages: empty/missing shows the empty state", async () => {
|
|
const list = makeList();
|
|
renderChatMessages(list, [], "bruno");
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, 0);
|
|
assert.strictEqual(list.querySelector(".file-chat-empty") !== null, true);
|
|
renderChatMessages(list, undefined, "bruno");
|
|
assert.strictEqual(list.querySelector(".file-chat-empty") !== null, true);
|
|
});
|
|
|
|
test("renderChatMessages: renders chronologically with mine class", async () => {
|
|
const list = makeList();
|
|
renderChatMessages(
|
|
list,
|
|
[
|
|
{ id: "1", user: "alice", text: "bonjour", ts: 1700000000 },
|
|
{ id: "2", user: "bruno", text: "salut !", ts: 1700000060 },
|
|
],
|
|
"bruno",
|
|
);
|
|
const msgs = list.querySelectorAll(".file-chat-msg");
|
|
assert.strictEqual(msgs.length, 2);
|
|
assert.strictEqual(msgs[0].querySelector(".file-chat-user").textContent, "alice");
|
|
assert.strictEqual(msgs[0].classList.contains("mine"), false);
|
|
assert.strictEqual(msgs[1].classList.contains("mine"), true);
|
|
assert.strictEqual(msgs[1].querySelector(".file-chat-body").textContent, "salut !");
|
|
});
|
|
|
|
test("renderChatMessages: message text is text (no HTML injection)", async () => {
|
|
const list = makeList();
|
|
renderChatMessages(list, [{ id: "1", user: "<img>", text: "<b>x</b>", ts: 1 }], "u");
|
|
assert.strictEqual(list.querySelector(".file-chat-body").innerHTML, "<b>x</b>");
|
|
assert.strictEqual(list.querySelectorAll("b").length, 0);
|
|
});
|
|
|
|
test("onChatMessage: appends live message to the open panel", async () => {
|
|
await openFileChat("V", "note.md");
|
|
const panel = document.getElementById("file-chat-panel");
|
|
assert.strictEqual(panel.classList.contains("hidden"), false);
|
|
// Seed an empty state so we can assert its removal
|
|
const list = panel.querySelector(".file-chat-list");
|
|
renderChatMessages(list, [], "bruno");
|
|
onChatMessage({ vault: "V", path: "note.md", message: { id: "m1", user: "alice", text: "oui", ts: 2 } });
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, 1);
|
|
assert.strictEqual(list.querySelector(".file-chat-empty"), null);
|
|
// Same id again → deduped
|
|
onChatMessage({ vault: "V", path: "note.md", message: { id: "m1", user: "alice", text: "oui", ts: 2 } });
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, 1);
|
|
closeFileChat();
|
|
assert.strictEqual(document.getElementById("file-chat-panel").classList.contains("hidden"), true);
|
|
});
|
|
|
|
test("onChatMessage: ignores events for another file (no cross-file append)", async () => {
|
|
await openFileChat("V", "note.md");
|
|
const panel = document.getElementById("file-chat-panel");
|
|
const list = panel.querySelector(".file-chat-list");
|
|
renderChatMessages(list, [], "bruno");
|
|
onChatMessage({ vault: "V", path: "OTHER.md", message: { id: "x", user: "alice", text: "no", ts: 3 } });
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, 0);
|
|
closeFileChat();
|
|
});
|
|
|
|
test("toggleFileChat: opens then closes on the same file", async () => {
|
|
await toggleFileChat("V", "t.md");
|
|
let panel = document.getElementById("file-chat-panel");
|
|
assert.strictEqual(panel.classList.contains("hidden"), false);
|
|
await toggleFileChat("V", "t.md");
|
|
panel = document.getElementById("file-chat-panel");
|
|
assert.strictEqual(panel.classList.contains("hidden"), true);
|
|
closeFileChat();
|
|
});
|
|
|
|
test("#190 A — followFileChat re-targets the open panel", async () => {
|
|
await openFileChat("V", "one.md", "Doc Un");
|
|
const panel = document.getElementById("file-chat-panel");
|
|
assert.strictEqual(panel.classList.contains("hidden"), false);
|
|
followFileChat("V", "two.md", "Doc Deux");
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
const target = panel.querySelector(".file-chat-target");
|
|
assert.strictEqual(target.textContent, "Doc Deux");
|
|
assert.strictEqual(target.title, "two.md");
|
|
closeFileChat();
|
|
followFileChat("V", "three.md", "Doc Trois");
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
assert.strictEqual(panel.querySelector(".file-chat-target").textContent, "Doc Deux");
|
|
});
|
|
|
|
test("#190 B — openSidebarChat renders the general history", async () => {
|
|
await openSidebarChat();
|
|
const list = document.getElementById("sidebar-panel-chat-list");
|
|
assert.strictEqual(list.querySelector(".file-chat-empty") !== null, true);
|
|
});
|
|
|
|
test("#190 B — onChatMessage routes __global__ to the sidebar list", async () => {
|
|
const list = document.getElementById("sidebar-panel-chat-list");
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "g1", user: "alice", text: "salut tout le monde", ts: 5 } });
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, 1);
|
|
assert.strictEqual(list.querySelector(".file-chat-body").textContent, "salut tout le monde");
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "g1", user: "alice", text: "salut tout le monde", ts: 5 } });
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, 1);
|
|
});
|
|
|
|
test("#190 C — filterChatMessages narrows by text/user", async () => {
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "f1", user: "alice", text: "recette de pizza", ts: 10 } });
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "f2", user: "bob", text: "deploiement docker", ts: 11 } });
|
|
const list = document.getElementById("sidebar-panel-chat-list");
|
|
filterChatMessages("pizza");
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, 1);
|
|
assert.strictEqual(list.querySelector(".file-chat-body").textContent, "recette de pizza");
|
|
filterChatMessages("bob"); // la recherche couvre aussi l'auteur
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, 1);
|
|
filterChatMessages("");
|
|
assert.ok(list.querySelectorAll(".file-chat-msg").length >= 3);
|
|
});
|
|
|
|
test("#190 — URL rendered as a link, surrounding text stays plain", async () => {
|
|
const list = document.getElementById("sidebar-panel-chat-list");
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "u1", user: "alice", text: "voici https://example.com/doc et du texte", ts: 20 } });
|
|
const msg = list.querySelector('[data-id="u1"]');
|
|
const a = msg.querySelector("a.file-chat-link");
|
|
assert.strictEqual(a.href, "https://example.com/doc");
|
|
assert.strictEqual(a.target, "_blank");
|
|
assert.ok(msg.textContent.includes("du texte"));
|
|
});
|
|
|
|
test("#191 — preview card rendered with site/title/description", () => {
|
|
const list = document.createElement("div");
|
|
renderChatMessages(list, [{
|
|
id: "p1", user: "alice", text: "voir https://example.com", ts: 40,
|
|
preview: { url: "https://example.com", title: "Mon site", description: "Le desc", image: "", site: "example.com" },
|
|
}], "bruno");
|
|
const card = list.querySelector("a.file-chat-preview");
|
|
assert.ok(card, "carte preview présente");
|
|
assert.strictEqual(card.href, "https://example.com/");
|
|
assert.strictEqual(card.target, "_blank");
|
|
assert.strictEqual(card.querySelector(".file-chat-preview-title").textContent, "Mon site");
|
|
assert.strictEqual(card.querySelector(".file-chat-preview-desc").textContent, "Le desc");
|
|
assert.strictEqual(card.querySelector(".file-chat-preview-site").textContent, "example.com");
|
|
renderChatMessages(list, [{ id: "p2", user: "alice", text: "https://x.test", ts: 41 }], "bruno");
|
|
assert.strictEqual(list.querySelector("a.file-chat-preview"), null);
|
|
});
|
|
|
|
test("#191 — delete button hidden without delUrl, shown for own post", () => {
|
|
const list = document.createElement("div");
|
|
renderChatMessages(list, [{ id: "d1", user: "bruno", text: "x", ts: 50 }], "bruno");
|
|
assert.strictEqual(list.querySelector(".file-chat-del"), null);
|
|
renderChatMessages(list, [{ id: "d2", user: "bruno", text: "x", ts: 51 }], "bruno", "/api/chat");
|
|
assert.ok(list.querySelector(".file-chat-del"), "bouton suppression visible");
|
|
renderChatMessages(list, [{ id: "d3", user: "alice", text: "x", ts: 52 }], "bruno", "/api/chat");
|
|
assert.strictEqual(list.querySelector(".file-chat-del"), null);
|
|
});
|
|
|
|
test("#191 — onChatDeleted removes the message from any list", async () => {
|
|
await openSidebarChat();
|
|
const list = document.getElementById("sidebar-panel-chat-list");
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "del1", user: "alice", text: "a bientot", ts: 60 } });
|
|
assert.strictEqual(list.querySelector('[data-id="del1"]') !== null, true);
|
|
onChatDeleted({ vault: "__global__", path: "general", id: "del1" });
|
|
assert.strictEqual(list.querySelector('[data-id="del1"]'), null);
|
|
});
|
|
|
|
test("#191 — unread counters accumulate then reset on open", async () => {
|
|
localStorage.removeItem("obsigate-chat-unread");
|
|
const panel = document.getElementById("sidebar-panel-chat");
|
|
panel.classList.remove("active");
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "u-1", user: "bob", text: "1", ts: 70 } });
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "u-2", user: "bob", text: "2", ts: 71 } });
|
|
let u = JSON.parse(localStorage.getItem("obsigate-chat-unread"));
|
|
assert.strictEqual(u.general, 2);
|
|
refreshUnreadBadge();
|
|
assert.strictEqual(document.querySelector("#sidebar-tab-chat .chat-unread-badge").textContent, "2");
|
|
panel.classList.add("active");
|
|
await openSidebarChat();
|
|
u = JSON.parse(localStorage.getItem("obsigate-chat-unread"));
|
|
assert.strictEqual(u.general, 0);
|
|
assert.strictEqual(document.querySelector("#sidebar-tab-chat .chat-unread-badge"), null);
|
|
});
|
|
|
|
test("#191 — DM routed by pair path, isolated from general", async () => {
|
|
const list = document.getElementById("sidebar-panel-chat-list");
|
|
const before = list.querySelectorAll(".file-chat-msg").length;
|
|
onChatMessage({ vault: "__dm__", path: "alice|bruno", message: { id: "dm-1", user: "alice", text: "prive", ts: 80 } });
|
|
const u = JSON.parse(localStorage.getItem("obsigate-chat-unread"));
|
|
assert.strictEqual(u["dm:alice"], 1);
|
|
assert.strictEqual(list.querySelectorAll(".file-chat-msg").length, before);
|
|
});
|
|
|
|
// --- #192 : accusé de réception + boîte de saisie auto-agrandie --------------
|
|
|
|
test("#192 — receipt shows ✓ then ✓✓ once another participant read it", () => {
|
|
const list = document.createElement("div");
|
|
renderChatMessages(list, [{ id: "r1", user: "bruno", text: "coucou", ts: 100 }], "bruno", null, {});
|
|
let st = list.querySelector(".file-chat-status");
|
|
assert.strictEqual(st.textContent, "✓");
|
|
assert.strictEqual(st.classList.contains("read"), false);
|
|
// MY OWN read timestamp must never count as "the peer read it"
|
|
renderChatMessages(list, [{ id: "r2", user: "bruno", text: "coucou", ts: 100 }], "bruno", null, { bruno: 500 });
|
|
assert.strictEqual(list.querySelector(".file-chat-status").textContent, "✓");
|
|
renderChatMessages(list, [{ id: "r3", user: "bruno", text: "coucou", ts: 100 }], "bruno", null, { alice: 150 });
|
|
st = list.querySelector(".file-chat-status");
|
|
assert.strictEqual(st.textContent, "✓✓");
|
|
assert.strictEqual(st.classList.contains("read"), true);
|
|
// messages of others carry no receipt
|
|
renderChatMessages(list, [{ id: "r4", user: "alice", text: "ok", ts: 100 }], "bruno", null, {});
|
|
assert.strictEqual(list.querySelector(".file-chat-status"), null);
|
|
});
|
|
|
|
test("#192 — onChatRead flips ✓ → ✓✓ live and notifies the sender", async () => {
|
|
await openSidebarChat();
|
|
const list = document.getElementById("sidebar-panel-chat-list");
|
|
renderChatMessages(list, [{ id: "s1", user: "bruno", text: "msg", ts: 200 }], "bruno", "/api/chat", {});
|
|
assert.strictEqual(list.querySelector(".file-chat-status").textContent, "✓");
|
|
const container = document.getElementById("toast-container");
|
|
const before = container.children.length;
|
|
onChatRead({ vault: "__global__", path: "general", user: "alice", ts: 300, read: { bruno: 250, alice: 300 } });
|
|
assert.strictEqual(list.querySelector(".file-chat-status").textContent, "✓✓");
|
|
assert.ok(container.children.length > before, "notification de lecture affichée");
|
|
// notre propre écho ne notifie pas
|
|
const same = container.children.length;
|
|
onChatRead({ vault: "__global__", path: "general", user: "bruno", ts: 310, read: { bruno: 310 } });
|
|
assert.strictEqual(container.children.length, same);
|
|
});
|
|
|
|
test("#192 — saisie en textarea auto-agrandie, Enter envoie", async () => {
|
|
assert.strictEqual(document.getElementById("sidebar-panel-chat-input").tagName, "TEXTAREA");
|
|
await openFileChat("V", "grow.md");
|
|
const panel = document.getElementById("file-chat-panel");
|
|
const input = panel.querySelector(".file-chat-input");
|
|
assert.strictEqual(input.tagName, "TEXTAREA");
|
|
assert.strictEqual(input.rows, 1);
|
|
input.value = "ligne1\nligne2";
|
|
input.dispatchEvent(new w.Event("input", { bubbles: true }));
|
|
assert.ok(input.style.height, "hauteur pilotée en JS");
|
|
let prevented = false;
|
|
const ev = new w.KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true });
|
|
Object.defineProperty(ev, "preventDefault", { value: () => { prevented = true; } });
|
|
input.value = ""; // rien à envoyer : on ne teste que le geste
|
|
input.dispatchEvent(ev);
|
|
assert.strictEqual(prevented, true, "Enter (sans Shift) est intercepté");
|
|
const nl = new w.KeyboardEvent("keydown", { key: "Enter", shiftKey: true, bubbles: true, cancelable: true });
|
|
let nlPrevented = false;
|
|
Object.defineProperty(nl, "preventDefault", { value: () => { nlPrevented = true; } });
|
|
input.dispatchEvent(nl);
|
|
assert.strictEqual(nlPrevented, false, "Shift+Enter garde le retour à la ligne");
|
|
closeFileChat();
|
|
});
|
|
|
|
// --- #110 : boutons discrets, image en onglet, point non-lu, delete différé -
|
|
|
|
test("#110 — bouton supprimer visible (pas opacity:0)", () => {
|
|
const list = document.createElement("div");
|
|
renderChatMessages(list, [{ id: "d1", user: "bruno", text: "x", ts: 50 }], "bruno", "/api/chat");
|
|
const del = list.querySelector(".file-chat-del");
|
|
assert.ok(del, "bouton suppression présent");
|
|
assert.strictEqual(del.style.opacity, "", "pas de opacity:0 — bouton visible");
|
|
});
|
|
|
|
test("#110 — image : boutons icônes discrets sous l'image, à droite", () => {
|
|
const list = document.createElement("div");
|
|
renderChatMessages(list, [{
|
|
id: "img1", user: "alice", text: "", ts: 100,
|
|
attachment: { kind: "image", url: "/api/chat/attachment/photo.png", name: "photo.png", mime: "image/png" },
|
|
}], "bruno");
|
|
const container = list.querySelector(".file-chat-image-container");
|
|
assert.ok(container, "conteneur image présent");
|
|
const actions = container.querySelector(".file-chat-image-actions");
|
|
assert.ok(actions, "rangée de boutons sous l'image");
|
|
assert.ok(actions.querySelector(".file-chat-image-download"), "bouton téléchargement présent");
|
|
assert.ok(actions.querySelector(".file-chat-open"), "bouton visualisation présent");
|
|
actions.querySelectorAll("button").forEach((b) => {
|
|
assert.strictEqual(b.textContent.trim(), "", "bouton icône seule (sans libellé texte)");
|
|
assert.ok(b.getAttribute("aria-label"), "aria-label présent");
|
|
});
|
|
});
|
|
|
|
test("#110 — clic sur l'image : ouverture dans un onglet de l'app", async () => {
|
|
const ui = await import(pathToFileURL(path.join(JS_DIR, "ui.js")).href);
|
|
const seen = [];
|
|
const orig = ui.TabManager.openChatImage;
|
|
ui.TabManager.openChatImage = (att) => seen.push(att);
|
|
try {
|
|
const list = document.createElement("div");
|
|
const att = { kind: "image", url: "/api/chat/attachment/p2.png", name: "p2.png" };
|
|
renderChatMessages(list, [{ id: "img2", user: "alice", text: "", ts: 101, attachment: att }], "bruno");
|
|
list.querySelector(".file-chat-image").click();
|
|
assert.strictEqual(seen.length, 1, "openChatImage appelé au clic sur l'image");
|
|
assert.strictEqual(seen[0].url, att.url, "même pièce jointe transmise");
|
|
list.querySelector(".file-chat-open").click();
|
|
assert.strictEqual(seen.length, 2, "le bouton discret ouvre aussi l'onglet");
|
|
} finally {
|
|
ui.TabManager.openChatImage = orig;
|
|
}
|
|
});
|
|
|
|
test("#110 — point de couleur sur l'icône chat tant qu'il reste des non-lus", () => {
|
|
const tab = document.getElementById("sidebar-tab-chat");
|
|
assert.ok(tab, "onglet chat présent");
|
|
localStorage.setItem("obsigate-chat-unread", JSON.stringify({ general: 3 }));
|
|
refreshUnreadBadge();
|
|
assert.ok(tab.classList.contains("chat-has-unread"), "classe chat-has-unread posée");
|
|
localStorage.setItem("obsigate-chat-unread", JSON.stringify({}));
|
|
refreshUnreadBadge();
|
|
assert.ok(!tab.classList.contains("chat-has-unread"), "classe retirée quand tout est lu");
|
|
});
|
|
|
|
test("#110 — suppression différée : Annuler remet le message, aucun DELETE", () => {
|
|
const list = document.createElement("div");
|
|
renderChatMessages(list, [{ id: "del9", user: "bruno", text: "mon message", ts: 200 }], "bruno", "/api/chat");
|
|
const before = fetchCalls.length;
|
|
list.querySelector(".file-chat-del").click();
|
|
assert.ok(list.querySelector(".file-chat-deleted"), "ligne remplacée par « supprimé »");
|
|
assert.ok(list.querySelector(".file-chat-undo"), "bouton Annuler présent");
|
|
assert.ok(!fetchCalls.slice(before).some((c) => c.method === "DELETE"), "aucun DELETE parti pendant la fenêtre");
|
|
list.querySelector(".file-chat-undo").click();
|
|
assert.ok(list.querySelector("[data-id='del9']"), "message d'origine restauré");
|
|
assert.strictEqual(list.querySelector(".file-chat-deleted"), null, "ligne « supprimé » retirée");
|
|
assert.ok(!fetchCalls.slice(before).some((c) => c.method === "DELETE"), "toujours aucun DELETE");
|
|
});
|
|
|
|
test("#110 — notification OS quand l'app est en arrière-plan", () => {
|
|
const fired = [];
|
|
class FakeNotification {
|
|
constructor(title, opts) { fired.push({ title, opts }); }
|
|
close() {}
|
|
}
|
|
FakeNotification.permission = "granted";
|
|
window.Notification = FakeNotification;
|
|
Object.defineProperty(document, "hidden", { value: true, configurable: true });
|
|
try {
|
|
const panel = document.getElementById("sidebar-panel-chat");
|
|
panel.classList.remove("active");
|
|
onChatMessage({ vault: "__global__", path: "general", message: { id: "n-1", user: "bob", text: "coucou", ts: 300 } });
|
|
assert.strictEqual(fired.length, 1, "notification affichée");
|
|
assert.ok(String(fired[0].opts.body).includes("coucou"), "aperçu du message dans la notification");
|
|
} finally {
|
|
Object.defineProperty(document, "hidden", { value: false, configurable: true });
|
|
window.Notification = { permission: "denied" };
|
|
}
|
|
});
|
|
|
|
// --- #193 : un post est rendu comme un document (markdown + code coloré) -----
|
|
|
|
test("#193 — post avec html rendu : markdown + bloc de code", () => {
|
|
const list = document.createElement("div");
|
|
renderChatMessages(list, [{
|
|
id: "md1", user: "alice", text: "**gras**\n\n```python\nprint(1)\n```", ts: 400,
|
|
html: '<p><strong>gras</strong></p><pre><code class="language-python">print(1)</code></pre>',
|
|
}], "bruno");
|
|
const body = list.querySelector("[data-id='md1'] .file-chat-body");
|
|
assert.ok(body.classList.contains("file-chat-md"), "classe md posée sur le corps");
|
|
assert.ok(body.querySelector(".md-content"), "conteneur md-content");
|
|
assert.ok(body.querySelector(".md-content strong"), "markdown rendu (strong)");
|
|
assert.ok(body.querySelector("pre code.language-python"), "bloc de code conservé");
|
|
});
|
|
|
|
test("#193 — sans html, repli texte brut + URL cliquable", () => {
|
|
const list = document.createElement("div");
|
|
renderChatMessages(list, [{ id: "md2", user: "alice", text: "voir https://ex.com/a", ts: 401 }], "bruno");
|
|
const body = list.querySelector("[data-id='md2'] .file-chat-body");
|
|
assert.ok(!body.classList.contains("file-chat-md"), "pas de rendu markdown");
|
|
assert.ok(body.querySelector("a.file-chat-link"), "URL toujours cliquable");
|
|
assert.strictEqual(body.textContent, "voir https://ex.com/a", "texte intact");
|
|
});
|
|
|
|
// Sequential execution: each case depends on panel state left by the previous.
|
|
for (const [name, fn] of cases) {
|
|
try {
|
|
await fn();
|
|
pass++;
|
|
console.log(` ✓ ${name}`);
|
|
} catch (err) {
|
|
console.error(` ✗ ${name}\n ${err.message}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
console.log(`\n${pass} passed`);
|