Files
storkit/frontend/src/components/ChatHeader.test.tsx

203 lines
6.5 KiB
TypeScript
Raw Normal View History

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("displays StorkIt branding in the header", () => {
render(<ChatHeader {...makeProps()} />);
expect(screen.getByText("StorkIt")).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)");
});
});