feat: chat — saisie auto-agrandissante, accusé de réception ✓✓, vignette de tuile #192 (BUG-109)
CI / lint (push) Successful in 2m52s
CI / security (push) Successful in 1m35s
CI / test (push) Successful in 4m27s
CI / build (push) Successful in 1m29s
CI / e2e (push) Successful in 17m1s

This commit is contained in:
2026-10-08 23:28:08 -04:00
parent f00a8bea8f
commit 54caa48d1f
22 changed files with 645 additions and 52 deletions
+62 -2
View File
@@ -25,7 +25,7 @@ const dom = new JSDOM(
<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"><input id="sidebar-panel-chat-input" /></form>
<form id="sidebar-panel-chat-form"><textarea id="sidebar-panel-chat-input"></textarea></form>
</div>
<button id="sidebar-tab-chat"></button>
</body></html>`,
@@ -77,7 +77,7 @@ const mod = await import("../../frontend/js/filechat.js");
const {
renderChatMessages, onChatMessage, openFileChat, closeFileChat, toggleFileChat,
followFileChat, openSidebarChat, filterChatMessages, refreshUnreadBadge,
onChatDeleted,
onChatDeleted, onChatRead,
} = mod;
let pass = 0;
@@ -278,6 +278,66 @@ test("#191 — DM routed by pair path, isolated from general", async () => {
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();
});
// Sequential execution: each case depends on panel state left by the previous.
for (const [name, fn] of cases) {
try {