feat(FN-1734): merge fusion/fn-1734
This commit is contained in:
@@ -183,8 +183,15 @@ export function ProjectOverview({
|
||||
onSelectProject(project);
|
||||
}, [onSelectProject, recentProjectIds]);
|
||||
|
||||
// Determine if we need to show skeleton
|
||||
// Show skeleton for initial load if:
|
||||
// 1. Projects list is still loading, OR
|
||||
// 2. Projects exist but we haven't fetched health data yet (healthLoading with no data)
|
||||
// Don't show skeleton during background health polling when health data already exists
|
||||
const needsInitialSkeleton = loading || (healthLoading && projects.length > 0 && Object.keys(healthMap).length === 0);
|
||||
|
||||
// Show skeleton while loading
|
||||
if (loading || healthLoading) {
|
||||
if (needsInitialSkeleton) {
|
||||
return <ProjectGridSkeleton />;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,28 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { ProjectOverview } from "../ProjectOverview";
|
||||
import type { ProjectInfo, ProjectHealth } from "@fusion/core";
|
||||
import { useProjectHealth } from "../../hooks/useProjectHealth";
|
||||
|
||||
// Default mock implementation
|
||||
function createDefaultHealthMap(projectIds: string[]): Record<string, ProjectHealth> {
|
||||
return projectIds.reduce((acc, id) => {
|
||||
acc[id] = {
|
||||
projectId: id,
|
||||
status: "active" as const,
|
||||
activeTaskCount: 5,
|
||||
inFlightAgentCount: 2,
|
||||
totalTasksCompleted: 100,
|
||||
totalTasksFailed: 3,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<string, ProjectHealth>);
|
||||
}
|
||||
|
||||
// Mock the hooks
|
||||
vi.mock("../../hooks/useProjectHealth", () => ({
|
||||
useProjectHealth: vi.fn((projectIds: string[]) => ({
|
||||
healthMap: projectIds.reduce((acc, id) => {
|
||||
acc[id] = {
|
||||
projectId: id,
|
||||
status: "active",
|
||||
activeTaskCount: 5,
|
||||
inFlightAgentCount: 2,
|
||||
totalTasksCompleted: 100,
|
||||
totalTasksFailed: 3,
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as ProjectHealth;
|
||||
return acc;
|
||||
}, {} as Record<string, ProjectHealth>),
|
||||
healthMap: createDefaultHealthMap(projectIds),
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
@@ -75,6 +81,14 @@ const noop = () => {};
|
||||
describe("ProjectOverview", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset mock to default state
|
||||
vi.mocked(useProjectHealth).mockImplementation((projectIds: string[]) => ({
|
||||
healthMap: createDefaultHealthMap(projectIds),
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
refreshProject: vi.fn(),
|
||||
}));
|
||||
});
|
||||
|
||||
it("renders without crashing with projects", () => {
|
||||
@@ -423,4 +437,93 @@ describe("ProjectOverview", () => {
|
||||
expect(screen.getByLabelText("Sort projects")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FN-1734: Health polling scroll position regression", () => {
|
||||
it("shows project cards when health hook is in loading state but healthMap has existing data", () => {
|
||||
// This is the key regression test for FN-1734:
|
||||
// When background polling refreshes health, loading becomes true but we
|
||||
// should NOT show skeleton if we already have health data
|
||||
vi.mocked(useProjectHealth).mockReturnValue({
|
||||
healthMap: {
|
||||
proj_1: {
|
||||
projectId: "proj_1",
|
||||
status: "active",
|
||||
activeTaskCount: 5,
|
||||
inFlightAgentCount: 2,
|
||||
totalTasksCompleted: 100,
|
||||
totalTasksFailed: 3,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
refreshProject: vi.fn(),
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<ProjectOverview
|
||||
projects={[makeProject({ id: "proj_1", name: "Test Project" })]}
|
||||
onSelectProject={noop}
|
||||
onAddProject={noop}
|
||||
onPauseProject={noop}
|
||||
onResumeProject={noop}
|
||||
onRemoveProject={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
// Project card should be visible
|
||||
expect(screen.getByTestId("project-card-proj_1")).toBeDefined();
|
||||
// Skeleton should NOT be shown (we have existing health data)
|
||||
expect(screen.queryByTestId("project-grid-skeleton")).toBeNull();
|
||||
// Grid should be rendered
|
||||
expect(container.querySelector(".project-grid")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows skeleton only when projects exist but no health data has been fetched", () => {
|
||||
// When loading prop is true AND we have no health data yet,
|
||||
// skeleton should be shown
|
||||
render(
|
||||
<ProjectOverview
|
||||
projects={[makeProject({ id: "proj_1", name: "Test Project" })]}
|
||||
loading={true} // Projects are loading
|
||||
onSelectProject={noop}
|
||||
onAddProject={noop}
|
||||
onPauseProject={noop}
|
||||
onResumeProject={noop}
|
||||
onRemoveProject={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
// Skeleton should be shown (projects loading)
|
||||
expect(screen.getByTestId("project-grid-skeleton")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows skeleton when health hook is loading with no existing health data", () => {
|
||||
// When health hook returns loading=true AND we have no health data yet,
|
||||
// skeleton should be shown even if loading prop is false
|
||||
vi.mocked(useProjectHealth).mockReturnValue({
|
||||
healthMap: {}, // Empty - no health data yet
|
||||
loading: true, // Health is still loading
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
refreshProject: vi.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
<ProjectOverview
|
||||
projects={[makeProject({ id: "proj_1", name: "Test Project" })]}
|
||||
loading={false}
|
||||
onSelectProject={noop}
|
||||
onAddProject={noop}
|
||||
onPauseProject={noop}
|
||||
onResumeProject={noop}
|
||||
onRemoveProject={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
// Skeleton should be shown (health loading with no data)
|
||||
expect(screen.getByTestId("project-grid-skeleton")).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user