feat(FN-2644): merge fusion/fn-2644
This commit is contained in:
@@ -58,6 +58,14 @@ vi.mock("../NewTaskModal", () => ({
|
||||
NewTaskModal: () => null,
|
||||
}));
|
||||
|
||||
const mockSystemStatsModalProps = vi.fn();
|
||||
vi.mock("../SystemStatsModal", () => ({
|
||||
SystemStatsModal: (props: any) => {
|
||||
mockSystemStatsModalProps(props);
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../ActivityLogModal", () => ({
|
||||
ActivityLogModal: () => null,
|
||||
}));
|
||||
@@ -133,6 +141,7 @@ describe("AppModals", () => {
|
||||
filesOpen: false,
|
||||
fileBrowserWorkspace: "project",
|
||||
usageOpen: false,
|
||||
systemStatsOpen: false,
|
||||
schedulesOpen: false,
|
||||
newTaskModalOpen: false,
|
||||
activityLogOpen: false,
|
||||
@@ -159,6 +168,8 @@ describe("AppModals", () => {
|
||||
setFileWorkspace: vi.fn(),
|
||||
openUsage: vi.fn(),
|
||||
closeUsage: vi.fn(),
|
||||
openSystemStats: vi.fn(),
|
||||
closeSystemStats: vi.fn(),
|
||||
openSchedules: vi.fn(),
|
||||
closeSchedules: vi.fn(),
|
||||
openNewTask: vi.fn(),
|
||||
@@ -194,6 +205,7 @@ describe("AppModals", () => {
|
||||
mockScheduledTasksModalProps.mockClear();
|
||||
mockModelOnboardingModalProps.mockClear();
|
||||
mockSettingsModalProps.mockClear();
|
||||
mockSystemStatsModalProps.mockClear();
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
@@ -344,4 +356,40 @@ describe("AppModals", () => {
|
||||
expect(mockScheduledTasksModalProps.mock.calls[0][0].projectId).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SystemStatsModal wiring", () => {
|
||||
const commonProps = {
|
||||
tasks: [],
|
||||
projects: [],
|
||||
currentProject: null,
|
||||
toasts: mockToasts,
|
||||
removeToast: vi.fn(),
|
||||
projectActions: { handleAddProject: vi.fn(), handleSetupComplete: vi.fn(), handleModelOnboardingComplete: vi.fn() },
|
||||
taskHandlers: { handleModalCreate: vi.fn(), handlePlanningTaskCreated: vi.fn(), handlePlanningTasksCreated: vi.fn(), handleSubtaskTasksCreated: vi.fn(), handleGitHubImport: vi.fn() },
|
||||
taskOperations: { moveTask: vi.fn(), deleteTask: vi.fn(), mergeTask: vi.fn(), retryTask: vi.fn(), duplicateTask: vi.fn() },
|
||||
deepLink: { handleDetailClose: vi.fn() },
|
||||
settings: mockSettings,
|
||||
};
|
||||
|
||||
it("passes modal manager state and projectId through to SystemStatsModal", () => {
|
||||
const closeSystemStats = vi.fn();
|
||||
render(
|
||||
<AppModals
|
||||
{...commonProps}
|
||||
projectId="proj-system"
|
||||
addToast={vi.fn()}
|
||||
modalManager={{ ...mockModalManager, systemStatsOpen: true, closeSystemStats }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(mockSystemStatsModalProps).toHaveBeenCalledTimes(1);
|
||||
expect(mockSystemStatsModalProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
isOpen: true,
|
||||
onClose: closeSystemStats,
|
||||
projectId: "proj-system",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +69,18 @@ describe("Header", () => {
|
||||
expect(screen.getByTitle("Settings")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders system stats button on desktop when handler is provided", () => {
|
||||
renderHeader({ onOpenSystemStats: vi.fn() }, "desktop");
|
||||
expect(screen.getByTitle("System Stats")).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onOpenSystemStats when system stats button is clicked", () => {
|
||||
const onOpenSystemStats = vi.fn();
|
||||
renderHeader({ onOpenSystemStats }, "desktop");
|
||||
fireEvent.click(screen.getByTitle("System Stats"));
|
||||
expect(onOpenSystemStats).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onOpenSettings when settings button is clicked", () => {
|
||||
const onOpenSettings = vi.fn();
|
||||
renderHeader({ onOpenSettings });
|
||||
|
||||
@@ -32,6 +32,7 @@ const createDefaultProps = () => ({
|
||||
modalOpen: false,
|
||||
onOpenSettings: vi.fn(),
|
||||
onOpenActivityLog: vi.fn(),
|
||||
onOpenSystemStats: vi.fn(),
|
||||
onOpenMailbox: vi.fn(),
|
||||
mailboxUnreadCount: 0,
|
||||
onOpenGitManager: vi.fn(),
|
||||
@@ -232,6 +233,7 @@ describe("MobileNavBar", () => {
|
||||
|
||||
expect(screen.getByTestId("mobile-more-item-mailbox")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-activity")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-system-stats")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-git")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-terminal")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-files")).toBeDefined();
|
||||
@@ -293,6 +295,17 @@ describe("MobileNavBar", () => {
|
||||
expect(props.onOpenActivityLog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("system stats item in more sheet calls onOpenSystemStats", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
fireEvent.click(screen.getByTestId("mobile-more-item-system-stats"));
|
||||
|
||||
expect(container.querySelector(".mobile-more-sheet")).toBeNull();
|
||||
expect(props.onOpenSystemStats).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("closes sheet and calls handler when item is clicked", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import { SystemStatsModal } from "../SystemStatsModal";
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
Monitor: () => <span data-testid="icon-monitor" />,
|
||||
RefreshCw: () => <span data-testid="icon-refresh" />,
|
||||
X: () => <span data-testid="icon-x" />,
|
||||
}));
|
||||
|
||||
const mockFetchSystemStats = vi.fn();
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
fetchSystemStats: (...args: unknown[]) => mockFetchSystemStats(...args),
|
||||
}));
|
||||
|
||||
const sampleStats = {
|
||||
systemStats: {
|
||||
rss: 5 * 1024 * 1024 * 1024,
|
||||
heapUsed: 900 * 1024 * 1024,
|
||||
heapTotal: 1200 * 1024 * 1024,
|
||||
heapLimit: 1000 * 1024 * 1024,
|
||||
external: 50 * 1024 * 1024,
|
||||
arrayBuffers: 20 * 1024 * 1024,
|
||||
cpuPercent: null,
|
||||
loadAvg: [1.2, 0.8, 0.5] as [number, number, number],
|
||||
cpuCount: 8,
|
||||
systemTotalMem: 10 * 1024 * 1024 * 1024,
|
||||
systemFreeMem: 1024 * 1024 * 1024,
|
||||
pid: 12345,
|
||||
nodeVersion: "v22.0.0",
|
||||
platform: "darwin/arm64",
|
||||
},
|
||||
taskStats: {
|
||||
total: 6,
|
||||
byColumn: {
|
||||
triage: 1,
|
||||
todo: 2,
|
||||
"in-progress": 1,
|
||||
"in-review": 1,
|
||||
done: 1,
|
||||
archived: 0,
|
||||
},
|
||||
active: 2,
|
||||
agents: {
|
||||
idle: 1,
|
||||
active: 2,
|
||||
running: 0,
|
||||
error: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("SystemStatsModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("shows loading state while initial stats are fetched", async () => {
|
||||
mockFetchSystemStats.mockReturnValue(new Promise(() => undefined));
|
||||
|
||||
render(<SystemStatsModal isOpen={true} onClose={vi.fn()} />);
|
||||
|
||||
expect(await screen.findByText("Loading system stats…")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders fetched metrics across all sections", async () => {
|
||||
mockFetchSystemStats.mockResolvedValue(sampleStats);
|
||||
|
||||
render(<SystemStatsModal isOpen={true} onClose={vi.fn()} projectId="proj-1" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSystemStats).toHaveBeenCalledWith("proj-1");
|
||||
});
|
||||
|
||||
expect(screen.getByText("System Stats")).toBeDefined();
|
||||
expect(screen.getByText("Process")).toBeDefined();
|
||||
expect(screen.getByText("CPU & Load")).toBeDefined();
|
||||
expect(screen.getByText("System")).toBeDefined();
|
||||
expect(screen.getByText("Tasks")).toBeDefined();
|
||||
expect(screen.getByText("Agents")).toBeDefined();
|
||||
|
||||
expect(screen.getByText("5.00 GB")).toBeDefined();
|
||||
expect(screen.getByText("900 MB")).toBeDefined();
|
||||
expect(screen.getByText("9.00 GB")).toBeDefined();
|
||||
expect(screen.getByText("90.0% of 10.00 GB")).toBeDefined();
|
||||
expect(screen.getByText("1.20 0.80 0.50")).toBeDefined();
|
||||
|
||||
const criticalValues = document.querySelectorAll(".system-stats-modal__value--critical");
|
||||
expect(criticalValues.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("shows error state when initial fetch fails", async () => {
|
||||
mockFetchSystemStats.mockRejectedValue(new Error("stats unavailable"));
|
||||
|
||||
render(<SystemStatsModal isOpen={true} onClose={vi.fn()} />);
|
||||
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("stats unavailable");
|
||||
});
|
||||
|
||||
it("refreshes every 5 seconds while open and stops when closed", async () => {
|
||||
vi.useFakeTimers();
|
||||
mockFetchSystemStats.mockResolvedValue(sampleStats);
|
||||
|
||||
const { rerender } = render(<SystemStatsModal isOpen={true} onClose={vi.fn()} />);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(mockFetchSystemStats).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
});
|
||||
expect(mockFetchSystemStats).toHaveBeenCalledTimes(2);
|
||||
|
||||
rerender(<SystemStatsModal isOpen={false} onClose={vi.fn()} />);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
});
|
||||
|
||||
expect(mockFetchSystemStats).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user