Files
ObsiGate/tests/frontend/sidebar-filters.test.mjs
T
bruno 4231f2e929
CI / lint (push) Failing after 1m20s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 59s
feat(sidebar+assistant): filtre Recents/Sauvegardes (#99), pastille Deep Research (#100) et icone du bouton + (BUG-049)
2026-09-16 20:16:41 -04:00

183 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
/**
* ObsiGate - JSDOM tests for the sidebar search/filter bar on the
* "Recent" and "Saved searches" tabs (#99).
*
* Covers:
* - `filterRecentFiles()` (config.js): narrows the recent-files list by
* title / path / vault / preview / tags, case- and accent-insensitively
* - `filterSavedSearches()` (viewer.js): narrows the saved-searches list and
* combines with the type pills, with a "no match" hint when empty
*
* Usage: node tests/frontend/sidebar-filters.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="recent-list" class="recent-list"></div>
<div id="recent-empty" class="recent-empty hidden"></div>
<select id="recent-vault-filter"></select>
<div class="saved-filter-bar" id="saved-filter-bar">
<button class="saved-filter-pill active" data-filter="all">Tous</button>
<button class="saved-filter-pill" data-filter="search">Recherches</button>
<button class="saved-filter-pill" data-filter="directory">Repertoires</button>
</div>
<div id="saved-searches-list" class="recent-list"></div>
<div id="saved-searches-empty" class="recent-empty"></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 RECENT = [
{ vault: "V", path: "Recettes/Pizza.md", title: "Pizza maison", preview: "pâte", tags: ["cuisine"], mtime_human: "il y a 1 h" },
{ vault: "V", path: "Notes/café.md", title: "Café du matin", preview: "arôme", tags: ["journal"], mtime_human: "il y a 2 h" },
{ vault: "W", path: "Projets/plan.md", title: "Plan de projet", preview: "étapes", tags: [], mtime_human: "hier" },
];
const SAVED = [
{ id: "s1", query: "pizza", vault: "V", include_paths: "Recettes", case_sensitive: false },
{ id: "s2", query: "", include_paths: "Recettes", vault: "V" },
{ id: "s3", query: "roadmap", vault: "all" },
];
globalThis.fetch = async (url) => {
const u = String(url);
if (u.includes("/api/recent")) {
return { ok: true, status: 200, json: async () => ({ files: RECENT }) };
}
if (u.includes("/api/saved-searches")) {
return { ok: true, status: 200, json: async () => SAVED };
}
return { ok: true, status: 200, json: async () => ({}) };
};
const configMod = await import(pathToFileURL(path.join(JS_DIR, "config.js")).href);
const viewerMod = await import(pathToFileURL(path.join(JS_DIR, "viewer.js")).href);
const { loadRecentFiles, filterRecentFiles } = configMod;
const { loadSavedSearches, filterSavedSearches } = viewerMod;
const recentItems = () => Array.from(document.querySelectorAll("#recent-list .recent-item"));
const visibleRecent = () => recentItems().filter((el) => el.style.display !== "none").length;
const savedItems = () => Array.from(document.querySelectorAll(".saved-search-item"));
const visibleSaved = () => savedItems().filter((el) => el.style.display !== "none").length;
console.log("Sidebar filters (#99) — Recent & Saved searches");
await test("loadRecentFiles renders every recent file", async () => {
await loadRecentFiles(null);
assert.equal(recentItems().length, 3, "three recent items rendered");
});
await test("filterRecentFiles narrows by title, case-insensitively", async () => {
filterRecentFiles("pizza");
assert.equal(visibleRecent(), 1, "one recent item matches 'pizza'");
filterRecentFiles("PLAN");
assert.equal(visibleRecent(), 1, "case-insensitive match on 'PLAN'");
});
await test("filterRecentFiles matches accents and tags", async () => {
filterRecentFiles("cafe");
assert.equal(visibleRecent(), 1, "'cafe' matches 'Café du matin'");
filterRecentFiles("journal");
assert.equal(visibleRecent(), 1, "tag match");
});
await test("filterRecentFiles matches the vault and clears", async () => {
filterRecentFiles("Projets");
assert.equal(visibleRecent(), 1, "path match");
filterRecentFiles("");
assert.equal(visibleRecent(), 3, "no query restores everything");
});
await test("filterRecentFiles shows a no-match hint", async () => {
filterRecentFiles("zzz-inexistant");
assert.equal(visibleRecent(), 0, "no item left");
assert.ok(document.querySelector("#recent-list .sidebar-filter-empty"),
"a no-match hint is rendered in the recent list");
filterRecentFiles("");
});
await test("loadSavedSearches renders every saved search", async () => {
await loadSavedSearches();
assert.equal(savedItems().length, 3, "three saved searches rendered");
});
await test("filterSavedSearches narrows by query text", async () => {
filterSavedSearches("pizza");
assert.equal(visibleSaved(), 1, "one saved search matches 'pizza'");
filterSavedSearches("roadmap");
assert.equal(visibleSaved(), 1, "match on the query field");
});
await test("filterSavedSearches combines with the type pills", async () => {
filterSavedSearches("");
assert.equal(visibleSaved(), 3, "all visible before the pill filter");
document.querySelector('.saved-filter-pill[data-filter="directory"]').click();
assert.equal(visibleSaved(), 1, "only the directory search stays visible");
filterSavedSearches("roadmap");
assert.equal(visibleSaved(), 0, "the query excludes the directory search");
assert.ok(document.querySelector("#saved-searches-list .sidebar-filter-empty"),
"a no-match hint is rendered in the saved list");
document.querySelector('.saved-filter-pill[data-filter="all"]').click();
filterSavedSearches("");
assert.equal(visibleSaved(), 3, "clearing both filters restores everything");
});
// ── Summary ──
console.log(`\n${passCount}/${testCount} tests passed`);
if (passCount !== testCount) {
process.exit(1);
}