import { describe, expect, it } from "vitest"; import { commandNav, filterCommands, scoreCommand, type CommandItem, type CommandNavKey, type CommandNavState, } from "./command-filter.js"; const ITEMS: CommandItem[] = [ { id: "1", title: "Create page" }, { id: "2", title: "Create database" }, { id: "3", title: "Toggle dark mode" }, { id: "4", title: "Share workspace" }, { id: "5", title: "Export page" }, ]; describe("scoreCommand (fuzzy subsequence)", () => { it("empty query matches everything with score 0", () => { expect(scoreCommand("", "Create page")).toBe(0); expect(scoreCommand(" ", "Create page")).toBe(0); }); it("rejects non-subsequences", () => { expect(scoreCommand("xyz", "Create page")).toBe(-1); expect(scoreCommand("pagee", "Create page")).toBe(-1); expect(scoreCommand("pp", "Create page")).toBe(-1); }); it("case-insensitive subsequence match", () => { expect(scoreCommand("crpage", "Create page")).toBeGreaterThanOrEqual(0); expect(scoreCommand("CRPAGE", "Create page")).toBeGreaterThanOrEqual(0); }); it("contiguous matches outrank scattered ones", () => { const contiguous = scoreCommand("create", "Create page"); const scattered = scoreCommand("crtee", "Create page"); // gaps → penalty expect(scattered).toBeGreaterThanOrEqual(0); expect(contiguous).toBeGreaterThan(scattered); }); it("word-boundary matches get a bonus", () => { const wordStart = scoreCommand("cp", "Create page"); // c at 0, p at word start const nonBoundary = scoreCommand("ct", "Create page"); // c at 0, t mid-word expect(wordStart).toBeGreaterThan(nonBoundary); }); }); describe("filterCommands (deterministic ranking)", () => { it("empty query returns all items in original order", () => { expect(filterCommands(ITEMS, "").map((r) => r.item.id)).toEqual(["1", "2", "3", "4", "5"]); }); it("filters out non-matches; equal scores tiebreak by original index", () => { const out = filterCommands(ITEMS, "page"); // "Create page" and "Export page" both end in the identical "page" word → // identical scores → deterministic tiebreak by original index. expect(out.map((r) => r.item.id)).toEqual(["1", "5"]); // "Create page": the only 'p' is the tail word → tighter match than // "Export page" (its leading 'p' inside "Export" costs gap penalties). expect(out[0]?.score).toBeGreaterThan(out[1]?.score ?? 0); }); it("tiebreaks by ORIGINAL INDEX (deterministic)", () => { const items: CommandItem[] = [ { id: "b", title: "Alpha beta" }, { id: "a", title: "Alpha beta" }, // same score, later index ]; const out = filterCommands(items, "alpha"); expect(out.map((r) => r.item.id)).toEqual(["b", "a"]); }); it("is stable across repeated calls", () => { expect(filterCommands(ITEMS, "a").map((r) => r.item.id)).toEqual( filterCommands(ITEMS, "a").map((r) => r.item.id), ); }); }); describe("commandNav (keyboard reducer)", () => { const open3: CommandNavState = { active: 0, count: 3, open: true }; const key = (k: CommandNavKey) => ({ type: "key" as const, key: k }); it("ArrowDown moves and wraps", () => { expect(commandNav(open3, key("ArrowDown")).active).toBe(1); expect(commandNav({ ...open3, active: 2 }, key("ArrowDown")).active).toBe(0); }); it("ArrowUp moves and wraps", () => { expect(commandNav({ ...open3, active: 0 }, key("ArrowUp")).active).toBe(2); expect(commandNav(open3, key("ArrowUp")).active).toBe(2); // -1 → last }); it("Home/End jump", () => { expect(commandNav({ ...open3, active: 1 }, key("Home")).active).toBe(0); expect(commandNav({ ...open3, active: 1 }, key("End")).active).toBe(2); }); it("Enter is a no-op on state (selection is a side effect)", () => { expect(commandNav(open3, key("Enter"))).toEqual(open3); }); it("Escape closes and resets highlight", () => { expect(commandNav(open3, key("Escape"))).toEqual({ active: -1, count: 3, open: false }); }); it("keys are ignored while closed or empty", () => { const closed = { active: -1, count: 3, open: false }; expect(commandNav(closed, key("ArrowDown"))).toEqual(closed); const empty: CommandNavState = { active: -1, count: 0, open: true }; expect(commandNav(empty, key("ArrowDown"))).toEqual(empty); }); it("open activates index 0; close resets", () => { const closed = { active: -1, count: 3, open: false }; expect(commandNav(closed, { type: "open" })).toEqual({ active: 0, count: 3, open: true }); expect(commandNav(open3, { type: "close" })).toEqual({ active: -1, count: 3, open: false }); }); it("results re-bounds active when the list shrinks", () => { expect(commandNav({ ...open3, active: 2 }, { type: "results", count: 1 })).toEqual({ active: 0, count: 1, open: true, }); expect(commandNav({ ...open3, active: -1 }, { type: "results", count: 0 })).toEqual({ active: -1, count: 0, open: true, }); }); });