Render registered project cards immediately while optional health telemetry hydrates in the background. - Publish completed health batches progressively and prevent stale responses from overwriting current state. - Keep project controls usable while health metrics load and indicate per-card pending telemetry. - Add coverage and operator documentation for progressive overview hydration. Files changed: .changeset/fn-8703-project-overview-load.md | 7 ++ docs/dashboard-guide.md | 3 +- packages/dashboard/app/components/ProjectCard.tsx | 12 ++- .../dashboard/app/components/ProjectOverview.tsx | 15 +-- .../components/__tests__/ProjectOverview.test.tsx | 101 ++++++++++++++++++++ .../app/hooks/__tests__/useProjectHealth.test.ts | 98 ++++++++++++++++++++ packages/dashboard/app/hooks/useProjectHealth.ts | 102 ++++++++++----------- 7 files changed, 277 insertions(+), 61 deletions(-) Fusion-Task-Id: FN-8703 Fusion-Task-Lineage: eb816c9d-c808-4d52-a3f0-28bb9de5f149 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
102 lines
3.1 KiB
TypeScript
102 lines
3.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { ProjectOverview } from "../ProjectOverview";
|
|
import * as api from "../../api";
|
|
import type { ProjectHealth, ProjectInfoWithSource } from "../../api";
|
|
|
|
vi.mock("../../api", () => ({
|
|
fetchProjectHealth: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("react-i18next", () => ({
|
|
useTranslation: () => ({ t: (_key: string, fallback?: string) => fallback ?? _key }),
|
|
}));
|
|
|
|
const mockFetchProjectHealth = vi.mocked(api.fetchProjectHealth);
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
const promise = new Promise<T>((res) => {
|
|
resolve = res;
|
|
});
|
|
return { promise, resolve };
|
|
}
|
|
|
|
function createProject(id: string): ProjectInfoWithSource {
|
|
return {
|
|
id,
|
|
name: `Project ${id}`,
|
|
path: `/workspace/${id}`,
|
|
status: "active",
|
|
isolationMode: "in-process",
|
|
createdAt: "2026-01-01T00:00:00.000Z",
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
function createHealth(projectId: string): ProjectHealth {
|
|
return {
|
|
projectId,
|
|
status: "active",
|
|
activeTaskCount: 3,
|
|
inFlightAgentCount: 1,
|
|
totalTasksCompleted: 12,
|
|
totalTasksFailed: 0,
|
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("ProjectOverview health hydration", () => {
|
|
it("keeps registered project cards and controls usable while batched health resolves progressively", async () => {
|
|
const pendingHealth = new Map(
|
|
Array.from({ length: 6 }, (_, index) => {
|
|
const id = `p${index + 1}`;
|
|
return [id, deferred<ProjectHealth>()] as const;
|
|
}),
|
|
);
|
|
const onSelectProject = vi.fn();
|
|
mockFetchProjectHealth.mockImplementation((projectId: string) => pendingHealth.get(projectId)!.promise);
|
|
|
|
render(
|
|
<ProjectOverview
|
|
projects={Array.from({ length: 6 }, (_, index) => createProject(`p${index + 1}`))}
|
|
onSelectProject={onSelectProject}
|
|
onAddProject={vi.fn()}
|
|
onPauseProject={vi.fn()}
|
|
onResumeProject={vi.fn()}
|
|
onRemoveProject={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(mockFetchProjectHealth).toHaveBeenCalledTimes(5);
|
|
});
|
|
|
|
expect(screen.getByText("Project p1")).toBeTruthy();
|
|
expect(screen.getByText("Project p6")).toBeTruthy();
|
|
expect(screen.getByRole("button", { name: "Add Project" })).toBeTruthy();
|
|
expect(screen.getByRole("combobox", { name: "Sort projects" })).toBeTruthy();
|
|
expect(screen.getAllByRole("button", { name: "Open project" })).toHaveLength(6);
|
|
|
|
fireEvent.click(screen.getByText("Project p1"));
|
|
expect(onSelectProject).toHaveBeenCalledWith(expect.objectContaining({ id: "p1" }));
|
|
|
|
await act(async () => {
|
|
for (let index = 1; index <= 5; index += 1) {
|
|
pendingHealth.get(`p${index}`)!.resolve(createHealth(`p${index}`));
|
|
}
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getAllByText("12")).toHaveLength(5);
|
|
expect(mockFetchProjectHealth).toHaveBeenCalledTimes(6);
|
|
});
|
|
|
|
expect(screen.getByText("Project p6")).toBeTruthy();
|
|
});
|
|
});
|