Files
nanobot/webui/src/tests/thread-composer.test.tsx
T

95 lines
3.3 KiB
TypeScript
Raw Normal View History

2026-05-06 15:54:15 +00:00
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { ThreadComposer } from "@/components/thread/ThreadComposer";
2026-05-06 15:54:15 +00:00
import type { SlashCommand } from "@/lib/types";
const COMMANDS: SlashCommand[] = [
{
command: "/stop",
title: "Stop current task",
description: "Cancel the active agent turn.",
icon: "square",
},
{
command: "/history",
title: "Show conversation history",
description: "Print the last N persisted messages.",
icon: "history",
argHint: "[n]",
},
];
describe("ThreadComposer", () => {
it("renders a readonly hero model composer when provided", () => {
render(
<ThreadComposer
onSend={vi.fn()}
modelLabel="claude-opus-4-5"
2026-05-06 14:15:36 +00:00
placeholder="Ask anything..."
variant="hero"
/>,
);
expect(screen.getByText("claude-opus-4-5")).toBeInTheDocument();
2026-05-06 14:15:36 +00:00
expect(screen.queryByRole("button", { name: "Search" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Reason" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Deep research" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Voice input" })).not.toBeInTheDocument();
const input = screen.getByPlaceholderText("Ask anything...");
expect(input).toBeInTheDocument();
2026-05-06 14:15:36 +00:00
expect(input.className).toContain("min-h-[78px]");
expect(input.parentElement?.className).toContain("max-w-[58rem]");
});
it("keeps the thread composer compact while matching the hero style", () => {
render(
<ThreadComposer
onSend={vi.fn()}
modelLabel="gpt-4o"
placeholder="Type your message..."
/>,
);
expect(screen.getByText("gpt-4o")).toBeInTheDocument();
const input = screen.getByPlaceholderText("Type your message...");
expect(input.className).toContain("min-h-[50px]");
expect(input.parentElement?.className).toContain("max-w-[49.5rem]");
expect(input.parentElement?.className).toContain("rounded-[22px]");
expect(input.parentElement?.className).toContain("shadow-[0_12px_30px_rgba(15,23,42,0.07)]");
expect(screen.getByRole("button", { name: "Attach image" }).className).toContain("bg-card");
expect(screen.getByRole("button", { name: "Send message" }).className).toContain("bg-foreground");
});
2026-05-06 15:54:15 +00:00
it("opens a slash command palette and inserts the selected command", () => {
const onSend = vi.fn();
render(
<ThreadComposer
onSend={onSend}
placeholder="Type your message..."
slashCommands={COMMANDS}
/>,
);
const input = screen.getByLabelText("Message input");
fireEvent.change(input, { target: { value: "/" } });
expect(screen.getByRole("listbox", { name: "Slash commands" })).toBeInTheDocument();
expect(screen.getByRole("option", { name: /\/stop/i })).toHaveAttribute(
"aria-selected",
"true",
);
fireEvent.keyDown(input, { key: "ArrowDown" });
expect(screen.getByRole("option", { name: /\/history/i })).toHaveAttribute(
"aria-selected",
"true",
);
fireEvent.keyDown(input, { key: "Enter" });
expect(input).toHaveValue("/history ");
expect(onSend).not.toHaveBeenCalled();
expect(screen.queryByRole("listbox", { name: "Slash commands" })).not.toBeInTheDocument();
});
});