import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import test from "node:test"; const requireCore = createRequire(new URL("../../../govoplan-core/webui/package.json", import.meta.url)); const ts = requireCore("typescript"); function loadTypeScript(relativePath, dependencies = {}) { const source = readFileSync(new URL(relativePath, import.meta.url), "utf8"); const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS, } }); const module = { exports: {} }; new Function("require", "module", "exports", outputText)((id) => { if (id in dependencies) return dependencies[id]; throw new Error(`Unexpected runtime dependency: ${id}`); }, module, module.exports); return module.exports; } const { readSearchFilter, writeSearchFilter, effectiveSearchFilters, searchFilterOptions } = loadTypeScript("../src/components/searchFilters.ts"); test("legacy missing filters remain all; explicit none wins over repeated values", () => { assert.equal(readSearchFilter(new URLSearchParams("q=permit"), "module"), null); assert.deepEqual(readSearchFilter(new URLSearchParams("module=files&module=cases&module=files"), "module"), ["cases", "files"]); assert.deepEqual(readSearchFilter(new URLSearchParams("module=cases&module_none=1"), "module"), []); assert.deepEqual(readSearchFilter(new URLSearchParams("resource_type=case&resource_type_none=1"), "resource_type"), []); }); test("filter URL roundtrips preserve query, context, language and the other filter", () => { const original = new URLSearchParams("q=permit&context=cases.current&language=german&resource_type=case&other=keep"); const selected = writeSearchFilter(original, "module", ["files", "cases", "files"]); assert.deepEqual(selected.getAll("module"), ["cases", "files"]); assert.equal(original.has("module"), false); const none = writeSearchFilter(selected, "module", []); assert.equal(none.get("module_none"), "1"); assert.equal(none.has("module"), false); assert.deepEqual(readSearchFilter(none, "module"), []); const all = writeSearchFilter(none, "module", null); assert.equal(all.toString(), original.toString()); assert.equal(readSearchFilter(all, "module"), null); }); test("global all, none and subset selections keep distinct effective requests", () => { assert.deepEqual(effectiveSearchFilters(null, null), { modules: undefined, resourceTypes: undefined, matchNone: false }); assert.equal(effectiveSearchFilters([], null).matchNone, true); assert.equal(effectiveSearchFilters(null, []).matchNone, true); assert.deepEqual(effectiveSearchFilters(["cases", "files"], ["case", "file"]), { modules: ["cases", "files"], resourceTypes: ["case", "file"], matchNone: false, }); }); test("context bounds never broaden an empty resource-type intersection", () => { const context = { id: "files.context", moduleId: "files", label: "Files", pathPrefixes: ["/files"], resourceTypes: ["file", "folder"] }; assert.deepEqual(effectiveSearchFilters(null, null, context), { modules: ["files"], resourceTypes: ["file", "folder"], matchNone: false }); assert.deepEqual(effectiveSearchFilters(["cases"], ["case", "file"], context), { modules: ["files"], resourceTypes: ["file"], matchNone: false }); assert.deepEqual(effectiveSearchFilters(null, ["case"], context), { modules: ["files"], resourceTypes: [], matchNone: true }); assert.equal(effectiveSearchFilters(null, [], context).matchNone, true); // The hidden global module selection must not override the current context. assert.equal(effectiveSearchFilters([], null, context).matchNone, false); assert.equal(effectiveSearchFilters(null, null, { ...context, resourceTypes: [] }).matchNone, false); }); const catalogue = [ { provider_id: "files.source", module_id: "files", resource_type: "file", label: "Files", order: 1 }, { provider_id: "files.source", module_id: "files", resource_type: "folder", label: "Folders", order: 2 }, { provider_id: "cases.source", module_id: "cases", resource_type: "case", label: "Cases", order: 3 }, ]; const observed = [{ module_id: "mail", resource_type: "message" }]; const labels = new Map([["files", "Dateien"], ["cases", "Fälle"]]); test("filter choices combine catalogue, observed values and removable stale selections", () => { const all = searchFilterOptions(catalogue, observed, labels, ["files", "retired-source"], ["unknown-type"]); assert.deepEqual(new Set(all.modules.map((item) => item.value)), new Set(["files", "cases", "mail", "retired-source"])); assert.equal(all.modules.find((item) => item.value === "files").label, "Dateien"); assert.deepEqual(new Set(all.resourceTypes.map((item) => item.value)), new Set(["file", "folder", "unknown-type"])); assert.deepEqual(searchFilterOptions(catalogue, observed, labels, [], null).resourceTypes, []); const context = { id: "files.context", moduleId: "files", label: "Files", pathPrefixes: ["/files"], resourceTypes: ["file"] }; assert.deepEqual(searchFilterOptions(catalogue, observed, labels, null, null, context).resourceTypes, [{ value: "file", label: "Files" }]); }); test("explicit none is a frontend-only empty response; legacy API empty arrays still request all", async () => { const paths = [], calls = []; const response = { query: "permit", results: [], diagnostics: [], next_cursor: null, has_more: false }; const { search } = loadTypeScript("../src/api/search.ts", { "@govoplan/core-webui": { apiPath: (path, parameters) => { paths.push({ path, parameters }); return path; }, apiFetch: async (...args) => { calls.push(args); return response; }, } }); const settings = { apiBaseUrl: "", apiKey: "", accessToken: "" }; assert.deepEqual(await search(settings, { query: "permit", matchNone: true, modules: [] }), response); assert.equal(calls.length, 0); assert.equal(paths.length, 0); await search(settings, { query: "permit", modules: [], resourceTypes: [] }); assert.equal(calls.length, 1); assert.deepEqual(paths[0].parameters.module, []); assert.deepEqual(paths[0].parameters.resource_type, []); assert.equal("matchNone" in paths[0].parameters, false); await search(settings, { query: "permit", modules: ["cases", "files"], resourceTypes: ["case", "file"] }); assert.deepEqual(paths[1].parameters.module, ["cases", "files"]); assert.deepEqual(paths[1].parameters.resource_type, ["case", "file"]); });