File size: 6,929 Bytes
fd2c364 | 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 221 222 223 224 225 226 227 228 | import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeAll } from "vitest";
import { renderWithProviders } from "test-utils";
import {
SlashCommandMenu,
getSkillDescription,
stripMarkdown,
} from "#/components/features/chat/components/slash-command-menu";
import { SlashCommandItem } from "#/hooks/chat/use-slash-command";
// jsdom does not implement scrollIntoView
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn();
});
const makeItem = (
name: string,
command: string,
content: string = "",
): SlashCommandItem => ({
skill: {
name,
type: "agentskills" as const,
source: null,
content,
triggers: [command],
},
command,
});
const defaultItems: SlashCommandItem[] = [
makeItem("code-search", "/code-search", "Search code semantically."),
makeItem("random-number", "/random-number", "Generate a random number."),
makeItem(
"init",
"/init",
"---\nname: init\ndescription: Initialize a project\n---\n## Usage\nRun /init to start.",
),
];
describe("SlashCommandMenu", () => {
it("renders nothing when items is empty", () => {
const { container } = renderWithProviders(
<SlashCommandMenu items={[]} selectedIndex={0} onSelect={vi.fn()} />,
);
expect(container.innerHTML).toBe("");
});
it("renders all items with slash commands as primary text", () => {
renderWithProviders(
<SlashCommandMenu
items={defaultItems}
selectedIndex={0}
onSelect={vi.fn()}
/>,
);
expect(screen.getByText("/code-search")).toBeInTheDocument();
expect(screen.getByText("/random-number")).toBeInTheDocument();
expect(screen.getByText("/init")).toBeInTheDocument();
});
it("marks the selected item with aria-selected", () => {
renderWithProviders(
<SlashCommandMenu
items={defaultItems}
selectedIndex={1}
onSelect={vi.fn()}
/>,
);
const options = screen.getAllByRole("option");
expect(options[0]).toHaveAttribute("aria-selected", "false");
expect(options[1]).toHaveAttribute("aria-selected", "true");
expect(options[2]).toHaveAttribute("aria-selected", "false");
});
it("calls onSelect on mouseDown", async () => {
const onSelect = vi.fn();
const user = userEvent.setup();
renderWithProviders(
<SlashCommandMenu
items={defaultItems}
selectedIndex={0}
onSelect={onSelect}
/>,
);
const options = screen.getAllByRole("option");
await user.click(options[1]);
expect(onSelect).toHaveBeenCalledWith(defaultItems[1]);
});
it("displays skill descriptions", () => {
renderWithProviders(
<SlashCommandMenu
items={defaultItems}
selectedIndex={0}
onSelect={vi.fn()}
/>,
);
// First item: first-sentence extraction
expect(screen.getByText("Search code semantically.")).toBeInTheDocument();
// Third item: frontmatter description extraction
expect(screen.getByText("Initialize a project")).toBeInTheDocument();
});
it("has an accessible listbox role and translated aria-label", () => {
renderWithProviders(
<SlashCommandMenu
items={defaultItems}
selectedIndex={0}
onSelect={vi.fn()}
/>,
);
const listbox = screen.getByRole("listbox");
expect(listbox).toBeInTheDocument();
// In test env, translation key is returned as-is
expect(listbox).toHaveAttribute("aria-label", "CHAT_INTERFACE$COMMANDS");
});
});
describe("getSkillDescription", () => {
it("extracts description from YAML frontmatter", () => {
const content =
"---\nname: test\ndescription: A test skill\n---\n## Usage\nDetails here.";
expect(getSkillDescription(content)).toBe("A test skill");
});
it("strips double quotes from frontmatter description", () => {
const content = '---\ndescription: "Quoted description"\n---\nBody.';
expect(getSkillDescription(content)).toBe("Quoted description");
});
it("strips single quotes from frontmatter description", () => {
const content = "---\ndescription: 'Single quoted'\n---\nBody.";
expect(getSkillDescription(content)).toBe("Single quoted");
});
it("falls back to first meaningful line when no frontmatter", () => {
const content = "# Title\n\nThis is a description.";
expect(getSkillDescription(content)).toBe("This is a description.");
});
it("falls back to first sentence from body when frontmatter has no description", () => {
const content =
"---\nname: test\ntriggers: ['/test']\n---\nA helpful tool. It does things.";
expect(getSkillDescription(content)).toBe("A helpful tool.");
});
it("skips headers and empty lines", () => {
const content = "\n\n# Header\n## Subheader\n\nActual content here";
expect(getSkillDescription(content)).toBe("Actual content here");
});
it("returns null for empty content", () => {
expect(getSkillDescription("")).toBeNull();
});
it("returns null for content with only headers", () => {
expect(getSkillDescription("# Title\n## Subtitle")).toBeNull();
});
it("returns the whole line when there is no sentence-ending punctuation", () => {
const content = "A description without punctuation";
expect(getSkillDescription(content)).toBe(
"A description without punctuation",
);
});
it("strips markdown from frontmatter description", () => {
const content =
'---\ndescription: "A **bold** and *italic* description"\n---\nBody.';
expect(getSkillDescription(content)).toBe(
"A bold and italic description",
);
});
it("strips markdown from body fallback", () => {
const content = "# Title\n\nUse `code` and [links](http://example.com).";
expect(getSkillDescription(content)).toBe("Use code and links.");
});
});
describe("stripMarkdown", () => {
it("strips bold syntax", () => {
expect(stripMarkdown("a **bold** word")).toBe("a bold word");
});
it("strips italic syntax", () => {
expect(stripMarkdown("an *italic* word")).toBe("an italic word");
});
it("strips bold-italic syntax", () => {
expect(stripMarkdown("***both***")).toBe("both");
});
it("strips inline code", () => {
expect(stripMarkdown("run `npm test` now")).toBe("run npm test now");
});
it("strips links", () => {
expect(stripMarkdown("see [docs](http://example.com)")).toBe("see docs");
});
it("strips images", () => {
expect(stripMarkdown("")).toBe("alt text");
});
it("strips strikethrough", () => {
expect(stripMarkdown("~~removed~~")).toBe("removed");
});
it("strips underscore emphasis", () => {
expect(stripMarkdown("__bold__ and _italic_")).toBe("bold and italic");
});
it("returns plain text unchanged", () => {
expect(stripMarkdown("plain text")).toBe("plain text");
});
});
|