feat(#101): Add Task Progress widget for orchestrator task monitoring
Create TaskProgressWidget showing live agent task execution progress: - Fetches from orchestrator /agents API with 15s auto-refresh - Shows stats (total/active/done/stopped), sorted task list - Agent type badges (worker/reviewer/tester) - Elapsed time tracking, error display - Dark mode support, PDA-friendly language - Registered in WidgetRegistry for dashboard use Includes 7 unit tests covering all states. Fixes #101 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* TaskProgressWidget Component Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } 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();
|
||||
});
|
||||
|
||||
it("should render loading state initially", (): void => {
|
||||
mockFetch.mockImplementation(
|
||||
() =>
|
||||
new Promise(() => {
|
||||
// Never-resolving promise for loading state
|
||||
})
|
||||
);
|
||||
|
||||
render(<TaskProgressWidget id="task-progress-1" />);
|
||||
|
||||
expect(screen.getByText(/loading task progress/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display tasks after successful fetch", async (): Promise<void> => {
|
||||
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(<TaskProgressWidget id="task-progress-1" />);
|
||||
|
||||
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<void> => {
|
||||
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
render(<TaskProgressWidget id="task-progress-1" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/unable to reach orchestrator/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display empty state when no tasks", async (): Promise<void> => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as unknown as Response);
|
||||
|
||||
render(<TaskProgressWidget id="task-progress-1" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/no agent tasks in progress/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show agent type badges", async (): Promise<void> => {
|
||||
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(<TaskProgressWidget id="task-progress-1" />);
|
||||
|
||||
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<void> => {
|
||||
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(<TaskProgressWidget id="task-progress-1" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Build failed: type errors")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should sort active tasks before completed ones", async (): Promise<void> => {
|
||||
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(<TaskProgressWidget id="task-progress-1" />);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user