143 lines
5.0 KiB
JavaScript
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);
|
|
} |