Skip to content

Commit f9d1e5d

Browse files
haasonsaasclaude
andcommitted
test: add comprehensive tests for components, hooks, and store
- Add tests for hooks (useKeyboard, useExpandable) - Add tests for task components (TaskProgress, TaskItem) - Add tests for activity components (ActivityItem, StatusText, ActivityIcons) - Add tests for ArtifactCard component - Add comprehensive Zustand store tests Coverage improved from ~25% to 38% 495 tests now passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 424c818 commit f9d1e5d

File tree

9 files changed

+2743
-0
lines changed

9 files changed

+2743
-0
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
/**
2+
* Tests for ActivityItem component
3+
*/
4+
import { describe, it, expect, vi, beforeEach } from "vitest";
5+
import { render, screen, fireEvent } from "@testing-library/react";
6+
import { ActivityItem } from "./ActivityItem";
7+
import type { ActivityItem as ActivityItemType } from "@/types";
8+
9+
// Mock framer-motion to avoid animation issues in tests
10+
vi.mock("framer-motion", () => ({
11+
motion: {
12+
div: ({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
13+
<div className={className} {...props}>
14+
{children}
15+
</div>
16+
),
17+
},
18+
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
19+
}));
20+
21+
// Mock clipboard API
22+
const mockClipboard = {
23+
writeText: vi.fn(),
24+
};
25+
Object.assign(navigator, { clipboard: mockClipboard });
26+
27+
describe("ActivityItem", () => {
28+
const createActivity = (overrides: Partial<ActivityItemType> = {}): ActivityItemType => ({
29+
id: "activity-1",
30+
type: "command",
31+
title: "Test Activity",
32+
timestamp: new Date(),
33+
status: "completed",
34+
...overrides,
35+
});
36+
37+
const defaultProps = {
38+
index: 0,
39+
isExpanded: false,
40+
onToggle: vi.fn(),
41+
};
42+
43+
beforeEach(() => {
44+
vi.clearAllMocks();
45+
});
46+
47+
describe("rendering", () => {
48+
it("should render activity title", () => {
49+
const activity = createActivity({ title: "Running npm install" });
50+
51+
render(<ActivityItem {...defaultProps} activity={activity} />);
52+
53+
expect(screen.getByText("Running npm install")).toBeInTheDocument();
54+
});
55+
56+
it("should render activity icon", () => {
57+
const activity = createActivity({ type: "command" });
58+
59+
const { container } = render(<ActivityItem {...defaultProps} activity={activity} />);
60+
61+
// Should have the icon container
62+
const iconContainer = container.querySelector(".size-7");
63+
expect(iconContainer).toBeInTheDocument();
64+
});
65+
66+
it("should render timestamp when showTimestamp is true", () => {
67+
const activity = createActivity({ duration: "1m 30s" });
68+
69+
render(<ActivityItem {...defaultProps} activity={activity} showTimestamp />);
70+
71+
expect(screen.getByText("1m 30s")).toBeInTheDocument();
72+
});
73+
74+
it("should not render timestamp when showTimestamp is false", () => {
75+
const activity = createActivity({ duration: "1m 30s" });
76+
77+
render(<ActivityItem {...defaultProps} activity={activity} showTimestamp={false} />);
78+
79+
expect(screen.queryByText("1m 30s")).not.toBeInTheDocument();
80+
});
81+
});
82+
83+
describe("expandable behavior", () => {
84+
it("should call onToggle when clicked", () => {
85+
const onToggle = vi.fn();
86+
const activity = createActivity({ description: "Some description" });
87+
88+
render(<ActivityItem {...defaultProps} activity={activity} onToggle={onToggle} />);
89+
90+
const button = screen.getByRole("button", { name: /test activity/i });
91+
fireEvent.click(button);
92+
93+
expect(onToggle).toHaveBeenCalledTimes(1);
94+
});
95+
96+
it("should show description preview when collapsed", () => {
97+
const activity = createActivity({ description: "This is a description" });
98+
99+
render(<ActivityItem {...defaultProps} activity={activity} isExpanded={false} />);
100+
101+
expect(screen.getByText("This is a description")).toBeInTheDocument();
102+
});
103+
104+
it("should show full description when expanded", () => {
105+
const activity = createActivity({
106+
description: "This is a full description with more details",
107+
});
108+
109+
render(<ActivityItem {...defaultProps} activity={activity} isExpanded />);
110+
111+
expect(screen.getByText("This is a full description with more details")).toBeInTheDocument();
112+
});
113+
114+
it("should render chevron icon for expandable items", () => {
115+
const activity = createActivity({ description: "Some description" });
116+
117+
const { container } = render(<ActivityItem {...defaultProps} activity={activity} />);
118+
119+
// ChevronDown is an SVG inside the button
120+
const chevrons = container.querySelectorAll("svg");
121+
expect(chevrons.length).toBeGreaterThan(0);
122+
});
123+
124+
it("should not be expandable without description or children", () => {
125+
const activity = createActivity({
126+
description: undefined,
127+
children: undefined,
128+
});
129+
130+
render(<ActivityItem {...defaultProps} activity={activity} />);
131+
132+
// Get the toggle button (first button, not the copy button)
133+
const buttons = screen.getAllByRole("button");
134+
const toggleButton = buttons[0];
135+
expect(toggleButton).toBeDisabled();
136+
});
137+
});
138+
139+
describe("children rendering", () => {
140+
it("should render knowledge type children", () => {
141+
const activity = createActivity({
142+
children: [{ id: "child-1", type: "knowledge", title: "Important knowledge" }],
143+
});
144+
145+
render(<ActivityItem {...defaultProps} activity={activity} isExpanded />);
146+
147+
expect(screen.getByText("Important knowledge")).toBeInTheDocument();
148+
});
149+
150+
it("should render file type children", () => {
151+
const activity = createActivity({
152+
children: [{ id: "child-1", type: "file", title: "src/components/App.tsx" }],
153+
});
154+
155+
render(<ActivityItem {...defaultProps} activity={activity} isExpanded />);
156+
157+
expect(screen.getByText("src/components/App.tsx")).toBeInTheDocument();
158+
});
159+
160+
it("should render info type children", () => {
161+
const activity = createActivity({
162+
children: [{ id: "child-1", type: "info", title: "Additional information" }],
163+
});
164+
165+
render(<ActivityItem {...defaultProps} activity={activity} isExpanded />);
166+
167+
expect(screen.getByText("Additional information")).toBeInTheDocument();
168+
});
169+
170+
it("should render external link for children with links", () => {
171+
const activity = createActivity({
172+
children: [
173+
{
174+
id: "child-1",
175+
type: "file",
176+
title: "GitHub Issue",
177+
link: "https://github.com/test/repo/issues/1",
178+
},
179+
],
180+
});
181+
182+
render(<ActivityItem {...defaultProps} activity={activity} isExpanded />);
183+
184+
const link = screen.getByRole("link");
185+
expect(link).toHaveAttribute("href", "https://github.com/test/repo/issues/1");
186+
expect(link).toHaveAttribute("target", "_blank");
187+
expect(link).toHaveAttribute("rel", "noopener noreferrer");
188+
});
189+
190+
it("should not render children when collapsed", () => {
191+
const activity = createActivity({
192+
children: [{ id: "child-1", type: "knowledge", title: "Hidden knowledge" }],
193+
});
194+
195+
render(<ActivityItem {...defaultProps} activity={activity} isExpanded={false} />);
196+
197+
expect(screen.queryByText("Hidden knowledge")).not.toBeInTheDocument();
198+
});
199+
});
200+
201+
describe("status indicator", () => {
202+
it("should show status dot for running activities", () => {
203+
const activity = createActivity({ status: "running" });
204+
205+
const { container } = render(<ActivityItem {...defaultProps} activity={activity} />);
206+
207+
const statusDot = container.querySelector(".animate-pulse-subtle");
208+
expect(statusDot).toBeInTheDocument();
209+
});
210+
211+
it("should apply font-medium class for running activities", () => {
212+
const activity = createActivity({ status: "running", title: "Running" });
213+
214+
render(<ActivityItem {...defaultProps} activity={activity} />);
215+
216+
const title = screen.getByText("Running");
217+
expect(title).toHaveClass("font-medium");
218+
});
219+
220+
it("should not show status dot for completed activities", () => {
221+
const activity = createActivity({ status: "completed" });
222+
223+
const { container } = render(<ActivityItem {...defaultProps} activity={activity} />);
224+
225+
const statusDot = container.querySelector(".animate-pulse-subtle");
226+
expect(statusDot).not.toBeInTheDocument();
227+
});
228+
});
229+
230+
describe("copy functionality", () => {
231+
it("should copy activity title to clipboard", async () => {
232+
const activity = createActivity({ title: "Copy this" });
233+
mockClipboard.writeText.mockResolvedValueOnce(undefined);
234+
235+
render(<ActivityItem {...defaultProps} activity={activity} />);
236+
237+
// Find copy button by its title
238+
const copyButton = screen.getByTitle("Copy");
239+
fireEvent.click(copyButton);
240+
241+
expect(mockClipboard.writeText).toHaveBeenCalledWith("Copy this");
242+
});
243+
244+
it("should copy title and description to clipboard", async () => {
245+
const activity = createActivity({
246+
title: "Activity",
247+
description: "Description",
248+
});
249+
mockClipboard.writeText.mockResolvedValueOnce(undefined);
250+
251+
render(<ActivityItem {...defaultProps} activity={activity} />);
252+
253+
const copyButton = screen.getByTitle("Copy");
254+
fireEvent.click(copyButton);
255+
256+
expect(mockClipboard.writeText).toHaveBeenCalledWith("Activity: Description");
257+
});
258+
259+
it("should handle clipboard errors gracefully", async () => {
260+
const activity = createActivity({ title: "Test" });
261+
mockClipboard.writeText.mockRejectedValueOnce(new Error("Access denied"));
262+
263+
render(<ActivityItem {...defaultProps} activity={activity} />);
264+
265+
const copyButton = screen.getByTitle("Copy");
266+
267+
// Should not throw
268+
expect(() => fireEvent.click(copyButton)).not.toThrow();
269+
});
270+
271+
it("should stop event propagation on copy button click", async () => {
272+
const onToggle = vi.fn();
273+
const activity = createActivity({ description: "Test" });
274+
mockClipboard.writeText.mockResolvedValueOnce(undefined);
275+
276+
render(<ActivityItem {...defaultProps} activity={activity} onToggle={onToggle} />);
277+
278+
const copyButton = screen.getByTitle("Copy");
279+
fireEvent.click(copyButton);
280+
281+
// onToggle should not be called because stopPropagation was called
282+
expect(onToggle).not.toHaveBeenCalled();
283+
});
284+
});
285+
286+
describe("compact mode", () => {
287+
it("should apply compact padding when compact is true", () => {
288+
const activity = createActivity();
289+
290+
const { container } = render(<ActivityItem {...defaultProps} activity={activity} compact />);
291+
292+
const wrapper = container.firstChild;
293+
expect(wrapper).toHaveClass("pb-2");
294+
});
295+
296+
it("should apply normal padding when compact is false", () => {
297+
const activity = createActivity();
298+
299+
const { container } = render(
300+
<ActivityItem {...defaultProps} activity={activity} compact={false} />
301+
);
302+
303+
const wrapper = container.firstChild;
304+
expect(wrapper).toHaveClass("pb-4");
305+
});
306+
});
307+
308+
describe("timeline connector", () => {
309+
it("should render timeline connector line", () => {
310+
const activity = createActivity();
311+
312+
const { container } = render(<ActivityItem {...defaultProps} activity={activity} />);
313+
314+
const connector = container.querySelector(".bg-gradient-to-b");
315+
expect(connector).toBeInTheDocument();
316+
});
317+
});
318+
319+
describe("edge cases", () => {
320+
it("should handle empty title", () => {
321+
const activity = createActivity({ title: "" });
322+
323+
const { container } = render(<ActivityItem {...defaultProps} activity={activity} />);
324+
325+
expect(container.firstChild).toBeInTheDocument();
326+
});
327+
328+
it("should handle very long title", () => {
329+
const longTitle = "A".repeat(200);
330+
const activity = createActivity({ title: longTitle });
331+
332+
render(<ActivityItem {...defaultProps} activity={activity} />);
333+
334+
const title = screen.getByText(longTitle);
335+
expect(title).toHaveClass("truncate");
336+
});
337+
338+
it("should handle multiple children", () => {
339+
const activity = createActivity({
340+
children: [
341+
{ id: "1", type: "knowledge", title: "Knowledge 1" },
342+
{ id: "2", type: "file", title: "file.ts" },
343+
{ id: "3", type: "info", title: "Info item" },
344+
],
345+
});
346+
347+
render(<ActivityItem {...defaultProps} activity={activity} isExpanded />);
348+
349+
expect(screen.getByText("Knowledge 1")).toBeInTheDocument();
350+
expect(screen.getByText("file.ts")).toBeInTheDocument();
351+
expect(screen.getByText("Info item")).toBeInTheDocument();
352+
});
353+
});
354+
});

0 commit comments

Comments
 (0)