/* @vitest-environment node */ import { describe, it, expect } from "vitest"; import { SEARCH_SCOPE, SEARCH_LIMITS, DEFAULT_SEARCH_LIMIT, parseBranchesCsv, serializeBranchesCsv, parseSearchUrlState, serializeSearchUrlState, } from "./urlState.js"; describe("lib/frontend/search/urlState", () => { describe("parseBranchesCsv / serializeBranchesCsv", () => { it("parses CSV into a unique, trimmed, sorted list", () => { expect(parseBranchesCsv(" NL20, NL06 ,NL06,, ")).toEqual([ "NL06", "NL20", ]); }); it("normalizes to uppercase (fail-open) and sorts deterministically", () => { expect(parseBranchesCsv("nl20, NL06")).toEqual(["NL06", "NL20"]); }); it("serializes branches into CSV (sorted + deduped)", () => { expect(serializeBranchesCsv(["NL20", " NL06 ", "NL20"])).toBe( "NL06,NL20" ); }); it("returns null when serializing empty branches", () => { expect(serializeBranchesCsv([])).toBe(null); expect(serializeBranchesCsv(null)).toBe(null); }); }); describe("parseSearchUrlState", () => { it("defaults to SINGLE with routeBranch when no params are present", () => { const sp = new URLSearchParams(); const state = parseSearchUrlState(sp, { routeBranch: "NL01" }); expect(state).toEqual({ q: null, scope: SEARCH_SCOPE.SINGLE, branch: "NL01", branches: [], limit: DEFAULT_SEARCH_LIMIT, from: null, to: null, }); }); it("parses SINGLE with explicit branch param (legacy URL)", () => { const sp = new URLSearchParams({ q: " test ", branch: "nl02" }); const state = parseSearchUrlState(sp, { routeBranch: "NL01" }); expect(state.scope).toBe(SEARCH_SCOPE.SINGLE); expect(state.q).toBe("test"); expect(state.branch).toBe("NL02"); expect(state.branches).toEqual([]); expect(SEARCH_LIMITS.includes(state.limit)).toBe(true); }); it("parses ALL when scope=all is set (highest precedence)", () => { const sp = new URLSearchParams({ q: "x", scope: "all", branch: "NL01", branches: "NL06,NL20", limit: "200", }); const state = parseSearchUrlState(sp, { routeBranch: "NL99" }); expect(state).toEqual({ q: "x", scope: SEARCH_SCOPE.ALL, branch: null, branches: [], limit: 200, from: null, to: null, }); }); it("parses MULTI when scope=multi is set", () => { const sp = new URLSearchParams({ q: " reifen ", scope: "multi", branches: "NL06, NL20, NL06", limit: "50", }); const state = parseSearchUrlState(sp, { routeBranch: "NL01" }); expect(state.scope).toBe(SEARCH_SCOPE.MULTI); expect(state.q).toBe("reifen"); expect(state.branch).toBe(null); expect(state.branches).toEqual(["NL06", "NL20"]); expect(state.limit).toBe(50); }); it("parses MULTI when branches=... is present even without scope", () => { const sp = new URLSearchParams({ q: "x", branches: "NL20,NL06", }); const state = parseSearchUrlState(sp, { routeBranch: "NL01" }); expect(state.scope).toBe(SEARCH_SCOPE.MULTI); expect(state.branches).toEqual(["NL06", "NL20"]); expect(state.limit).toBe(DEFAULT_SEARCH_LIMIT); }); it("keeps from/to when provided", () => { const sp = new URLSearchParams({ q: "x", branch: "NL01", from: "2025-12-01", to: "2025-12-31", limit: "200", }); const state = parseSearchUrlState(sp, { routeBranch: "NL01" }); expect(state.from).toBe("2025-12-01"); expect(state.to).toBe("2025-12-31"); expect(state.limit).toBe(200); }); it("falls back to default limit for invalid values", () => { const sp = new URLSearchParams({ q: "x", branch: "NL01", limit: "999" }); const state = parseSearchUrlState(sp, { routeBranch: "NL01" }); expect(state.limit).toBe(DEFAULT_SEARCH_LIMIT); }); }); describe("serializeSearchUrlState", () => { it("serializes SINGLE as q only (path branch is SoT)", () => { const qs = serializeSearchUrlState({ q: "bridgestone", scope: SEARCH_SCOPE.SINGLE, branch: "NL01", limit: DEFAULT_SEARCH_LIMIT, }); expect(qs).toBe("q=bridgestone"); }); it("serializes MULTI as q + scope=multi + branches", () => { const qs = serializeSearchUrlState({ q: "reifen", scope: SEARCH_SCOPE.MULTI, branches: ["NL20", "NL06"], limit: DEFAULT_SEARCH_LIMIT, }); expect(qs).toBe("q=reifen&scope=multi&branches=NL06%2CNL20"); }); it("serializes ALL as q + scope=all", () => { const qs = serializeSearchUrlState({ q: "x", scope: SEARCH_SCOPE.ALL, limit: DEFAULT_SEARCH_LIMIT, }); expect(qs).toBe("q=x&scope=all"); }); it("includes limit when non-default", () => { const qs = serializeSearchUrlState({ q: "x", scope: SEARCH_SCOPE.SINGLE, branch: "NL01", limit: 200, }); expect(qs).toBe("q=x&limit=200"); }); it("includes from/to when present (future-proof for RHL-025)", () => { const qs = serializeSearchUrlState({ q: "x", scope: SEARCH_SCOPE.SINGLE, branch: "NL01", limit: 200, from: "2025-12-01", to: "2025-12-31", }); expect(qs).toBe("q=x&limit=200&from=2025-12-01&to=2025-12-31"); }); }); });