Files
fusion/packages/dashboard/app/components/__tests__/AppModals.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

409 lines
14 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { AppModals } from "../AppModals";
import type { ModalManager } from "../../hooks/useModalManager";
import type { Toast } from "../../hooks/useToast";
// Mock the modals to avoid rendering all of them
vi.mock("../TaskDetailModal", () => ({
TaskDetailModal: () => null,
}));
const mockSettingsModalProps = vi.fn();
vi.mock("../SettingsModal", () => ({
SettingsModal: (props: any) => {
mockSettingsModalProps(props);
return <div data-testid="settings-modal">Settings Modal</div>;
},
}));
vi.mock("../GitHubImportModal", () => ({
GitHubImportModal: () => null,
}));
vi.mock("../PlanningModeModal", () => ({
PlanningModeModal: () => null,
}));
vi.mock("../SubtaskBreakdownModal", () => ({
SubtaskBreakdownModal: () => null,
}));
vi.mock("../TerminalModal", () => ({
TerminalModal: () => null,
}));
vi.mock("../ScriptsModal", () => ({
ScriptsModal: () => null,
}));
vi.mock("../FileBrowserModal", () => ({
FileBrowserModal: () => null,
}));
vi.mock("../UsageIndicator", () => ({
UsageIndicator: () => null,
}));
// Mock ScheduledTasksModal to capture props
const mockScheduledTasksModalProps = vi.fn();
vi.mock("../ScheduledTasksModal", () => ({
ScheduledTasksModal: ({ projectId, ...rest }: any) => {
mockScheduledTasksModalProps({ projectId, rest });
return null;
},
}));
vi.mock("../NewTaskModal", () => ({
NewTaskModal: () => null,
}));
const mockSystemStatsModalProps = vi.fn();
vi.mock("../SystemStatsModal", () => ({
SystemStatsModal: (props: any) => {
mockSystemStatsModalProps(props);
return null;
},
}));
vi.mock("../ActivityLogModal", () => ({
ActivityLogModal: () => null,
}));
vi.mock("../GitManagerModal", () => ({
GitManagerModal: () => null,
}));
vi.mock("../WorkflowStepManager", () => ({
WorkflowStepManager: () => null,
}));
vi.mock("../AgentListModal", () => ({
AgentListModal: () => null,
}));
vi.mock("../SetupWizardModal", () => ({
SetupWizardModal: () => null,
}));
const mockModelOnboardingModalProps = vi.fn();
vi.mock("../ModelOnboardingModal", () => ({
ModelOnboardingModal: (props: any) => {
mockModelOnboardingModalProps(props);
return null;
},
}));
vi.mock("../ToastContainer", () => ({
ToastContainer: () => null,
}));
vi.mock("../../hooks/useTaskHandlers", () => ({
useTaskHandlers: () => ({
handleModalCreate: vi.fn(),
handlePlanningTaskCreated: vi.fn(),
handlePlanningTasksCreated: vi.fn(),
handleSubtaskTasksCreated: vi.fn(),
handleGitHubImport: vi.fn(),
}),
}));
vi.mock("../../hooks/useProjectActions", () => ({
useProjectActions: () => ({
handleSetupComplete: vi.fn(),
handleModelOnboardingComplete: vi.fn(),
}),
}));
// Mock @fusion/core types
vi.mock("@fusion/core", () => ({}));
// Mock ModalErrorBoundary
vi.mock("../ErrorBoundary", () => ({
ModalErrorBoundary: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
describe("AppModals", () => {
const mockModalManager: ModalManager = {
// State
detailTask: null,
detailTaskInitialTab: "definition",
settingsOpen: false,
settingsInitialSection: undefined,
githubImportOpen: false,
isPlanningOpen: false,
planningInitialPlan: null,
planningResumeSessionId: undefined,
isSubtaskOpen: false,
subtaskInitialDescription: null,
subtaskResumeSessionId: undefined,
terminalOpen: false,
terminalInitialCommand: undefined,
scriptsOpen: false,
filesOpen: false,
fileBrowserWorkspace: "project",
usageOpen: false,
usageAnchorRect: null,
systemStatsOpen: false,
schedulesOpen: false,
newTaskModalOpen: false,
activityLogOpen: false,
gitManagerOpen: false,
workflowStepsOpen: false,
agentsOpen: false,
setupWizardOpen: false,
modelOnboardingOpen: false,
anyModalOpen: false,
// Handlers
openDetailTask: vi.fn(),
openDetailWithChangesTab: vi.fn(),
updateDetailTask: vi.fn(),
closeDetailTask: vi.fn(),
openSettings: vi.fn(),
closeSettings: vi.fn(),
openGitHubImport: vi.fn(),
closeGitHubImport: vi.fn(),
openPlanning: vi.fn(),
openPlanningWithInitialPlan: vi.fn(),
resumePlanning: vi.fn(),
openPlanningWithSession: vi.fn(),
closePlanning: vi.fn(),
openSubtaskBreakdown: vi.fn(),
openSubtaskWithSession: vi.fn(),
closeSubtask: vi.fn(),
toggleTerminal: vi.fn(),
closeTerminal: vi.fn(),
openScripts: vi.fn(),
closeScripts: vi.fn(),
runScript: vi.fn(),
openFiles: vi.fn(),
closeFiles: vi.fn(),
setFileWorkspace: vi.fn(),
openUsage: vi.fn(),
closeUsage: vi.fn(),
openSystemStats: vi.fn(),
closeSystemStats: vi.fn(),
openSchedules: vi.fn(),
closeSchedules: vi.fn(),
openNewTask: vi.fn(),
closeNewTask: vi.fn(),
openActivityLog: vi.fn(),
closeActivityLog: vi.fn(),
openGitManager: vi.fn(),
closeGitManager: vi.fn(),
openWorkflowSteps: vi.fn(),
closeWorkflowSteps: vi.fn(),
openAgents: vi.fn(),
closeAgents: vi.fn(),
openSetupWizard: vi.fn(),
closeSetupWizard: vi.fn(),
openModelOnboarding: vi.fn(),
closeModelOnboarding: vi.fn(),
onPlanningTaskCreated: vi.fn(),
onPlanningTasksCreated: vi.fn(),
onSubtaskTasksCreated: vi.fn(),
};
const mockToasts: Toast[] = [];
const mockSettings = {
prAuthAvailable: false,
themeMode: "dark" as const,
colorTheme: "default" as const,
setThemeMode: vi.fn(),
setColorTheme: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
mockScheduledTasksModalProps.mockClear();
mockModelOnboardingModalProps.mockClear();
mockSettingsModalProps.mockClear();
mockSystemStatsModalProps.mockClear();
});
it("renders without crashing", () => {
render(
<AppModals
projectId={undefined}
tasks={[]}
projects={[]}
currentProject={null}
addToast={vi.fn()}
toasts={mockToasts}
removeToast={vi.fn()}
modalManager={mockModalManager}
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}
/>
);
expect(document.body).toBeDefined();
});
describe("ModelOnboardingModal wiring", () => {
it("passes empty project id and setup-wizard callback into onboarding modal when no project is selected", () => {
const handleAddProject = vi.fn();
const manager = { ...mockModalManager, modelOnboardingOpen: true };
render(
<AppModals
projectId={undefined}
tasks={[]}
projects={[]}
currentProject={null}
addToast={vi.fn()}
toasts={mockToasts}
removeToast={vi.fn()}
modalManager={manager}
projectActions={{ handleAddProject, 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}
/>,
);
expect(mockModelOnboardingModalProps).toHaveBeenCalledTimes(1);
const props = mockModelOnboardingModalProps.mock.calls[0][0];
expect(props.projectId).toBe("");
expect(props.onOpenSetupWizard).toBe(handleAddProject);
});
it("passes active project id into onboarding modal when a project is selected", () => {
const manager = { ...mockModalManager, modelOnboardingOpen: true };
render(
<AppModals
projectId="proj_123"
tasks={[]}
projects={[]}
currentProject={null}
addToast={vi.fn()}
toasts={mockToasts}
removeToast={vi.fn()}
modalManager={manager}
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}
/>,
);
expect(mockModelOnboardingModalProps).toHaveBeenCalledTimes(1);
const props = mockModelOnboardingModalProps.mock.calls[0][0];
expect(props.projectId).toBe("proj_123");
});
});
describe("Settings modal lazy loading", () => {
it("renders SettingsModal asynchronously when settingsOpen is true", async () => {
render(
<AppModals
projectId="proj-123"
tasks={[]}
projects={[]}
currentProject={null}
addToast={vi.fn()}
toasts={mockToasts}
removeToast={vi.fn()}
modalManager={{ ...mockModalManager, settingsOpen: true, settingsInitialSection: "memory" }}
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}
/>,
);
expect(await screen.findByTestId("settings-modal")).toBeInTheDocument();
await waitFor(() => expect(mockSettingsModalProps).toHaveBeenCalled());
const props = mockSettingsModalProps.mock.calls[0][0];
expect(props.projectId).toBe("proj-123");
expect(props.initialSection).toBe("memory");
});
});
describe("ScheduledTasksModal projectId forwarding", () => {
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("does not render ScheduledTasksModal when schedulesOpen is false", () => {
render(
<AppModals
{...commonProps}
projectId="proj-123"
addToast={vi.fn()}
modalManager={{ ...mockModalManager, schedulesOpen: false }}
/>,
);
expect(mockScheduledTasksModalProps).not.toHaveBeenCalled();
});
it.each<[string, string | undefined, string | undefined]>([
["defined project id", "proj-abc", "proj-abc"],
["undefined project id", undefined, undefined],
["empty string project id passes through as-is", "", ""],
])("forwards projectId through to ScheduledTasksModal — %s", (_label, input, expected) => {
render(
<AppModals
{...commonProps}
projectId={input}
addToast={vi.fn()}
modalManager={{ ...mockModalManager, schedulesOpen: true }}
/>,
);
expect(mockScheduledTasksModalProps).toHaveBeenCalledTimes(1);
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",
}),
);
});
});
});