300 lines
8.4 KiB
TypeScript
300 lines
8.4 KiB
TypeScript
/**
|
|
* 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(<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 display error for non-ok HTTP responses", async (): Promise<void> => {
|
|
mockFetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
} as unknown as Response);
|
|
|
|
render(<TaskProgressWidget id="task-progress-1" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/unable to reach orchestrator/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("should clear interval on unmount", async (): Promise<void> => {
|
|
const clearIntervalSpy = vi.spyOn(global, "clearInterval");
|
|
|
|
mockFetch.mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve([]),
|
|
} as unknown as Response);
|
|
|
|
const { unmount } = render(<TaskProgressWidget id="task-progress-1" />);
|
|
|
|
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<void> => {
|
|
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(<TaskProgressWidget id="task-progress-1" />);
|
|
|
|
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<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");
|
|
});
|
|
});
|
|
|
|
it("should display latest orchestrator event when available", async (): Promise<void> => {
|
|
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(<TaskProgressWidget id="task-progress-1" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Latest: task.executing · TASK-123/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|