/** * TaskProgressWidget Component Tests */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, waitFor, cleanup } from "@testing-library/react"; import { TaskProgressWidget } from "../TaskProgressWidget"; const mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; describe("TaskProgressWidget", (): void => { beforeEach((): void => { vi.clearAllMocks(); }); afterEach((): void => { cleanup(); }); it("should render loading state initially", (): void => { mockFetch.mockImplementation( () => new Promise(() => { // Never-resolving promise for loading state }) ); render(); expect(screen.getByText(/loading task progress/i)).toBeInTheDocument(); }); it("should display tasks after successful fetch", async (): Promise => { const mockTasks = [ { agentId: "agent-1", taskId: "TASK-001", status: "running", agentType: "worker", spawnedAt: new Date().toISOString(), }, { agentId: "agent-2", taskId: "TASK-002", status: "completed", agentType: "reviewer", spawnedAt: new Date(Date.now() - 3600000).toISOString(), completedAt: new Date().toISOString(), }, ]; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockTasks), } as unknown as Response); render(); await waitFor(() => { expect(screen.getByText("TASK-001")).toBeInTheDocument(); expect(screen.getByText("TASK-002")).toBeInTheDocument(); }); // Check stats expect(screen.getByText("2")).toBeInTheDocument(); // Total }); it("should display error state when fetch fails", async (): Promise => { mockFetch.mockRejectedValueOnce(new Error("Network error")); render(); await waitFor(() => { expect(screen.getByText(/unable to reach orchestrator/i)).toBeInTheDocument(); }); }); it("should display empty state when no tasks", async (): Promise => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]), } as unknown as Response); render(); await waitFor(() => { expect(screen.getByText(/no agent tasks in progress/i)).toBeInTheDocument(); }); }); it("should show agent type badges", async (): Promise => { const mockTasks = [ { agentId: "agent-1", taskId: "TASK-001", status: "running", agentType: "worker", spawnedAt: new Date().toISOString(), }, { agentId: "agent-2", taskId: "TASK-002", status: "running", agentType: "reviewer", spawnedAt: new Date().toISOString(), }, { agentId: "agent-3", taskId: "TASK-003", status: "running", agentType: "tester", spawnedAt: new Date().toISOString(), }, ]; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockTasks), } as unknown as Response); render(); await waitFor(() => { expect(screen.getByText("Worker")).toBeInTheDocument(); expect(screen.getByText("Reviewer")).toBeInTheDocument(); expect(screen.getByText("Tester")).toBeInTheDocument(); }); }); it("should display error message for failed tasks", async (): Promise => { const mockTasks = [ { agentId: "agent-1", taskId: "TASK-FAIL", status: "failed", agentType: "worker", spawnedAt: new Date().toISOString(), error: "Build failed: type errors", }, ]; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockTasks), } as unknown as Response); render(); await waitFor(() => { expect(screen.getByText("Build failed: type errors")).toBeInTheDocument(); }); }); it("should display error for non-ok HTTP responses", async (): Promise => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, } as unknown as Response); render(); await waitFor(() => { expect(screen.getByText(/unable to reach orchestrator/i)).toBeInTheDocument(); }); }); it("should clear interval on unmount", async (): Promise => { const clearIntervalSpy = vi.spyOn(global, "clearInterval"); mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve([]), } as unknown as Response); const { unmount } = render(); await waitFor(() => { expect(screen.getByText(/no agent tasks in progress/i)).toBeInTheDocument(); }); unmount(); expect(clearIntervalSpy).toHaveBeenCalled(); clearIntervalSpy.mockRestore(); }); it("should limit displayed tasks to 10", async (): Promise => { const mockTasks = Array.from({ length: 15 }, (_, i) => ({ agentId: `agent-${String(i)}`, taskId: `SLICE-${String(i).padStart(3, "0")}`, status: "running", agentType: "worker", spawnedAt: new Date().toISOString(), })); mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockTasks), } as unknown as Response); render(); await waitFor(() => { // Only 10 task cards should be rendered despite 15 tasks const workerBadges = screen.getAllByText("Worker"); expect(workerBadges).toHaveLength(10); }); }); it("should sort active tasks before completed ones", async (): Promise => { const mockTasks = [ { agentId: "agent-completed", taskId: "COMPLETED-TASK", status: "completed", agentType: "worker", spawnedAt: new Date(Date.now() - 7200000).toISOString(), completedAt: new Date().toISOString(), }, { agentId: "agent-running", taskId: "RUNNING-TASK", status: "running", agentType: "worker", spawnedAt: new Date().toISOString(), }, ]; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockTasks), } as unknown as Response); render(); await waitFor(() => { const taskElements = screen.getAllByText(/TASK/); expect(taskElements).toHaveLength(2); // Running task should appear before completed expect(taskElements[0]?.textContent).toBe("RUNNING-TASK"); expect(taskElements[1]?.textContent).toBe("COMPLETED-TASK"); }); }); it("should display latest orchestrator event when available", async (): Promise => { mockFetch.mockImplementation((input: RequestInfo | URL) => { let url = ""; if (typeof input === "string") { url = input; } else if (input instanceof URL) { url = input.toString(); } else { url = input.url; } if (url.includes("/api/orchestrator/agents")) { return Promise.resolve({ ok: true, json: () => Promise.resolve([]), } as unknown as Response); } if (url.includes("/api/orchestrator/queue/stats")) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ pending: 0, active: 0, completed: 0, failed: 0, delayed: 0, }), } as unknown as Response); } if (url.includes("/api/orchestrator/events/recent")) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ events: [ { type: "task.executing", timestamp: new Date().toISOString(), taskId: "TASK-123", }, ], }), } as unknown as Response); } return Promise.reject(new Error("Unknown endpoint")); }); render(); await waitFor(() => { expect(screen.getByText(/Latest: task.executing ยท TASK-123/i)).toBeInTheDocument(); }); }); });