Spaces:
Running
Running
File size: 6,888 Bytes
fb4d8fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | import { describe, expect, it } from "vitest";
import { SearchableSelectList, type SearchableSelectListTheme } from "./searchable-select-list.js";
const mockTheme: SearchableSelectListTheme = {
selectedPrefix: (t) => `[${t}]`,
selectedText: (t) => `**${t}**`,
description: (t) => `(${t})`,
scrollInfo: (t) => `~${t}~`,
noMatch: (t) => `!${t}!`,
searchPrompt: (t) => `>${t}<`,
searchInput: (t) => `|${t}|`,
matchHighlight: (t) => `*${t}*`,
};
const testItems = [
{
value: "anthropic/claude-3-opus",
label: "anthropic/claude-3-opus",
description: "Claude 3 Opus",
},
{
value: "anthropic/claude-3-sonnet",
label: "anthropic/claude-3-sonnet",
description: "Claude 3 Sonnet",
},
{ value: "openai/gpt-4", label: "openai/gpt-4", description: "GPT-4" },
{ value: "openai/gpt-4-turbo", label: "openai/gpt-4-turbo", description: "GPT-4 Turbo" },
{ value: "google/gemini-pro", label: "google/gemini-pro", description: "Gemini Pro" },
];
describe("SearchableSelectList", () => {
it("renders all items when no filter is applied", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme);
const output = list.render(80);
// Should have search prompt line, spacer, and items
expect(output.length).toBeGreaterThanOrEqual(3);
expect(output[0]).toContain("search");
});
it("filters items when typing", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme);
// Simulate typing "gemini" - unique enough to narrow down
list.handleInput("g");
list.handleInput("e");
list.handleInput("m");
list.handleInput("i");
list.handleInput("n");
list.handleInput("i");
const selected = list.getSelectedItem();
expect(selected?.value).toBe("google/gemini-pro");
});
it("prioritizes exact substring matches over fuzzy matches", () => {
// Add items where one has early exact match, others are fuzzy or late matches
const items = [
{ value: "openrouter/auto", label: "openrouter/auto", description: "Routes to best" },
{ value: "opus-direct", label: "opus-direct", description: "Direct opus model" },
{
value: "anthropic/claude-3-opus",
label: "anthropic/claude-3-opus",
description: "Claude 3 Opus",
},
];
const list = new SearchableSelectList(items, 5, mockTheme);
// Type "opus" - should match "opus-direct" first (earliest exact substring)
for (const ch of "opus") {
list.handleInput(ch);
}
// First result should be "opus-direct" where "opus" appears at position 0
const selected = list.getSelectedItem();
expect(selected?.value).toBe("opus-direct");
});
it("keeps exact label matches ahead of description matches", () => {
const longPrefix = "x".repeat(250);
const items = [
{ value: "late-label", label: `${longPrefix}opus`, description: "late exact match" },
{ value: "desc-first", label: "provider/other", description: "opus in description" },
];
const list = new SearchableSelectList(items, 5, mockTheme);
for (const ch of "opus") {
list.handleInput(ch);
}
const selected = list.getSelectedItem();
expect(selected?.value).toBe("late-label");
});
it("exact label match beats description match", () => {
const items = [
{
value: "provider/other",
label: "provider/other",
description: "This mentions opus in description",
},
{ value: "provider/opus-model", label: "provider/opus-model", description: "Something else" },
];
const list = new SearchableSelectList(items, 5, mockTheme);
for (const ch of "opus") {
list.handleInput(ch);
}
// Label match should win over description match
const selected = list.getSelectedItem();
expect(selected?.value).toBe("provider/opus-model");
});
it("orders description matches by earliest index", () => {
const items = [
{ value: "first", label: "first", description: "prefix opus value" },
{ value: "second", label: "second", description: "opus suffix value" },
];
const list = new SearchableSelectList(items, 5, mockTheme);
for (const ch of "opus") {
list.handleInput(ch);
}
const selected = list.getSelectedItem();
expect(selected?.value).toBe("second");
});
it("filters items with fuzzy matching", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme);
// Simulate typing "gpt" which should match openai/gpt-4 models
list.handleInput("g");
list.handleInput("p");
list.handleInput("t");
const selected = list.getSelectedItem();
expect(selected?.value).toContain("gpt");
});
it("preserves fuzzy ranking when only fuzzy matches exist", () => {
const items = [
{ value: "xg---4", label: "xg---4", description: "Worse fuzzy match" },
{ value: "gpt-4", label: "gpt-4", description: "Better fuzzy match" },
];
const list = new SearchableSelectList(items, 5, mockTheme);
for (const ch of "g4") {
list.handleInput(ch);
}
const selected = list.getSelectedItem();
expect(selected?.value).toBe("gpt-4");
});
it("highlights matches in rendered output", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme);
for (const ch of "gpt") {
list.handleInput(ch);
}
const output = list.render(80).join("\n");
expect(output).toContain("*gpt*");
});
it("shows no match message when filter yields no results", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme);
// Type something that won't match
list.handleInput("x");
list.handleInput("y");
list.handleInput("z");
const output = list.render(80);
expect(output.some((line) => line.includes("No matches"))).toBe(true);
});
it("navigates with arrow keys", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme);
// Initially first item is selected
expect(list.getSelectedItem()?.value).toBe("anthropic/claude-3-opus");
// Press down arrow (escape sequence for down arrow)
list.handleInput("\x1b[B");
expect(list.getSelectedItem()?.value).toBe("anthropic/claude-3-sonnet");
});
it("calls onSelect when enter is pressed", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme);
let selectedValue: string | undefined;
list.onSelect = (item) => {
selectedValue = item.value;
};
// Press enter
list.handleInput("\r");
expect(selectedValue).toBe("anthropic/claude-3-opus");
});
it("calls onCancel when escape is pressed", () => {
const list = new SearchableSelectList(testItems, 5, mockTheme);
let cancelled = false;
list.onCancel = () => {
cancelled = true;
};
// Press escape
list.handleInput("\x1b");
expect(cancelled).toBe(true);
});
});
|