feat: facette Extensions dans les facettes de recherche #157
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
#!/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(
|
||||
`<!DOCTYPE html>
|
||||
<html><body>
|
||||
<input id="search-input" value="" />
|
||||
<select id="vault-filter"><option value="all">all</option></select>
|
||||
<div id="results-bar" style="display:none"></div>
|
||||
<span id="rb-count"></span>
|
||||
<div id="content-area"></div>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -225,6 +225,14 @@ class TestAdvancedSearchAPI:
|
||||
resp = client.get("/api/advanced-search?q=python&vault=all")
|
||||
data = self._check(resp)
|
||||
assert "facets" in data
|
||||
# #157 — extension facet: keys are query-ready (no leading dot) and,
|
||||
# when there are results, at least one extension is counted.
|
||||
exts = data["facets"]["extensions"]
|
||||
assert isinstance(exts, dict)
|
||||
assert all(not e.startswith(".") for e in exts)
|
||||
if data["total"] > 0:
|
||||
assert len(exts) >= 1
|
||||
assert sum(exts.values()) <= data["total"]
|
||||
|
||||
def test_empty_query(self, client):
|
||||
resp = client.get("/api/advanced-search?q=")
|
||||
|
||||
Reference in New Issue
Block a user