Files
fusion/packages/dashboard/app/components/__tests__/utility-mobile.test.tsx
gsxdsm 3031d05a68 fix(dashboard): clear all 661 test-file type errors
Production typecheck (tsconfig.json + tsconfig.app.json) was already
clean, but a third config that includes test files surfaced 661 errors
across 60+ test files — accumulated drift between mock fixtures and
production types. Six parallel typescript-pro agents fixed every one
without touching production code.

Per-scope before/after (errors → 0):
  ChatView                                                     183
  Mailbox + Agent suite (5 files)                              156
  Task / Modal suite (6 files)                                 127
  App + small components (12 files)                             96
  Hooks + api/auth (8 files)                                    48
  Long tail (32 files)                                          51
  -----------------------------------------------------------------
  Total                                                        661

Major fix categories:
- Untyped state objects inferring `never[]` / `null` literals (root
  cause of ~120 errors in ChatView alone — added a single
  `UseChatReturn` annotation)
- Mock objects missing fields that became required: `WorkflowStep.mode`,
  `ChatMessage.thinkingOutput / metadata`, `ChatSession.projectId`,
  `Task.log`, `ProjectHealth` fields, `PtyTerminalSessionInfo.createdAt`,
  `Agent.metadata`, `InboxResponse.total`, etc.
- Mock objects with stale fields that no longer exist:
  `AgentBudgetStatus.budgetPeriod`, `truncated` on log responses,
  `OutboxResponse.unreadCount`, `MergeResult.source/target/details`
- Modal props that became required (e.g. `PlanningModeModal.onTasksCreated`)
- String literals not in narrowed unions (`Column`, `WorkflowStepPhase`,
  `InsightStatus`, `AgentLogType`, etc.)
- `querySelector` returning `Element` cast to `HTMLElement` for
  `@testing-library/react`'s `within()`
- Vitest mock typing: `.mock.calls` access needing `vi.mocked(...)`,
  zero-param tuple handling, generic `vi.fn(() => [])` inferring
  `never[]`

Helpers introduced in test files (no shared infra):
- `makeSettings(overrides)` in ModelSelectorTab.test.tsx
- `makePromptOverrides(overrides)` in AgentPromptsManager.test.tsx
- `FileBrowserTestOverrides` type alias in FileBrowser.test.tsx
- `makeInboxResponse / makeOutboxResponse` in MailboxView.test.tsx

Verification:
- tsc -p tsconfig.json:        exit 0
- tsc -p tsconfig.app.json:    exit 0
- tsc -p tsconfig.test-check.json (new — includes test files): exit 0
- vitest run:                  9639 / 9641 (2 pre-existing failures
                               in terminal-mobile-keyboard-layout.test.ts
                               unrelated to this work; verified via
                               `git stash` + run on clean HEAD)

Adds packages/dashboard/tsconfig.test-check.json to keep this regression
guard available locally — same as tsconfig.app.json minus the test
exclude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:59:52 -07:00

247 lines
7.4 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { loadAllAppCss } from "../../test/cssFixture";
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";
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",
lockedByTab: null,
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",
lockedByTab: null,
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("calls onOpenSession when clicking on a milestone_interview session item", () => {
const onOpenSession = vi.fn();
const sessions: AiSessionSummary[] = [
{
id: "sess-milestone-1",
type: "milestone_interview",
status: "awaiting_input",
title: "Plan milestone scope",
projectId: "proj-1",
lockedByTab: null,
updatedAt: new Date().toISOString(),
},
];
render(
<BackgroundTasksIndicator
sessions={sessions}
generating={0}
needsInput={1}
onOpenSession={onOpenSession}
onDismissSession={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /AI 1/i }));
fireEvent.click(screen.getByText("Plan milestone scope"));
expect(onOpenSession).toHaveBeenCalledWith(sessions[0]);
});
it("calls onOpenSession when clicking on a slice_interview session item", () => {
const onOpenSession = vi.fn();
const sessions: AiSessionSummary[] = [
{
id: "sess-slice-1",
type: "slice_interview",
status: "error",
title: "Plan slice scope",
projectId: "proj-1",
lockedByTab: null,
updatedAt: new Date().toISOString(),
},
];
render(
<BackgroundTasksIndicator
sessions={sessions}
generating={0}
needsInput={0}
onOpenSession={onOpenSession}
onDismissSession={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /AI 1/i }));
fireEvent.click(screen.getByText("Plan slice scope"));
expect(onOpenSession).toHaveBeenCalledWith(sessions[0]);
});
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 = loadAllAppCss();
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", "top: calc(var(--header-height, 57px) + env(safe-area-inset-top, 0px) + var(--space-sm));");
expectMobileRule(css, ".toast-container", "bottom: auto;");
expectMobileRule(css, ".toast-container", "right: var(--space-sm);");
expectMobileRule(css, ".toast-container", "left: var(--space-sm);");
expectMobileRule(css, ".background-tasks-indicator__popover", "position: fixed;");
});
});