feat(search): recherche semantique - embeddings vectoriels + hybride RRF (#70)
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
#!/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);
|
||||
Reference in New Issue
Block a user