183 lines
7.0 KiB
JavaScript
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);
|
|
} |