#!/usr/bin/env node /** * ObsiGate - JSDOM tests for the search results facets panel (#157): * vault, tag and file-extension facets rendered by * `renderAdvancedSearchResults()` (frontend/js/search.js). * * Covers: * - the three facet groups are rendered (Vaults / Tags / Extensions) * - the extensions facet appears even when tags and vaults are empty * - clicking an extension replaces any `ext:` operator in the search * input and triggers a new search * * Usage: node tests/frontend/search-facets.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; } } // Backend payload shape for /api/search/advanced (facets only matter here). const PAYLOAD = { results: [], total: 0, offset: 0, limit: 50, query_time_ms: 1, facets: { vaults: { IT: 4, Recettes: 2 }, tags: { linux: 3 }, extensions: { md: 5, pdf: 2, xlsx: 1 }, }, }; globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => PAYLOAD, }); const { renderAdvancedSearchResults } = await import( pathToFileURL(path.join(JS_DIR, "search.js")).href ); const facetGroups = () => Array.from(document.querySelectorAll(".search-facets__group")); const extGroup = () => document.querySelector('.search-facets__group[data-facet="extensions"]'); const extItems = () => Array.from(extGroup()?.querySelectorAll(".search-facets__item") || []); console.log("Search facets (#157) — Vaults / Tags / Extensions"); await test("renders the three facet groups", async () => { renderAdvancedSearchResults(PAYLOAD, "python", null); assert.equal(facetGroups().length, 3, "vault, tag and extension groups"); const labels = facetGroups().map((g) => g.querySelector(".search-facets__label").textContent, ); assert.deepEqual(labels, ["Vaults", "Tags", "Extensions"]); }); await test("lists extensions with their counts", async () => { const texts = extItems().map((i) => i.textContent.trim()); assert.deepEqual(texts, [".md 5", ".pdf 2", ".xlsx 1"]); assert.equal( extItems()[0].querySelector(".facet-count").textContent, "5", "count lives in .facet-count", ); }); await test("extension facet shows even with empty tags and vaults", async () => { renderAdvancedSearchResults( { ...PAYLOAD, facets: { vaults: {}, tags: {}, extensions: { md: 1 } } }, "", null, ); assert.ok(extGroup(), "extensions group rendered alone"); assert.equal(extItems().length, 1); }); await test("clicking an extension adds the ext: operator", async () => { document.getElementById("search-input").value = "python ext:md"; renderAdvancedSearchResults(PAYLOAD, "python", null); extItems()[1].click(); // .pdf assert.equal( document.getElementById("search-input").value, "python ext:pdf", "existing ext: operator replaced, not duplicated", ); }); // ── Summary ── console.log(`\n${passCount}/${testCount} tests passed`); if (passCount !== testCount) { process.exit(1); }