#!/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( ` `, { 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"); }); // ── #194 : icône du dossier personnel ── await test("getVaultIcon returns the lucide house icon for home-* vaults", async () => { const homeIcon = viewerMod.getVaultIcon("home-bruno", 16); assert.equal(homeIcon.tagName.toLowerCase(), "i", "placeholder lucide (comme les autres icônes)"); assert.equal(homeIcon.getAttribute("data-lucide"), "home", "icône maison lucide"); assert.equal(homeIcon.style.width, "16px", "taille demandée respectée"); }); await test("getVaultIcon is unchanged for regular vaults", async () => { const vaultIcon = viewerMod.getVaultIcon("Recettes", 16); assert.equal(vaultIcon.tagName.toLowerCase(), "svg", "gemme violette SVG d'origine"); }); // ── Summary ── console.log(`\n${passCount}/${testCount} tests passed`); if (passCount !== testCount) { process.exit(1); }