feat: chat — boutons discrets sous l'image, image en onglet d'app, point de non-lus, notification OS, suppression annulable BUG-110
This commit is contained in:
@@ -46,8 +46,10 @@ 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();
|
||||
globalThis.fetch = (url) => {
|
||||
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 {
|
||||
@@ -77,7 +79,7 @@ const mod = await import("../../frontend/js/filechat.js");
|
||||
const {
|
||||
renderChatMessages, onChatMessage, openFileChat, closeFileChat, toggleFileChat,
|
||||
followFileChat, openSidebarChat, filterChatMessages, refreshUnreadBadge,
|
||||
onChatDeleted, onChatRead,
|
||||
onChatDeleted, onChatRead, _deleteMessage,
|
||||
} = mod;
|
||||
|
||||
let pass = 0;
|
||||
@@ -338,6 +340,99 @@ test("#192 — saisie en textarea auto-agrandie, Enter envoie", async () => {
|
||||
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" };
|
||||
}
|
||||
});
|
||||
|
||||
// Sequential execution: each case depends on panel state left by the previous.
|
||||
for (const [name, fn] of cases) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user