Files
ObsiGate/tests/frontend/ai-sidebar.test.mjs
T
bruno 8e2b6203a1
CI / lint (push) Successful in 1m28s
CI / security (push) Successful in 58s
CI / test (push) Successful in 2m19s
CI / build (push) Successful in 54s
CI / e2e (push) Successful in 10m44s
feat: filtre de recherche dans la sidebar Historique IA (#98) + menus '@' et '/' depuis le menu '+' (BUG-048)
2026-09-16 16:11:54 -04:00

143 lines
5.0 KiB
JavaScript

#!/usr/bin/env node
/**
* ObsiGate - JSDOM tests for the AI history sidebar filter (#98).
*
* Covers the client-side search over the AI conversation history rendered in
* the left sidebar "Historique IA" tab:
* - sessions are loaded and rendered through `loadAISessionList()`
* - `filterAIHistory()` narrows the list by title / preview / directory /
* context / mode label, case- and accent-insensitively
* - an empty match shows a "no match" hint instead of the empty state
*
* Usage: node tests/frontend/ai-sidebar.test.mjs
*/
import { strict as assert } from "node:assert";
import { JSDOM } from "jsdom";
import { fileURLToPath, pathToFileURL } from "node:url";
import path from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const JS_DIR = path.resolve(__dirname, "..", "..", "frontend", "js");
const dom = new JSDOM(
`<!DOCTYPE html>
<html><body>
<aside class="sidebar">
<input id="sidebar-filter-input" value="" />
<button id="sidebar-filter-case-btn"></button>
<button id="sidebar-filter-clear-btn"></button>
<div id="ai-history-list" class="recent-list"></div>
<div id="ai-history-empty" class="recent-empty hidden">
<i data-lucide="messages-square"></i><span>Aucune conversation</span>
</div>
</aside>
</body></html>`,
{ url: "http://localhost/", pretendToBeVisual: true },
);
const w = dom.window;
globalThis.window = w;
globalThis.document = w.document;
globalThis.HTMLElement = w.HTMLElement;
globalThis.Element = w.Element;
globalThis.Node = w.Node;
globalThis.Event = w.Event;
globalThis.CustomEvent = w.CustomEvent;
globalThis.KeyboardEvent = w.KeyboardEvent;
globalThis.localStorage = w.localStorage;
globalThis.sessionStorage = w.sessionStorage;
Object.defineProperty(globalThis, "navigator", {
value: w.navigator,
configurable: true,
writable: true,
});
globalThis.MutationObserver = w.MutationObserver;
globalThis.getComputedStyle = w.getComputedStyle.bind(w);
globalThis.requestAnimationFrame = (cb) => setTimeout(cb, 0);
globalThis.cancelAnimationFrame = (id) => clearTimeout(id);
let testCount = 0;
let passCount = 0;
async function test(name, fn) {
testCount++;
try {
await fn();
passCount++;
console.log(` ✓ ${name}`);
} catch (err) {
console.error(` ✗ ${name}\n ${err.message}`);
process.exitCode = 1;
}
}
const SESSIONS = [
{ id: "s1", title: "Pizza maison", mode: "general", updatedAt: 1, preview: "La pâte à pizza se lève toute la nuit" },
{ id: "s2", title: "Recette café au lait", mode: "directory", directory: "Recettes", updatedAt: 2 },
{ id: "s3", title: "Plan du cours", mode: "documents", context: "documents-books", updatedAt: 3 },
];
globalThis.fetch = async (url) => {
if (String(url).includes("/api/ai/bookslm/history")) {
return { ok: true, status: 200, json: async () => ({ sessions: SESSIONS }) };
}
return { ok: true, status: 200, json: async () => ({}) };
};
const mod = await import(
pathToFileURL(path.join(JS_DIR, "config.js")).href
);
const { loadAISessionList, filterAIHistory } = mod;
const listEl = () => document.getElementById("ai-history-list");
const items = () => Array.from(listEl().querySelectorAll(".recent-item"));
console.log("AI history sidebar filter (#98) — config.js");
await test("loadAISessionList renders every session", async () => {
await loadAISessionList();
assert.equal(items().length, 3, "three sessions rendered");
});
await test("filter narrows by title, case-insensitively", async () => {
filterAIHistory("pizza");
assert.equal(items().length, 1, "one session matches 'pizza'");
filterAIHistory("PLAN");
assert.equal(items().length, 1, "case-insensitive match on 'PLAN'");
});
await test("filter matches accents-insensitively", async () => {
filterAIHistory("cafe");
assert.equal(items().length, 1, "'cafe' matches 'café au lait'");
const titles = items().map((el) => el.textContent);
assert.ok(titles.some((t) => t.includes("café")), "the café session is the one kept");
});
await test("filter matches the preview and the directory", async () => {
filterAIHistory("pâte");
assert.equal(items().length, 1, "'pâte' matches the preview text");
filterAIHistory("recettes");
assert.equal(items().length, 1, "'recettes' matches the session directory");
});
await test("clearing the query restores the full list", async () => {
filterAIHistory("");
assert.equal(items().length, 3, "no query keeps everything");
});
await test("an unmatched query shows a no-match hint, not the empty state", async () => {
filterAIHistory("zzz-inexistant");
assert.equal(items().length, 0, "no item left");
assert.ok(listEl().querySelector(".sidebar-filter-empty"),
"a 'no match' hint is rendered in the list box");
const emptyEl = document.getElementById("ai-history-empty");
assert.ok(emptyEl.classList.contains("hidden") === false, "empty state is also visible");
});
// ── Summary ──
console.log(`\n${passCount}/${testCount} tests passed`);
if (passCount !== testCount) {
process.exit(1);
}