import { describe, it, expect, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { ActivityFeed } from "../ActivityFeed"; import type { ActivityFeedEntry } from "../../api"; // Mock lucide-react icons vi.mock("lucide-react", async () => { const actual = await vi.importActual("lucide-react"); return { ...actual, GitPullRequest: () => PR, GitMerge: () => Merge, CheckCircle: () => , XCircle: () => , Plus: () => +, ArrowRightLeft: () => , Settings: () => , AlertTriangle: () => , Folder: () => 📁, }; }); function makeEntry(overrides: Partial = {}): ActivityFeedEntry { return { id: "entry_001", timestamp: new Date().toISOString(), type: "task:created", projectId: "proj_abc123", projectName: "Test Project", details: "Task created", ...overrides, }; } describe("ActivityFeed", () => { it("renders empty state when no entries", () => { render(); expect(screen.getByText("No recent activity")).toBeDefined(); expect(screen.getByText(/Activity will appear here/)).toBeDefined(); }); it("renders custom empty message", () => { render(); expect(screen.getByText("Custom empty message")).toBeDefined(); }); it("renders loading state", () => { const { container } = render(); expect(container.querySelector(".activity-feed-loading")).toBeDefined(); expect(container.querySelector(".activity-feed-skeleton")).toBeDefined(); }); it("renders error state", () => { render(); expect(screen.getByText("Failed to load activity")).toBeDefined(); }); it("renders activity entries", () => { const entries: ActivityFeedEntry[] = [ makeEntry({ id: "entry_001", type: "task:created", details: "Created FN-001" }), makeEntry({ id: "entry_002", type: "task:moved", details: "Moved FN-002 to in-progress" }), ]; render(); expect(screen.getByText("Created")).toBeDefined(); expect(screen.getByText("Moved")).toBeDefined(); expect(screen.getByText("Created FN-001")).toBeDefined(); expect(screen.getByText("Moved FN-002 to in-progress")).toBeDefined(); }); it("shows project names when provided", () => { const entries: ActivityFeedEntry[] = [ makeEntry({ projectId: "proj_abc123", projectName: "Project Alpha" }), ]; render( ); expect(screen.getByText("Project Alpha")).toBeDefined(); }); it("displays task ID when available", () => { const entries: ActivityFeedEntry[] = [ makeEntry({ taskId: "FN-042", taskTitle: "Fix bug" }), ]; render(); expect(screen.getByText("FN-042")).toBeDefined(); expect(screen.getByText("Fix bug")).toBeDefined(); }); it("groups entries by date", () => { const today = new Date().toISOString(); const yesterday = new Date(Date.now() - 86400000).toISOString(); const entries: ActivityFeedEntry[] = [ makeEntry({ id: "entry_001", timestamp: today }), makeEntry({ id: "entry_002", timestamp: yesterday }), ]; const { container } = render(); const groups = container.querySelectorAll(".activity-feed-group"); expect(groups.length).toBe(2); }); it("shows relative time for recent entries", () => { const fiveMinutesAgo = new Date(Date.now() - 5 * 60000).toISOString(); const entries: ActivityFeedEntry[] = [ makeEntry({ timestamp: fiveMinutesAgo }), ]; render(); expect(screen.getByText("5m ago")).toBeDefined(); }); it("renders different event types with correct labels", () => { const entries: ActivityFeedEntry[] = [ makeEntry({ id: "1", type: "task:created" }), makeEntry({ id: "2", type: "task:moved" }), makeEntry({ id: "3", type: "task:updated" }), makeEntry({ id: "4", type: "task:deleted" }), makeEntry({ id: "5", type: "task:merged" }), makeEntry({ id: "6", type: "task:failed" }), makeEntry({ id: "7", type: "settings:updated" }), ]; render(); expect(screen.getByText("Created")).toBeDefined(); expect(screen.getByText("Moved")).toBeDefined(); expect(screen.getByText("Updated")).toBeDefined(); expect(screen.getByText("Deleted")).toBeDefined(); expect(screen.getByText("Merged")).toBeDefined(); expect(screen.getByText("Failed")).toBeDefined(); expect(screen.getByText("Settings")).toBeDefined(); }); it("renders with data-type attribute for styling", () => { const entries: ActivityFeedEntry[] = [ makeEntry({ type: "task:created" }), ]; const { container } = render(); const item = container.querySelector('[data-type="task:created"]'); expect(item).toBeDefined(); }); it("truncates long task titles", () => { const longTitle = "A".repeat(200); const entries: ActivityFeedEntry[] = [ makeEntry({ taskTitle: longTitle }), ]; const { container } = render(); const titleEl = container.querySelector(".activity-feed-task-title"); expect(titleEl).toBeDefined(); expect(titleEl?.getAttribute("title")).toBe(longTitle); }); it("shows full timestamp on hover via title attribute", () => { // Use a recent timestamp so it shows "ago" format const recentTime = new Date(Date.now() - 5 * 60000).toISOString(); const entries: ActivityFeedEntry[] = [ makeEntry({ timestamp: recentTime }), ]; render(); // Should show relative time like "5m ago" const timeEl = screen.getByText(/ago/); expect(timeEl).toBeDefined(); // Title attribute should have full timestamp expect(timeEl.getAttribute("title")).toContain(":"); }); it("uses theme tokens for event type icon colors", () => { const entries: ActivityFeedEntry[] = [ makeEntry({ id: "1", type: "task:deleted" }), makeEntry({ id: "2", type: "task:failed" }), ]; const { container } = render(); // Verify that deleted and failed events use --color-error (not undefined --error) const icons = container.querySelectorAll(".activity-feed-icon"); expect(icons.length).toBeGreaterThanOrEqual(2); for (const icon of icons) { const style = (icon as HTMLElement).style; const color = style.color || style.getPropertyValue("color"); // Should use var(--color-error), NOT var(--error) which is undefined expect(color).toContain("var(--color-error)"); expect(color).not.toContain("var(--error)"); } }); });