Files
oh-my-pi/packages/tui/test/select-list.test.ts
T
can1357 42b79bc696 feat(coding-agent): added fuzzy token matching in coding-agent components
- Added fuzzy token matching in agent-dashboard, state-manager, and tree-selector, replacing lowercased checks.
- Added search-query state and fuzzy-filter helpers to hook, oauth, and user-message selectors for query filtering.
- Updated filtered selectors to render match results, status lines, no-match text, and move selection within results.
- Added `overflowSearch` and filter state to `SelectList`, switching overflowing list matching to fuzzy checks.
- Configured `SelectList` input flow and fixed cancel so Escape/Ctrl+C closes lists when no matches exist.
- Updated changelogs and added tests for fuzzy-filter behavior in hook, oauth, user-message, and list selectors.
2026-05-31 07:45:22 +02:00

207 lines
5.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { visibleWidth } from "@oh-my-pi/pi-tui/utils";
import { SelectList } from "../src/components/select-list";
import { KeybindingsManager, setKeybindings, TUI_KEYBINDINGS } from "../src/keybindings";
const testTheme = {
selectedPrefix: (text: string) => text,
selectedText: (text: string) => text,
description: (text: string) => text,
scrollInfo: (text: string) => text,
noMatch: (text: string) => text,
symbols: {
cursor: "→",
inputCursor: "|",
hrChar: "─",
quoteBorder: "│",
boxRound: { topLeft: "╭", topRight: "╮", bottomLeft: "╰", bottomRight: "╯", horizontal: "─", vertical: "│" },
boxSharp: {
topLeft: "┌",
topRight: "┐",
bottomLeft: "└",
bottomRight: "┘",
horizontal: "─",
vertical: "│",
teeDown: "┬",
teeUp: "┴",
teeLeft: "┤",
teeRight: "├",
cross: "┼",
},
table: {
topLeft: "┌",
topRight: "┐",
bottomLeft: "└",
bottomRight: "┘",
horizontal: "─",
vertical: "│",
teeDown: "┬",
teeUp: "┴",
teeLeft: "┤",
teeRight: "├",
cross: "┼",
},
spinnerFrames: ["|"],
},
};
const visibleIndexOf = (line: string, text: string): number => {
const index = line.indexOf(text);
expect(index).not.toBe(-1);
return visibleWidth(line.slice(0, index));
};
describe("SelectList", () => {
beforeEach(() => {
setKeybindings(new KeybindingsManager(TUI_KEYBINDINGS));
});
afterEach(() => {
setKeybindings(new KeybindingsManager(TUI_KEYBINDINGS));
});
it("normalizes multiline descriptions to single line", () => {
const items = [
{
value: "test",
label: "test",
description: "Line one\nLine two\nLine three",
},
];
const list = new SelectList(items, 5, testTheme);
const rendered = list.render(80);
expect(rendered.length).toBeGreaterThanOrEqual(1);
expect(rendered[0]).not.toContain("\n");
expect(rendered[0]).toContain("Line one Line two Line three");
});
it("keeps descriptions aligned when the primary text is truncated", () => {
const items = [
{ value: "short", label: "short", description: "short description" },
{
value: "very-long-command-name-that-needs-truncation",
label: "very-long-command-name-that-needs-truncation",
description: "long description",
},
];
const list = new SelectList(items, 5, testTheme);
const rendered = list.render(80);
expect(visibleIndexOf(rendered[0], "short description")).toBe(visibleIndexOf(rendered[1], "long description"));
});
it("uses the configured minimum primary column width", () => {
const items = [
{ value: "a", label: "a", description: "first" },
{ value: "bb", label: "bb", description: "second" },
];
const list = new SelectList(items, 5, testTheme, {
minPrimaryColumnWidth: 12,
maxPrimaryColumnWidth: 20,
});
const rendered = list.render(80);
expect(rendered[0].indexOf("first")).toBe(14);
expect(rendered[1].indexOf("second")).toBe(14);
});
it("uses the configured maximum primary column width", () => {
const items = [
{
value: "very-long-command-name-that-needs-truncation",
label: "very-long-command-name-that-needs-truncation",
description: "first",
},
{ value: "short", label: "short", description: "second" },
];
const list = new SelectList(items, 5, testTheme, {
minPrimaryColumnWidth: 12,
maxPrimaryColumnWidth: 20,
});
const rendered = list.render(80);
expect(visibleIndexOf(rendered[0], "first")).toBe(22);
expect(visibleIndexOf(rendered[1], "second")).toBe(22);
});
it("allows overriding primary truncation while preserving description alignment", () => {
const items = [
{
value: "very-long-command-name-that-needs-truncation",
label: "very-long-command-name-that-needs-truncation",
description: "first",
},
{ value: "short", label: "short", description: "second" },
];
const list = new SelectList(items, 5, testTheme, {
minPrimaryColumnWidth: 12,
maxPrimaryColumnWidth: 12,
truncatePrimary: ({ text, maxWidth }) => {
if (text.length <= maxWidth) {
return text;
}
return `${text.slice(0, Math.max(0, maxWidth - 1))}…`;
},
});
const rendered = list.render(80);
expect(rendered[0]).toContain("…");
expect(visibleIndexOf(rendered[0], "first")).toBe(visibleIndexOf(rendered[1], "second"));
});
it("confirms the selected item when Enter arrives as LF", () => {
const items = [{ value: "run", label: "run" }];
const list = new SelectList(items, 5, testTheme);
let selectedValue: string | undefined;
list.onSelect = item => {
selectedValue = item.value;
};
list.handleInput("\n");
expect(selectedValue).toBe("run");
});
it("fuzzy-filters overflowing lists from typed input", () => {
const items = [
{ value: "ollama", label: "Ollama" },
{ value: "kagi", label: "Kagi" },
{ value: "opencode-go", label: "OpenCode Go" },
{ value: "tavily", label: "Tavily" },
];
const list = new SelectList(items, 3, testTheme);
list.handleInput("o");
list.handleInput("g");
const rendered = list.render(80).join("\n");
expect(rendered).toContain("OpenCode Go");
expect(rendered).not.toContain("Ollama");
expect(rendered).toContain("Search: og");
expect(list.getSelectedItem()?.value).toBe("opencode-go");
});
it("keeps printable keys inert when the list does not overflow", () => {
const items = [
{ value: "alpha", label: "Alpha" },
{ value: "beta", label: "Beta" },
];
const list = new SelectList(items, 2, testTheme);
list.handleInput("b");
const rendered = list.render(80).join("\n");
expect(rendered).toContain("Alpha");
expect(rendered).toContain("Beta");
expect(rendered).not.toContain("Search:");
expect(list.getSelectedItem()?.value).toBe("alpha");
});
});