#!/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( `
`, { 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);