import { describe, expect, it } from "vitest"; import { searchItems } from "./search"; const items = [ { name: "Mualani", aliases: ["Shark Surfer"], overview: "A hydro guide", key: "internal" }, { name: "Obsidian Codex", aliases: ["Codex"], overview: "An artifact", key: "obsidian" }, ]; const search = (query: string) => searchItems(items, query, (item) => `${item.name} ${item.aliases.join(" ")} ${item.overview} ${item.key}`, (item) => [item.name, ...item.aliases]); const names = (items: string[], query: string) => searchItems(items, query, (item) => item, (item) => [item]); describe("fuzzy search fallback", () => { it("preserves direct substring matches, case, whitespace, and empty queries", () => { expect(search(" MUALA ")).toEqual({ items: [items[0]], fuzzy: false }); expect(search(" ")).toEqual({ items, fuzzy: false }); expect(search("hydro").items).toEqual([items[0]]); }); it("does not append typo matches when another item matches directly", () => { expect(names(["Mualani", "Mulani Guide"], "mulani")).toEqual({ items: ["Mulani Guide"], fuzzy: false }); }); it.each(["mulani", "muallani", "mualoni", "mualnai"])("finds missing, extra, substituted, or swapped characters: %s", (query) => { expect(search(query)).toEqual({ items: [items[0]], fuzzy: true }); }); it("matches aliases and individual words in multiword names", () => { expect(search("surfre")).toEqual({ items: [items[0]], fuzzy: true }); expect(search("codxe")).toEqual({ items: [items[1]], fuzzy: true }); expect(search("obsidain codex")).toEqual({ items: [items[1]], fuzzy: true }); }); it("does not fuzzy match overview prose or internal keys", () => { expect(search("hydor")).toEqual({ items: [], fuzzy: false }); expect(search("interanl")).toEqual({ items: [], fuzzy: false }); }); it("keeps short queries strict and enforces edit limits", () => { expect(names(["Jean"], "jen")).toEqual({ items: [], fuzzy: false }); expect(names(["Mualani"], "muloni").items).toEqual([]); expect(names(["abcdefgh"], "abxdxfgh")).toEqual({ items: ["abcdefgh"], fuzzy: true }); expect(names(["abcdefgh"], "abxdxygh").items).toEqual([]); expect(search("unrelated").items).toEqual([]); }); it("normalizes composed Unicode and handles Thai code points", () => { expect(names(["Café"], "Cafe\u0301")).toEqual({ items: ["Café"], fuzzy: false }); expect(names(["ตัวละคร"], "ตัวลคร")).toEqual({ items: ["ตัวละคร"], fuzzy: true }); expect(names(["😀abcd"], "😀abdc")).toEqual({ items: ["😀abcd"], fuzzy: true }); }); it("ranks by distance with stable ties and retains item identities", () => { expect(names(["abxdxfgh", "abcxefgh", "abcdxfgh"], "abcdefgh").items) .toEqual(["abcxefgh", "abcdxfgh", "abxdxfgh"]); expect(search("mulani").items[0]).toBe(items[0]); }); });