// @vitest-environment node // No DOM needed — jsdom is not installed in this workspace (same banner // convention as utils.test.ts); these are pure-logic tests for TASK-0098. import { describe, expect, it } from "vitest"; import { RECENT_CAP, addRecent, deepLinkFor, groupResultsByType, isRecordableQuery, paletteSearchResult, recentItemHref, savedSearchLabel } from "../search"; import type { SearchResult, SearchType } from "../types"; /** A minimal wire-shaped ResultItem (Phase-8 contract). */ function item(partial: Partial & { type: SearchType; id: string }): SearchResult { return { docId: `${partial.type}:${partial.id}`, title: `Title ${partial.id}`, updatedAt: "2025-01-01T00:00:00.000Z", workspaceId: "ws-1", score: 1, source: "opensearch", ...partial } as SearchResult; } describe("groupResultsByType", () => { it("groups by the 7 known types in the fixed display order", () => { const groups = groupResultsByType([ item({ type: "file", id: "f1" }), item({ type: "page", id: "p1" }), item({ type: "record", id: "r1" }), item({ type: "page", id: "p2" }), item({ type: "user", id: "u1" }), item({ type: "comment", id: "c1" }), item({ type: "block", id: "b1" }), item({ type: "database", id: "d1" }) ]); expect(groups.map((g) => g.type)).toEqual([ "page", "database", "record", "block", "comment", "file", "user" ]); }); it("keeps intra-group relevance order and counts", () => { const groups = groupResultsByType([ item({ type: "page", id: "p1" }), item({ type: "record", id: "r1" }), item({ type: "page", id: "p2" }), item({ type: "record", id: "r2" }), item({ type: "page", id: "p3" }) ]); expect(groups).toHaveLength(2); const [pages, records] = groups; expect(pages?.items.map((i) => i.id)).toEqual(["p1", "p2", "p3"]); expect(records?.items).toHaveLength(2); }); it("drops items of unknown types and returns [] for empty input", () => { expect(groupResultsByType([])).toEqual([]); const unknownType = "nope" as unknown as SearchType; const groups = groupResultsByType([ item({ type: "page", id: "p1" }), item({ type: unknownType, id: "x1" }) ]); expect(groups).toHaveLength(1); expect(groups[0]?.items).toHaveLength(1); }); }); describe("deepLinkFor", () => { it("page → /w/{slug}/page/{id}", () => { expect(deepLinkFor(item({ type: "page", id: "p1" }), "acme")).toBe("/w/acme/page/p1"); }); it("database → /w/{slug}/databases/{id}; record → containing database (databaseId ?? id)", () => { expect(deepLinkFor(item({ type: "database", id: "d1" }), "acme")).toBe("/w/acme/databases/d1"); expect(deepLinkFor(item({ type: "record", id: "r1", databaseId: "d9" }), "acme")).toBe("/w/acme/databases/d9"); expect(deepLinkFor(item({ type: "record", id: "r2" }), "acme")).toBe("/w/acme/databases/r2"); }); it("block → parent page when present, null otherwise; null slug → null", () => { expect(deepLinkFor(item({ type: "block", id: "b1", pageId: "p7" }), "acme")).toBe("/w/acme/page/p7"); expect(deepLinkFor(item({ type: "block", id: "b2" }), "acme")).toBeNull(); expect(deepLinkFor(item({ type: "page", id: "p1" }), null)).toBeNull(); }); it("comment/file/user → item url, else containing page, else null", () => { expect(deepLinkFor(item({ type: "comment", id: "c1", url: "/w/acme/page/p3#comment-c1" }), "acme")).toBe( "/w/acme/page/p3#comment-c1" ); expect(deepLinkFor(item({ type: "file", id: "f1", pageId: "p5" }), "acme")).toBe("/w/acme/page/p5"); expect(deepLinkFor(item({ type: "user", id: "u1" }), "acme")).toBeNull(); }); }); describe("addRecent (recent-list semantics — mirrors PUT /search/recent)", () => { it("prepends, dedupes, newest first", () => { expect(addRecent([], "alpha")).toEqual(["alpha"]); expect(addRecent(["alpha"], "beta")).toEqual(["beta", "alpha"]); expect(addRecent(["beta", "alpha"], "alpha")).toEqual(["alpha", "beta"]); }); it("trims and ignores blank queries", () => { expect(addRecent(["x"], " ")).toEqual(["x"]); expect(addRecent(["x"], null)).toEqual(["x"]); expect(addRecent([], " gamma ")).toEqual(["gamma"]); }); it("caps at RECENT_CAP (20) keeping the newest, dropping the oldest", () => { const list = Array.from({ length: RECENT_CAP }, (_, i) => `q${i}`); // q0 = newest … q19 = oldest const next = addRecent(list, "newest"); expect(next).toHaveLength(RECENT_CAP); expect(next[0]).toBe("newest"); expect(next).not.toContain(`q${RECENT_CAP - 1}`); // the oldest (q19) is evicted expect(next).toContain("q0"); // the second-oldest survives }); }); describe("isRecordableQuery / paletteSearchResult", () => { it("only records non-blank queries", () => { expect(isRecordableQuery("x")).toBe(true); expect(isRecordableQuery(" ")).toBe(false); expect(isRecordableQuery("")).toBe(false); }); it("keeps a database hit's id (the /databases/:id route takes the database id) and adds a path hint", () => { const mapped = paletteSearchResult(item({ type: "database", id: "d1", pageId: "p1" })); expect(mapped.id).toBe("d1"); // /w/:ws/databases/:id → the database id expect(mapped.path).toEqual(["Database"]); }); it("maps a record hit to its containing database (the palette's navigate target)", () => { const mapped = paletteSearchResult(item({ type: "record", id: "r1", databaseId: "d9" })); expect(mapped.id).toBe("d9"); expect(mapped.path).toEqual(["Database record"]); }); it("maps a block hit to its containing page (the palette's navigate target)", () => { const mapped = paletteSearchResult(item({ type: "block", id: "b1", pageId: "p7" })); expect(mapped.id).toBe("p7"); expect(mapped.path).toEqual(["Block"]); }); it("keeps a plain page hit's id and adds no path", () => { const mapped = paletteSearchResult(item({ type: "page", id: "p1" })); expect(mapped.id).toBe("p1"); expect(mapped.path).toBeUndefined(); }); }); describe("recentItemHref (T-153 recently-viewed)", () => { it("routes databases to /databases/:id and every other kind to /page/:id", () => { expect(recentItemHref({ itemType: "database", refId: "d1", at: "" }, "w")).toBe("/w/w/databases/d1"); expect(recentItemHref({ itemType: "page", refId: "p1", at: "" }, "w")).toBe("/w/w/page/p1"); expect(recentItemHref({ itemType: "whiteboard", refId: "p2", at: "" }, "w")).toBe("/w/w/page/p2"); expect(recentItemHref({ itemType: "task", refId: "p3", at: "" }, "w")).toBe("/w/w/page/p3"); }); it("URL-encodes the workspace slug and the id", () => { expect(recentItemHref({ itemType: "page", refId: "a b", at: "" }, "my ws")).toBe("/w/my%20ws/page/a%20b"); }); it("returns null when there is no workspace to scope to", () => { expect(recentItemHref({ itemType: "page", refId: "p1", at: "" }, "")).toBeNull(); }); }); describe("savedSearchLabel (T-154 saved searches)", () => { it("uses the query, truncating long ones", () => { expect(savedSearchLabel({ q: "pricing" })).toBe("pricing"); expect(savedSearchLabel({ q: " ".repeat(50) })).toBe("Saved search"); // trims to empty const long = "x".repeat(60); const label = savedSearchLabel({ q: long }); expect(label.length).toBe(40); expect(label.endsWith("…")).toBe(true); }); it("falls back for a null/empty query", () => { expect(savedSearchLabel(null)).toBe("Saved search"); expect(savedSearchLabel({ q: "" })).toBe("Saved search"); expect(savedSearchLabel(undefined, "Custom")).toBe("Custom"); }); });