198 lines
6.3 KiB
TypeScript
198 lines
6.3 KiB
TypeScript
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { ChatHeader } from "./ChatHeader";
|
|
|
|
interface ChatHeaderProps {
|
|
projectPath: string;
|
|
onCloseProject: () => void;
|
|
contextUsage: { used: number; total: number; percentage: number };
|
|
onClearSession: () => void;
|
|
model: string;
|
|
availableModels: string[];
|
|
claudeModels: string[];
|
|
hasAnthropicKey: boolean;
|
|
onModelChange: (model: string) => void;
|
|
enableTools: boolean;
|
|
onToggleTools: (enabled: boolean) => void;
|
|
}
|
|
|
|
function makeProps(overrides: Partial<ChatHeaderProps> = {}): ChatHeaderProps {
|
|
return {
|
|
projectPath: "/test/project",
|
|
onCloseProject: vi.fn(),
|
|
contextUsage: { used: 1000, total: 10000, percentage: 10 },
|
|
onClearSession: vi.fn(),
|
|
model: "claude-sonnet",
|
|
availableModels: ["llama3"],
|
|
claudeModels: ["claude-sonnet"],
|
|
hasAnthropicKey: true,
|
|
onModelChange: vi.fn(),
|
|
enableTools: true,
|
|
onToggleTools: vi.fn(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("ChatHeader", () => {
|
|
it("renders project path", () => {
|
|
render(<ChatHeader {...makeProps()} />);
|
|
expect(screen.getByText("/test/project")).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls onCloseProject when close button is clicked", () => {
|
|
const onCloseProject = vi.fn();
|
|
render(<ChatHeader {...makeProps({ onCloseProject })} />);
|
|
fireEvent.click(screen.getByText("\u2715"));
|
|
expect(onCloseProject).toHaveBeenCalled();
|
|
});
|
|
|
|
it("displays context percentage with green emoji when low", () => {
|
|
render(
|
|
<ChatHeader
|
|
{...makeProps({
|
|
contextUsage: { used: 1000, total: 10000, percentage: 10 },
|
|
})}
|
|
/>,
|
|
);
|
|
expect(screen.getByText(/10%/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("displays yellow emoji when context is 75-89%", () => {
|
|
render(
|
|
<ChatHeader
|
|
{...makeProps({
|
|
contextUsage: { used: 8000, total: 10000, percentage: 80 },
|
|
})}
|
|
/>,
|
|
);
|
|
expect(screen.getByText(/80%/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("displays red emoji when context is 90%+", () => {
|
|
render(
|
|
<ChatHeader
|
|
{...makeProps({
|
|
contextUsage: { used: 9500, total: 10000, percentage: 95 },
|
|
})}
|
|
/>,
|
|
);
|
|
expect(screen.getByText(/95%/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls onClearSession when New Session button is clicked", () => {
|
|
const onClearSession = vi.fn();
|
|
render(<ChatHeader {...makeProps({ onClearSession })} />);
|
|
fireEvent.click(screen.getByText(/New Session/));
|
|
expect(onClearSession).toHaveBeenCalled();
|
|
});
|
|
|
|
it("renders select dropdown when model options are available", () => {
|
|
render(<ChatHeader {...makeProps()} />);
|
|
const select = screen.getByRole("combobox");
|
|
expect(select).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders text input when no model options are available", () => {
|
|
render(
|
|
<ChatHeader {...makeProps({ availableModels: [], claudeModels: [] })} />,
|
|
);
|
|
expect(screen.getByPlaceholderText("Model")).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls onModelChange when model is selected from dropdown", () => {
|
|
const onModelChange = vi.fn();
|
|
render(<ChatHeader {...makeProps({ onModelChange })} />);
|
|
const select = screen.getByRole("combobox");
|
|
fireEvent.change(select, { target: { value: "llama3" } });
|
|
expect(onModelChange).toHaveBeenCalledWith("llama3");
|
|
});
|
|
|
|
it("calls onModelChange when text is typed in model input", () => {
|
|
const onModelChange = vi.fn();
|
|
render(
|
|
<ChatHeader
|
|
{...makeProps({
|
|
availableModels: [],
|
|
claudeModels: [],
|
|
onModelChange,
|
|
})}
|
|
/>,
|
|
);
|
|
const input = screen.getByPlaceholderText("Model");
|
|
fireEvent.change(input, { target: { value: "custom-model" } });
|
|
expect(onModelChange).toHaveBeenCalledWith("custom-model");
|
|
});
|
|
|
|
it("calls onToggleTools when checkbox is toggled", () => {
|
|
const onToggleTools = vi.fn();
|
|
render(<ChatHeader {...makeProps({ onToggleTools })} />);
|
|
const checkbox = screen.getByRole("checkbox");
|
|
fireEvent.click(checkbox);
|
|
expect(onToggleTools).toHaveBeenCalled();
|
|
});
|
|
|
|
it("displays the build timestamp in human-readable format", () => {
|
|
render(<ChatHeader {...makeProps()} />);
|
|
expect(screen.getByText("Built: 2026-01-01 00:00")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows disabled placeholder when claudeModels is empty and no API key", () => {
|
|
render(
|
|
<ChatHeader
|
|
{...makeProps({
|
|
claudeModels: [],
|
|
hasAnthropicKey: false,
|
|
availableModels: ["llama3"],
|
|
})}
|
|
/>,
|
|
);
|
|
expect(
|
|
screen.getByText("Add Anthropic API key to load models"),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
// ── Close button hover/focus handlers ─────────────────────────────────────
|
|
|
|
it("close button changes background on mouseOver and resets on mouseOut", () => {
|
|
render(<ChatHeader {...makeProps()} />);
|
|
const closeBtn = screen.getByText("\u2715");
|
|
fireEvent.mouseOver(closeBtn);
|
|
expect(closeBtn.style.background).toBe("rgb(51, 51, 51)");
|
|
fireEvent.mouseOut(closeBtn);
|
|
expect(closeBtn.style.background).toBe("transparent");
|
|
});
|
|
|
|
it("close button changes background on focus and resets on blur", () => {
|
|
render(<ChatHeader {...makeProps()} />);
|
|
const closeBtn = screen.getByText("\u2715");
|
|
fireEvent.focus(closeBtn);
|
|
expect(closeBtn.style.background).toBe("rgb(51, 51, 51)");
|
|
fireEvent.blur(closeBtn);
|
|
expect(closeBtn.style.background).toBe("transparent");
|
|
});
|
|
|
|
// ── New Session button hover/focus handlers ───────────────────────────────
|
|
|
|
it("New Session button changes style on mouseOver and resets on mouseOut", () => {
|
|
render(<ChatHeader {...makeProps()} />);
|
|
const sessionBtn = screen.getByText(/New Session/);
|
|
fireEvent.mouseOver(sessionBtn);
|
|
expect(sessionBtn.style.backgroundColor).toBe("rgb(63, 63, 63)");
|
|
expect(sessionBtn.style.color).toBe("rgb(204, 204, 204)");
|
|
fireEvent.mouseOut(sessionBtn);
|
|
expect(sessionBtn.style.backgroundColor).toBe("rgb(47, 47, 47)");
|
|
expect(sessionBtn.style.color).toBe("rgb(136, 136, 136)");
|
|
});
|
|
|
|
it("New Session button changes style on focus and resets on blur", () => {
|
|
render(<ChatHeader {...makeProps()} />);
|
|
const sessionBtn = screen.getByText(/New Session/);
|
|
fireEvent.focus(sessionBtn);
|
|
expect(sessionBtn.style.backgroundColor).toBe("rgb(63, 63, 63)");
|
|
expect(sessionBtn.style.color).toBe("rgb(204, 204, 204)");
|
|
fireEvent.blur(sessionBtn);
|
|
expect(sessionBtn.style.backgroundColor).toBe("rgb(47, 47, 47)");
|
|
expect(sessionBtn.style.color).toBe("rgb(136, 136, 136)");
|
|
});
|
|
});
|