Files
fusion/packages/dashboard/app/components/__tests__/agent-modals-mobile.test.tsx
gsxdsm 4b9fd3dd64 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

343 lines
14 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import fs from "node:fs";
import path from "node:path";
import { AgentDetailView } from "../AgentDetailView";
import { AgentGenerationModal } from "../AgentGenerationModal";
import { AgentImportModal } from "../AgentImportModal";
import { AgentListModal } from "../AgentListModal";
import * as api from "../../api";
vi.mock("../../api", () => ({
fetchAgent: vi.fn(),
updateAgent: vi.fn(),
updateAgentState: vi.fn(),
deleteAgent: vi.fn(),
fetchAgentLogs: vi.fn(),
fetchAgentLogsWithMeta: vi.fn(),
fetchAgentRunLogs: vi.fn(),
fetchAgentChildren: vi.fn(),
fetchAgentRuns: vi.fn(),
fetchAgentRunDetail: vi.fn(),
startAgentRun: vi.fn(),
stopAgentRun: vi.fn(),
updateAgentInstructions: vi.fn(),
updateAgentSoul: vi.fn(),
updateAgentMemory: vi.fn(),
fetchAgentMemoryFiles: vi.fn(),
fetchAgentMemoryFile: vi.fn(),
saveAgentMemoryFile: vi.fn(),
fetchAgentTasks: vi.fn(),
fetchChainOfCommand: vi.fn(),
fetchWorkspaceFileContent: vi.fn(),
saveWorkspaceFileContent: vi.fn(),
fetchModels: vi.fn(),
fetchPluginRuntimes: vi.fn(() => Promise.resolve([])),
fetchAgents: vi.fn(),
createAgent: vi.fn(),
startAgentGeneration: vi.fn(),
generateAgentSpec: vi.fn(),
cancelAgentGeneration: vi.fn(),
fetchAgentBudgetStatus: vi.fn(),
resetAgentBudget: vi.fn(),
}));
vi.mock("../AgentLogViewer", () => ({
AgentLogViewer: ({ entries }: { entries: Array<{ text: string }> }) => (
<div data-testid="agent-log-viewer">{entries.map((entry, index) => <span key={index}>{entry.text}</span>)}</div>
),
}));
const mockFetchAgent = vi.mocked(api.fetchAgent);
const mockUpdateAgent = vi.mocked(api.updateAgent);
const mockUpdateAgentState = vi.mocked(api.updateAgentState);
const mockDeleteAgent = vi.mocked(api.deleteAgent);
const mockFetchAgentLogs = vi.mocked(api.fetchAgentLogs);
const mockFetchAgentLogsWithMeta = vi.mocked(api.fetchAgentLogsWithMeta);
const mockFetchAgentRunLogs = vi.mocked(api.fetchAgentRunLogs);
const mockFetchAgentChildren = vi.mocked(api.fetchAgentChildren);
const mockFetchAgentRuns = vi.mocked(api.fetchAgentRuns);
const mockFetchAgentRunDetail = vi.mocked(api.fetchAgentRunDetail);
const mockStartAgentRun = vi.mocked(api.startAgentRun);
const mockStopAgentRun = vi.mocked(api.stopAgentRun);
const mockUpdateAgentInstructions = vi.mocked(api.updateAgentInstructions);
const mockUpdateAgentSoul = vi.mocked(api.updateAgentSoul);
const mockUpdateAgentMemory = vi.mocked(api.updateAgentMemory);
const mockFetchAgentMemoryFiles = vi.mocked(api.fetchAgentMemoryFiles);
const mockFetchAgentMemoryFile = vi.mocked(api.fetchAgentMemoryFile);
const mockSaveAgentMemoryFile = vi.mocked(api.saveAgentMemoryFile);
const mockFetchAgentTasks = vi.mocked(api.fetchAgentTasks);
const mockFetchChainOfCommand = vi.mocked(api.fetchChainOfCommand);
const mockFetchWorkspaceFileContent = vi.mocked(api.fetchWorkspaceFileContent);
const mockSaveWorkspaceFileContent = vi.mocked(api.saveWorkspaceFileContent);
const mockFetchModels = vi.mocked(api.fetchModels);
const mockFetchAgents = vi.mocked(api.fetchAgents);
const mockCreateAgent = vi.mocked(api.createAgent);
const mockStartAgentGeneration = vi.mocked(api.startAgentGeneration);
const mockGenerateAgentSpec = vi.mocked(api.generateAgentSpec);
const mockCancelAgentGeneration = vi.mocked(api.cancelAgentGeneration);
const mockFetchAgentBudgetStatus = vi.mocked(api.fetchAgentBudgetStatus);
const mockResetAgentBudget = vi.mocked(api.resetAgentBudget);
const originalFetch = globalThis.fetch;
import { loadAllAppCss } from "../../test/cssFixture";
function readStyles(): string {
return loadAllAppCss();
}
describe("agent modal mobile CSS structure", () => {
beforeEach(() => {
vi.clearAllMocks();
const mockAgent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "active",
taskId: "FN-001",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
lastHeartbeatAt: "2026-01-01T00:10:00.000Z",
metadata: {},
runtimeConfig: {},
heartbeatHistory: [],
activeRun: null,
completedRuns: [],
};
mockFetchAgent.mockResolvedValue(mockAgent as any);
mockUpdateAgent.mockResolvedValue(mockAgent as any);
mockUpdateAgentState.mockResolvedValue({ ...mockAgent, state: "paused" } as any);
mockDeleteAgent.mockResolvedValue(undefined as any);
mockFetchAgentLogs.mockResolvedValue([]);
mockFetchAgentLogsWithMeta.mockResolvedValue({ entries: [], total: 0, hasMore: false } as any);
mockFetchAgentRunLogs.mockResolvedValue([]);
mockFetchAgentChildren.mockResolvedValue([]);
mockFetchAgentRuns.mockResolvedValue([]);
mockFetchAgentRunDetail.mockResolvedValue(undefined as any);
mockStartAgentRun.mockResolvedValue({ id: "run-001", status: "active" } as any);
mockStopAgentRun.mockResolvedValue({ ok: true });
mockUpdateAgentInstructions.mockResolvedValue(mockAgent as any);
mockUpdateAgentSoul.mockResolvedValue(mockAgent as any);
mockUpdateAgentMemory.mockResolvedValue(mockAgent as any);
mockFetchAgentMemoryFiles.mockResolvedValue({ files: [] } as any);
mockFetchAgentMemoryFile.mockResolvedValue({ content: "" } as any);
mockSaveAgentMemoryFile.mockResolvedValue({ success: true });
mockFetchAgentTasks.mockResolvedValue([]);
mockFetchChainOfCommand.mockResolvedValue([mockAgent] as any);
mockFetchWorkspaceFileContent.mockResolvedValue({ content: "" } as any);
mockSaveWorkspaceFileContent.mockResolvedValue({ success: true, mtime: "2026-01-01T00:00:00.000Z", size: 0 });
mockFetchModels.mockResolvedValue({ models: [], favoriteProviders: [], favoriteModels: [] });
mockFetchAgents.mockResolvedValue([
{
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
metadata: {},
},
] as any);
mockCreateAgent.mockResolvedValue({
id: "agent-002",
name: "New Agent",
role: "executor",
state: "idle",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
metadata: {},
} as any);
mockStartAgentGeneration.mockResolvedValue({
sessionId: "session-1",
roleDescription: "Build a reviewer agent",
});
mockGenerateAgentSpec.mockResolvedValue({
spec: {
title: "Accessibility Reviewer",
icon: "♿",
role: "reviewer",
description: "Reviews UI accessibility compliance",
systemPrompt: "You are an accessibility expert.",
thinkingLevel: "high",
maxTurns: 8,
},
});
mockCancelAgentGeneration.mockResolvedValue({ success: true });
mockFetchAgentBudgetStatus.mockResolvedValue({ agentId: "agent-001", currentUsage: 0, budgetLimit: null, usagePercent: null, thresholdPercent: null, isOverBudget: false, isOverThreshold: false, lastResetAt: null, nextResetAt: null });
mockResetAgentBudget.mockResolvedValue(undefined);
globalThis.fetch = vi.fn(async () =>
({
ok: true,
status: 200,
json: async () => ({
companyName: "Acme AI",
agents: [{ name: "Reviewer", role: "reviewer", title: "Code Reviewer" }],
created: ["Reviewer"],
skipped: [],
errors: [],
dryRun: true,
}),
}) as Response,
);
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
describe("AgentDetailView", () => {
it("overlay and modal have mobile-targetable classes", async () => {
render(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(document.querySelector(".agent-detail-overlay")).toBeTruthy();
expect(document.querySelector(".agent-detail-modal")).toBeTruthy();
});
});
it("tabs have scrollable container class", async () => {
render(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(document.querySelector(".agent-detail-tabs")).toBeTruthy();
expect(document.querySelectorAll(".agent-detail-tab").length).toBeGreaterThan(0);
});
});
it("footer has safe-area class", async () => {
render(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(document.querySelector(".agent-detail-footer")).toBeTruthy();
});
});
it("styles.css has mobile full-screen rules for agent-detail modal", () => {
const styles = readStyles();
expect(styles).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.agent-detail-modal[\s\S]*?width:\s*100vw/);
expect(styles).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.agent-detail-modal[\s\S]*?height:\s*100dvh/);
expect(styles).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.agent-detail-modal[\s\S]*?border-radius:\s*0/);
});
});
describe("AgentGenerationModal", () => {
it("overlay and dialog classes exist", () => {
render(<AgentGenerationModal isOpen={true} onClose={vi.fn()} onGenerated={vi.fn()} />);
expect(document.querySelector(".agent-dialog-overlay")).toBeTruthy();
expect(document.querySelector(".agent-dialog")).toBeTruthy();
});
it("does not hardcode inline width on dialog", () => {
render(<AgentGenerationModal isOpen={true} onClose={vi.fn()} onGenerated={vi.fn()} />);
const dialog = document.querySelector(".agent-dialog") as HTMLElement | null;
expect(dialog).toBeTruthy();
expect(dialog?.style.width).toBe("");
expect(dialog?.style.maxWidth).toBe("");
});
it("summary rows have targetable classes in preview", async () => {
render(<AgentGenerationModal isOpen={true} onClose={vi.fn()} onGenerated={vi.fn()} />);
const user = userEvent.setup();
await user.type(screen.getByLabelText("Role Description"), "Build an accessibility reviewer");
await user.click(screen.getByRole("button", { name: "Generate" }));
await waitFor(() => {
expect(screen.getByRole("button", { name: "Use This" })).toBeInTheDocument();
});
expect(document.querySelectorAll(".agent-dialog-summary-row").length).toBeGreaterThan(0);
});
it("styles.css has mobile full-screen rules for agent-dialog", () => {
const styles = readStyles();
expect(styles).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.agent-dialog[\s\S]*?width:\s*100vw(?:\s*!important)?/);
expect(styles).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.agent-dialog[\s\S]*?border-radius:\s*0/);
});
});
describe("AgentImportModal", () => {
it("import dialog class exists", () => {
render(<AgentImportModal isOpen={true} onClose={vi.fn()} onImported={vi.fn()} />);
expect(document.querySelector(".agent-import-dialog")).toBeTruthy();
});
it("file upload area has targetable class", () => {
render(<AgentImportModal isOpen={true} onClose={vi.fn()} onImported={vi.fn()} />);
expect(document.querySelector(".agent-import-file-upload")).toBeTruthy();
});
it("styles.css has mobile rules for agent-import classes", () => {
const styles = readStyles();
expect(styles).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.agent-import-file-upload[\s\S]*?flex-direction:\s*column/);
});
});
describe("AgentListModal", () => {
it("renders modal with modal--wide class", async () => {
render(<AgentListModal isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(document.querySelector(".modal--wide")).toBeTruthy();
});
});
it("styles.css contains 768px and 640px breakpoints for agent list modal", async () => {
render(<AgentListModal isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Agents")).toBeInTheDocument();
});
const styles = readStyles();
expect(styles).toContain("@media (max-width: 768px)");
expect(styles).toContain("@media (max-width: 640px)");
});
it("styles.css has board single-column at 640px", async () => {
render(<AgentListModal isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Agents")).toBeInTheDocument();
});
const styles = readStyles();
expect(styles).toMatch(/@media \(max-width: 640px\)[\s\S]*?\.agent-list-modal \.agent-board\s*{[\s\S]*?grid-template-columns:\s*1fr/);
});
it("styles.css has controls stacking at 640px", async () => {
render(<AgentListModal isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Agents")).toBeInTheDocument();
});
const styles = readStyles();
expect(styles).toMatch(/@media \(max-width: 640px\)[\s\S]*?\.agent-list-modal \.agent-controls\s*{[\s\S]*?flex-direction:\s*column/);
});
});
describe("Cross-cutting mobile CSS", () => {
it("agent-dialog mobile rules include safe-area inset handling", () => {
const styles = readStyles();
expect(styles).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.agent-dialog[\s\S]*?safe-area-inset-bottom/);
});
it("agent-dialog mobile rules tokenize field font-size for iOS zoom prevention", () => {
const styles = readStyles();
expect(styles).toMatch(/@media \(max-width: 768px\)[\s\S]*?\.agent-dialog-field[\s\S]*?font-size:\s*calc\(var\(--space-md\) \+ var\(--space-xs\)\)/);
});
});
});