Restore codebase deleted by bad auto-commit e4227cf
Commit e4227cf (a story creation auto-commit) erroneously deleted 175
files from master's tree, likely due to a race condition between
concurrent git operations. This commit re-adds all files from the
working directory.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ChatHeader } from "./ChatHeader";
|
||||
|
||||
vi.mock("../api/client", () => ({
|
||||
api: {
|
||||
rebuildAndRestart: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
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;
|
||||
wsConnected: boolean;
|
||||
}
|
||||
|
||||
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(),
|
||||
wsConnected: false,
|
||||
...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("labels the claude-pty optgroup as 'Claude Code'", () => {
|
||||
render(<ChatHeader {...makeProps()} />);
|
||||
const optgroup = document.querySelector('optgroup[label="Claude Code"]');
|
||||
expect(optgroup).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("labels the Anthropic API optgroup as 'Anthropic API'", () => {
|
||||
render(<ChatHeader {...makeProps()} />);
|
||||
const optgroup = document.querySelector('optgroup[label="Anthropic API"]');
|
||||
expect(optgroup).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)");
|
||||
});
|
||||
|
||||
// ── Rebuild button ────────────────────────────────────────────────────────
|
||||
|
||||
it("renders rebuild button", () => {
|
||||
render(<ChatHeader {...makeProps()} />);
|
||||
expect(
|
||||
screen.getByTitle("Rebuild and restart the server"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows confirmation dialog when rebuild button is clicked", () => {
|
||||
render(<ChatHeader {...makeProps()} />);
|
||||
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||
expect(screen.getByText("Rebuild and restart?")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides confirmation dialog when cancel is clicked", () => {
|
||||
render(<ChatHeader {...makeProps()} />);
|
||||
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||
fireEvent.click(screen.getByText("Cancel"));
|
||||
expect(screen.queryByText("Rebuild and restart?")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls api.rebuildAndRestart and shows Building... when confirmed", async () => {
|
||||
const { api } = await import("../api/client");
|
||||
vi.mocked(api.rebuildAndRestart).mockReturnValue(new Promise(() => {}));
|
||||
|
||||
render(<ChatHeader {...makeProps()} />);
|
||||
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||
fireEvent.click(screen.getByText("Rebuild"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Building...")).toBeInTheDocument();
|
||||
});
|
||||
expect(api.rebuildAndRestart).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows Reconnecting... when rebuild triggers a network error", async () => {
|
||||
const { api } = await import("../api/client");
|
||||
vi.mocked(api.rebuildAndRestart).mockRejectedValue(
|
||||
new TypeError("Failed to fetch"),
|
||||
);
|
||||
|
||||
render(<ChatHeader {...makeProps()} />);
|
||||
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||
fireEvent.click(screen.getByText("Rebuild"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Reconnecting...")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error when rebuild returns a failure message", async () => {
|
||||
const { api } = await import("../api/client");
|
||||
vi.mocked(api.rebuildAndRestart).mockResolvedValue(
|
||||
"error[E0308]: mismatched types",
|
||||
);
|
||||
|
||||
render(<ChatHeader {...makeProps()} />);
|
||||
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||
fireEvent.click(screen.getByText("Rebuild"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("⚠ Rebuild failed")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("error[E0308]: mismatched types"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("clears reconnecting state when wsConnected transitions to true", async () => {
|
||||
const { api } = await import("../api/client");
|
||||
vi.mocked(api.rebuildAndRestart).mockRejectedValue(
|
||||
new TypeError("Failed to fetch"),
|
||||
);
|
||||
|
||||
const { rerender } = render(
|
||||
<ChatHeader {...makeProps({ wsConnected: false })} />,
|
||||
);
|
||||
fireEvent.click(screen.getByTitle("Rebuild and restart the server"));
|
||||
fireEvent.click(screen.getByText("Rebuild"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Reconnecting...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
rerender(<ChatHeader {...makeProps({ wsConnected: true })} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("↺ Rebuild")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user