Files
kandev/apps/web/lib/commands/search.test.ts
T
2026-09-17 19:00:34 +01:00

227 lines
7.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { CommandItem } from "./types";
import {
findFirstMatchingCommand,
getCommandSearchTerms,
scoreCommandSearch,
selectCommandSearchResult,
sortCommandsForSearch,
selectContentSearchResult,
} from "./search";
const SHARED_TERM = "Shared term";
function command(
id: string,
label: string,
keywords: string[] = [],
priority?: number,
): CommandItem {
return {
id,
label,
group: "Test",
keywords,
priority,
action: () => undefined,
};
}
function createPRCommand(extraKeywords: string[] = []): CommandItem {
return command("git-create-pr", "Create PR", ["pull request", "pr", ...extraKeywords]);
}
describe("command search", () => {
it("searches labels and aliases without matching internal IDs", () => {
const theme = command("pref-theme", "Switch to Dark Mode", ["theme", "appearance"]);
expect(getCommandSearchTerms(theme)).toEqual(["Switch to Dark Mode", "theme", "appearance"]);
expect(scoreCommandSearch("pr", "pr", [])).toBe(0);
expect(scoreCommandSearch(theme.id, "pr", getCommandSearchTerms(theme))).toBeLessThan(1);
});
it("ranks an exact alias above partial fuzzy matches", () => {
const push = command("git-push", "Push", ["push to remote"]);
const createPR = createPRCommand();
expect(sortCommandsForSearch([push, createPR], "pr")).toEqual([createPR, push]);
expect(findFirstMatchingCommand([push, createPR], "pr")).toBe(createPR);
});
it("ignores surrounding whitespace and casing in aliases", () => {
const createPR = createPRCommand();
expect(findFirstMatchingCommand([createPR], " PULL REQUEST ")).toBe(createPR);
});
it("matches compound queries across separate search terms", () => {
const push = command("git-push", "Push", ["push", "git", "push to remote"]);
const createPR = createPRCommand(["git"]);
const rebase = command("git-rebase", "Rebase", ["rebase", "git", "branch"]);
const allTasks = command("nav-tasks", "Go to All Tasks", ["tasks", "list", "all"]);
const theme = command("pref-theme", "Switch to Dark Mode", ["theme", "appearance", "dark"]);
const commands = [push, createPR, rebase, allTasks, theme];
expect(findFirstMatchingCommand(commands, "git push")).toBe(push);
expect(findFirstMatchingCommand(commands, "git pull request")).toBe(createPR);
expect(findFirstMatchingCommand(commands, "git rebase")).toBe(rebase);
expect(findFirstMatchingCommand(commands, "task list")).toBe(allTasks);
expect(findFirstMatchingCommand(commands, "dark theme")).toBe(theme);
});
it("does not expand short aliases into longer unrelated query tokens", () => {
const createPR = createPRCommand(["git"]);
expect(findFirstMatchingCommand([createPR], "git preview")).toBeUndefined();
});
it("uses command priority to break equal-score ties", () => {
const later = command("later", SHARED_TERM, [], 10);
const earlier = command("earlier", SHARED_TERM, [], 0);
expect(sortCommandsForSearch([later, earlier], "shared")).toEqual([earlier, later]);
});
it("preserves a matching preferred command when registrations update", () => {
const first = command("first", SHARED_TERM, [], 0);
const selected = command("selected", SHARED_TERM, [], 10);
expect(findFirstMatchingCommand([first, selected], "shared", selected.id)).toBe(selected);
});
it("returns no command when nothing matches", () => {
expect(findFirstMatchingCommand([command("home", "Go Home")], "terminal")).toBeUndefined();
});
});
describe("selectCommandSearchResult", () => {
it("preserves a matching command when task results lead the palette", () => {
const first = command("first", SHARED_TERM, [], 0);
const selected = command("selected", SHARED_TERM, [], 10);
const taskValue = "__task:task-1 Shared task";
expect(
selectCommandSearchResult({
commands: [first, selected],
search: "shared",
taskResultValues: [taskValue],
preferredValue: selected.id,
commandsLeadResults: false,
}),
).toBe(selected.id);
});
it("highlights the best command match when a query puts commands first", () => {
const archive = command("task-archive", "Archive task", ["archive"], 0);
const taskValue = "__task:task-1 Close every remaining archive item";
expect(
selectCommandSearchResult({
commands: [archive],
search: "archive",
taskResultValues: [taskValue],
commandsLeadResults: true,
}),
).toBe(archive.id);
});
it("falls back to the first task when a query matches no command", () => {
const taskValue = "__task:task-1 Shared task";
expect(
selectCommandSearchResult({
commands: [command("unrelated", "Go Home")],
search: "shared",
taskResultValues: [taskValue],
commandsLeadResults: true,
}),
).toBe(taskValue);
});
it("leads with the active task list while nothing is typed", () => {
const taskValue = "__task:task-1 Shared task";
expect(
selectCommandSearchResult({
commands: [command("first", SHARED_TERM, [], 0)],
search: "",
taskResultValues: [taskValue],
commandsLeadResults: false,
}),
).toBe(taskValue);
});
it("keeps a task the user highlighted below the command matches", () => {
const archive = command("task-archive", "Archive task", ["archive"], 0);
const taskValue = "__task:task-1 Archive audit";
expect(
selectCommandSearchResult({
commands: [archive],
search: "archive",
taskResultValues: [taskValue],
preferredValue: taskValue,
commandsLeadResults: true,
}),
).toBe(taskValue);
});
});
describe("selectContentSearchResult", () => {
const values = ["repo\u0000a.ts\u00001", "repo\u0000b.ts\u00002"];
it("keeps a selection the user moved when a later batch appends", () => {
// The second batch appends a row; the highlighted one must not jump back.
expect(selectContentSearchResult([...values, "repo\u0000c.ts\u00003"], values[1])).toBe(
values[1],
);
});
it("falls back to the first row when the selection is gone", () => {
expect(selectContentSearchResult(values, "repo\u0000removed.ts\u00009")).toBe(values[0]);
});
it("selects the first row when nothing is selected yet", () => {
expect(selectContentSearchResult(values, "")).toBe(values[0]);
expect(selectContentSearchResult(values, undefined)).toBe(values[0]);
});
it("selects nothing when there are no results", () => {
expect(selectContentSearchResult([], "anything")).toBe("");
});
});
it("waits for idle task results instead of preselecting a task command", () => {
const commands = [command("pin", "Pin task")];
const loadingSelection = selectCommandSearchResult({
commands,
search: "",
taskResultValues: [],
commandsLeadResults: false,
});
expect(loadingSelection).toBe("");
expect(
selectCommandSearchResult({
commands,
search: "",
taskResultValues: ["__task:task-1 Active task"],
preferredValue: loadingSelection,
commandsLeadResults: false,
}),
).toBe("__task:task-1 Active task");
});
it("preselects the first enabled child when a submenu has no query", () => {
expect(
selectCommandSearchResult({
commands: [
{ ...command("disabled", "Disabled"), disabled: true },
command("review", "Review"),
],
search: "",
taskResultValues: [],
commandsLeadResults: true,
}),
).toBe("review");
});