| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | insertRankedSearchResult, |
| 5 | normalizeSearchQuery, |
| 6 | scoreQueryMatch, |
| 7 | scoreSubsequenceMatch, |
| 8 | } from "./searchRanking.ts"; |
| 9 | |
| 10 | describe("normalizeSearchQuery", () => { |
| 11 | it("trims and lowercases queries", () => { |
| 12 | expect(normalizeSearchQuery(" UI ")).toBe("ui"); |
| 13 | }); |
| 14 | |
| 15 | it("can strip leading trigger characters", () => { |
| 16 | expect(normalizeSearchQuery(" $ui", { trimLeadingPattern: /^\$+/ })).toBe("ui"); |
| 17 | }); |
| 18 | }); |
| 19 | |
| 20 | describe("scoreQueryMatch", () => { |
| 21 | it("prefers exact matches over broader contains matches", () => { |
| 22 | expect( |
| 23 | scoreQueryMatch({ |
| 24 | value: "ui", |
| 25 | query: "ui", |
| 26 | exactBase: 0, |
| 27 | prefixBase: 10, |
| 28 | includesBase: 20, |
| 29 | }), |
| 30 | ).toBe(0); |
| 31 | |
| 32 | expect( |
| 33 | scoreQueryMatch({ |
| 34 | value: "building native ui", |
| 35 | query: "ui", |
| 36 | exactBase: 0, |
| 37 | prefixBase: 10, |
| 38 | boundaryBase: 20, |
| 39 | includesBase: 30, |
| 40 | }), |
| 41 | ).toBeGreaterThan(0); |
| 42 | }); |
| 43 | |
| 44 | it("treats boundary matches as stronger than generic contains matches", () => { |
| 45 | const boundaryScore = scoreQueryMatch({ |
| 46 | value: "gh-fix-ci", |
| 47 | query: "fix", |
| 48 | exactBase: 0, |
| 49 | prefixBase: 10, |
| 50 | boundaryBase: 20, |
| 51 | includesBase: 30, |
| 52 | boundaryMarkers: ["-"], |
| 53 | }); |
| 54 | const containsScore = scoreQueryMatch({ |
| 55 | value: "highfixci", |
| 56 | query: "fix", |
| 57 | exactBase: 0, |
| 58 | prefixBase: 10, |
| 59 | boundaryBase: 20, |
| 60 | includesBase: 30, |
| 61 | boundaryMarkers: ["-"], |
| 62 | }); |
| 63 | |
| 64 | expect(boundaryScore).not.toBeNull(); |
| 65 | expect(containsScore).not.toBeNull(); |
| 66 | expect(boundaryScore!).toBeLessThan(containsScore!); |
| 67 | }); |
| 68 | }); |
| 69 | |
| 70 | describe("scoreSubsequenceMatch", () => { |
| 71 | it("scores tighter subsequences ahead of looser ones", () => { |
| 72 | const compact = scoreSubsequenceMatch("ghfixci", "gfc"); |
| 73 | const spread = scoreSubsequenceMatch("github-fix-ci", "gfc"); |
| 74 | |
| 75 | expect(compact).not.toBeNull(); |
| 76 | expect(spread).not.toBeNull(); |
| 77 | expect(compact!).toBeLessThan(spread!); |
| 78 | }); |
| 79 | }); |
| 80 | |
| 81 | describe("insertRankedSearchResult", () => { |
| 82 | it("keeps the best-ranked candidates within the limit", () => { |
| 83 | const ranked = [ |
| 84 | { item: "b", score: 20, tieBreaker: "b" }, |
| 85 | { item: "d", score: 40, tieBreaker: "d" }, |
| 86 | ]; |
| 87 | |
| 88 | insertRankedSearchResult(ranked, { item: "a", score: 10, tieBreaker: "a" }, 2); |
| 89 | insertRankedSearchResult(ranked, { item: "c", score: 30, tieBreaker: "c" }, 2); |
| 90 | |
| 91 | expect(ranked.map((entry) => entry.item)).toEqual(["a", "b"]); |
| 92 | }); |
| 93 | }); |