Files
ObsiGate/tests/frontend/semantic-search.test.mjs
T

123 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
/**
* ObsiGate - JSDOM tests for the semantic search toggle (ROADMAP #70).
*
* Verifies the "Recherche sémantique" toggle wiring in frontend/js/search.js:
* - the toggle is disabled until the backend reports semantic availability
* - clicking it flips state.semanticSearch and the active class
* - clicking while unavailable is a no-op
*
* Usage: node tests/frontend/semantic-search.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 REPO_ROOT = path.resolve(__dirname, "..", "..");
// ── JSDOM bootstrap ────────────────────────────────────────────────────────
const dom = new JSDOM(
`<!DOCTYPE html><html><body>
<input id="search-input" type="text" />
<select id="vault-filter"><option value="all">all</option></select>
<div id="results-bar" style="display:none">
<span id="rb-count">0/0</span>
<button id="rb-prev"></button>
<button id="rb-next"></button>
<button id="rb-case"></button>
<button id="rb-word"></button>
<button id="rb-regex"></button>
<button id="rb-semantic" disabled></button>
</div>
<button id="search-filter-btn"></button>
<button id="search-clear-btn"></button>
<div id="search-filter-row" style="display:none">
<input id="search-include-input" />
<input id="search-exclude-input" />
</div>
<div id="content-area"></div>
<div id="search-chips"></div>
</body></html>`,
{ url: "https://example.com/", 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;
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);
globalThis.fetch = async () => ({ ok: true, json: async () => ({}) });
const stateMod = await import(
pathToFileURL(path.join(REPO_ROOT, "frontend", "js", "state.js")).href
);
const { state } = stateMod;
const searchMod = await import(
pathToFileURL(path.join(REPO_ROOT, "frontend", "js", "search.js")).href
);
// ── Test harness ───────────────────────────────────────────────────────────
let testCount = 0;
let passCount = 0;
async function test(name, fn) {
testCount++;
try {
await fn();
console.log(` \u2713 ${name}`);
passCount++;
} catch (e) {
console.log(` \u2717 ${name}`);
console.log(` ${e.message}`);
}
}
searchMod.initSearch();
const semanticBtn = document.getElementById("rb-semantic");
await test("semantic toggle is disabled until availability is known", () => {
assert.equal(semanticBtn.disabled, true);
});
await test("clicking while unavailable does not toggle", () => {
semanticBtn.click();
assert.equal(state.semanticSearch, false);
});
await test("clicking while available toggles state and active class", () => {
state.semanticAvailable = true;
semanticBtn.disabled = false;
semanticBtn.click();
assert.equal(state.semanticSearch, true);
assert.ok(semanticBtn.classList.contains("active"));
semanticBtn.click();
assert.equal(state.semanticSearch, false);
assert.ok(!semanticBtn.classList.contains("active"));
});
await test("semantic state keys exist", () => {
assert.equal(state.semanticAvailable, true);
assert.equal(typeof state.semanticSearch, "boolean");
});
console.log(`\n${passCount}/${testCount} tests passed`);
if (passCount !== testCount) process.exit(1);