Files
fusion/packages/dashboard/app/components/__tests__/utility-mobile.test.tsx
gsxdsm 4eef067baa feat(FN-1141): improve dashboard mobile adaptations
- Extend mobile CSS for SettingsModal, AgentsView, ExecutorStatusBar, ActiveAgentsPanel, toast placement, and background task popovers
- Rename BackgroundTasksIndicator list/item class hooks to align with the updated responsive styling contract
- Add focused component tests covering settings, agents, and utility mobile adaptations plus key CSS media-rule assertions
- Document mobile component adaptation behavior and testing guidance in the dashboard README
2026-04-08 08:48:21 -07:00

184 lines
5.6 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import type { Agent, AiSessionSummary } from "../../api";
import type { Toast } from "../../hooks/useToast";
vi.mock("../../hooks/useExecutorStats", () => ({
useExecutorStats: vi.fn(),
}));
vi.mock("../../hooks/useLiveTranscript", () => ({
useLiveTranscript: vi.fn(() => ({
entries: [],
isConnected: false,
})),
}));
import { useExecutorStats } from "../../hooks/useExecutorStats";
import { BackgroundTasksIndicator } from "../BackgroundTasksIndicator";
import { ExecutorStatusBar } from "../ExecutorStatusBar";
import { ActiveAgentsPanel } from "../ActiveAgentsPanel";
import { ToastContainer } from "../ToastContainer";
const stylesPath = path.resolve(__dirname, "../../styles.css");
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function expectMobileRule(css: string, selector: string, declaration: string): void {
const pattern = new RegExp(
`@media\\s*\\(max-width:\\s*768px\\)\\s*\\{[\\s\\S]*?${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?${escapeRegExp(declaration)}`,
);
expect(pattern.test(css)).toBe(true);
}
describe("Utility component mobile adaptations", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useExecutorStats).mockReturnValue({
stats: {
runningTaskCount: 1,
blockedTaskCount: 2,
stuckTaskCount: 0,
queuedTaskCount: 3,
inReviewCount: 4,
executorState: "running",
maxConcurrent: 5,
lastActivityAt: new Date().toISOString(),
},
loading: false,
error: null,
refresh: vi.fn(),
});
});
it("renders BackgroundTasksIndicator pill when sessions exist", () => {
const sessions: AiSessionSummary[] = [
{
id: "sess-1",
type: "planning",
status: "generating",
title: "Refine onboarding flow",
projectId: "proj-1",
updatedAt: new Date().toISOString(),
},
];
render(
<BackgroundTasksIndicator
sessions={sessions}
generating={1}
needsInput={0}
onOpenSession={vi.fn()}
onDismissSession={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: /AI 1/i })).toBeTruthy();
});
it("renders BackgroundTasksIndicator popover on pill click", () => {
const sessions: AiSessionSummary[] = [
{
id: "sess-2",
type: "subtask",
status: "awaiting_input",
title: "Break down API tasks",
projectId: "proj-1",
updatedAt: new Date().toISOString(),
},
];
render(
<BackgroundTasksIndicator
sessions={sessions}
generating={0}
needsInput={1}
onOpenSession={vi.fn()}
onDismissSession={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /AI 1/i }));
expect(screen.getByText("Background Tasks")).toBeTruthy();
expect(screen.getByText("Break down API tasks")).toBeTruthy();
});
it("returns null for BackgroundTasksIndicator with no sessions", () => {
const { container } = render(
<BackgroundTasksIndicator
sessions={[]}
generating={0}
needsInput={0}
onOpenSession={vi.fn()}
onDismissSession={vi.fn()}
/>,
);
expect(container.firstChild).toBeNull();
});
it("renders ExecutorStatusBar segments", () => {
render(<ExecutorStatusBar tasks={[]} />);
const bar = screen.getByRole("status");
expect(bar).toHaveTextContent("Running");
expect(bar).toHaveTextContent("Blocked");
expect(bar).toHaveTextContent("Queued");
expect(bar).toHaveTextContent("In Review");
});
it("renders ActiveAgentsPanel grid and cards when agents are provided", () => {
const agents: Agent[] = [
{
id: "agent-1",
name: "Live Agent",
role: "executor",
state: "active",
taskId: "FN-555",
lastHeartbeatAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
},
];
const { container } = render(<ActiveAgentsPanel agents={agents} />);
expect(container.querySelector(".active-agents-grid")).toBeTruthy();
expect(container.querySelectorAll(".live-agent-card").length).toBe(1);
});
it("returns null for ActiveAgentsPanel when no agents are active", () => {
const { container } = render(<ActiveAgentsPanel agents={[]} />);
expect(container.firstChild).toBeNull();
});
it("renders toasts in ToastContainer", () => {
const toasts: Toast[] = [
{ id: 1, message: "Saved", type: "success" },
{ id: 2, message: "Failed", type: "error" },
];
const { container } = render(<ToastContainer toasts={toasts} onRemove={vi.fn()} />);
expect(container.querySelector(".toast-container")).toBeTruthy();
expect(container.querySelector(".toast-success")).toBeTruthy();
expect(container.querySelector(".toast-error")).toBeTruthy();
});
it("contains mobile CSS overrides for adapted utility and layout components", () => {
const css = fs.readFileSync(stylesPath, "utf-8");
expectMobileRule(css, ".settings-layout", "flex-direction: column;");
expectMobileRule(css, ".agent-board", "grid-template-columns: 1fr;");
expectMobileRule(css, ".active-agents-grid", "grid-template-columns: 1fr;");
expectMobileRule(css, ".toast-container", "bottom: calc(44px + env(safe-area-inset-bottom, 0px));");
expectMobileRule(css, ".background-tasks-indicator__popover", "position: fixed;");
});
});