import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; // Use vi.hoisted to ensure mock functions are defined before vi.mock factory runs const { mockSavePlanningDescription, mockGetPlanningDescription, mockClearPlanningDescription, mockSaveSubtaskDescription, mockGetSubtaskDescription, mockClearSubtaskDescription, mockSaveMissionGoal, mockGetMissionGoal, mockClearMissionGoal, } = vi.hoisted(() => ({ mockSavePlanningDescription: vi.fn<(description: string, projectId?: string) => void>(), mockGetPlanningDescription: vi.fn<(projectId?: string) => string>(() => ""), mockClearPlanningDescription: vi.fn<(projectId?: string) => void>(), mockSaveSubtaskDescription: vi.fn<(description: string, projectId?: string) => void>(), mockGetSubtaskDescription: vi.fn<(projectId?: string) => string>(() => ""), mockClearSubtaskDescription: vi.fn<(projectId?: string) => void>(), mockSaveMissionGoal: vi.fn<(goal: string, projectId?: string) => void>(), mockGetMissionGoal: vi.fn<(projectId?: string) => string>(() => ""), mockClearMissionGoal: vi.fn<(projectId?: string) => void>(), })); vi.mock("../../hooks/modalPersistence", () => ({ savePlanningDescription: (description: string, projectId?: string) => mockSavePlanningDescription(description, projectId), getPlanningDescription: (projectId?: string) => mockGetPlanningDescription(projectId), clearPlanningDescription: (projectId?: string) => mockClearPlanningDescription(projectId), saveSubtaskDescription: (description: string, projectId?: string) => mockSaveSubtaskDescription(description, projectId), getSubtaskDescription: (projectId?: string) => mockGetSubtaskDescription(projectId), clearSubtaskDescription: (projectId?: string) => mockClearSubtaskDescription(projectId), saveMissionGoal: (goal: string, projectId?: string) => mockSaveMissionGoal(goal, projectId), getMissionGoal: (projectId?: string) => mockGetMissionGoal(projectId), clearMissionGoal: (projectId?: string) => mockClearMissionGoal(projectId), })); // Mock the API functions const { mockStartPlanningStreaming, mockConnectPlanningStream, mockCancelPlanning, mockCreateTaskFromPlanning, mockRespondToPlanning, mockStartSubtaskBreakdown, mockConnectSubtaskStream, mockCancelSubtaskBreakdown, mockCreateTasksFromBreakdown, mockStartMissionInterview, mockConnectMissionInterviewStream, mockCancelMissionInterview, mockCreateMissionFromInterview, mockAcquireSessionLock, mockReleaseSessionLock, mockForceAcquireSessionLock, } = vi.hoisted(() => ({ mockStartPlanningStreaming: vi.fn(), mockConnectPlanningStream: vi.fn(), mockCancelPlanning: vi.fn(), mockCreateTaskFromPlanning: vi.fn(), mockRespondToPlanning: vi.fn(), mockStartSubtaskBreakdown: vi.fn(), mockConnectSubtaskStream: vi.fn(), mockCancelSubtaskBreakdown: vi.fn(), mockCreateTasksFromBreakdown: vi.fn(), mockStartMissionInterview: vi.fn(), mockConnectMissionInterviewStream: vi.fn(), mockCancelMissionInterview: vi.fn(), mockCreateMissionFromInterview: vi.fn(), mockAcquireSessionLock: vi.fn(), mockReleaseSessionLock: vi.fn(), mockForceAcquireSessionLock: vi.fn(), })); vi.mock("../../api", () => ({ startPlanningStreaming: (...args: any[]) => mockStartPlanningStreaming(...args), connectPlanningStream: (...args: any[]) => mockConnectPlanningStream(...args), cancelPlanning: (...args: any[]) => mockCancelPlanning(...args), createTaskFromPlanning: (...args: any[]) => mockCreateTaskFromPlanning(...args), respondToPlanning: (...args: any[]) => mockRespondToPlanning(...args), startSubtaskBreakdown: (...args: any[]) => mockStartSubtaskBreakdown(...args), connectSubtaskStream: (...args: any[]) => mockConnectSubtaskStream(...args), cancelSubtaskBreakdown: (...args: any[]) => mockCancelSubtaskBreakdown(...args), createTasksFromBreakdown: (...args: any[]) => mockCreateTasksFromBreakdown(...args), startMissionInterview: (...args: any[]) => mockStartMissionInterview(...args), connectMissionInterviewStream: (...args: any[]) => mockConnectMissionInterviewStream(...args), cancelMissionInterview: (...args: any[]) => mockCancelMissionInterview(...args), createMissionFromInterview: (...args: any[]) => mockCreateMissionFromInterview(...args), acquireSessionLock: (...args: any[]) => mockAcquireSessionLock(...args), releaseSessionLock: (...args: any[]) => mockReleaseSessionLock(...args), forceAcquireSessionLock: (...args: any[]) => mockForceAcquireSessionLock(...args), fetchSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }), fetchModels: vi.fn().mockResolvedValue({ models: [], favoriteProviders: [], favoriteModels: [] }), fetchWorkflowSteps: vi.fn().mockResolvedValue([]), refineText: vi.fn(), getRefineErrorMessage: vi.fn((err: any) => err?.message || "Failed to refine"), updateGlobalSettings: vi.fn().mockResolvedValue({}), duplicateTask: vi.fn().mockResolvedValue({}), uploadAttachment: vi.fn(), deleteAttachment: vi.fn(), updateTask: vi.fn(), pauseTask: vi.fn(), unpauseTask: vi.fn(), fetchTaskDetail: vi.fn(), requestSpecRevision: vi.fn(), approvePlan: vi.fn(), rejectPlan: vi.fn(), refineTask: vi.fn(), })); const mockConfirm = vi.fn(); vi.mock("../../hooks/useConfirm", () => ({ useConfirm: () => ({ confirm: mockConfirm }), })); // Import components AFTER mocking import { PlanningModeModal } from "../PlanningModeModal"; import { SubtaskBreakdownModal } from "../SubtaskBreakdownModal"; import { MissionInterviewModal } from "../MissionInterviewModal"; describe("ModalReentry", () => { beforeEach(() => { vi.clearAllMocks(); mockGetPlanningDescription.mockReturnValue(""); mockGetSubtaskDescription.mockReturnValue(""); mockGetMissionGoal.mockReturnValue(""); // Default API mocks mockStartPlanningStreaming.mockResolvedValue({ sessionId: "planning-session-1" }); mockConnectPlanningStream.mockReturnValue({ close: vi.fn(), isConnected: () => true }); mockCancelPlanning.mockResolvedValue(undefined); mockCreateTaskFromPlanning.mockResolvedValue({ id: "FN-100" }); mockStartSubtaskBreakdown.mockResolvedValue({ sessionId: "subtask-session-1" }); mockConnectSubtaskStream.mockReturnValue({ close: vi.fn(), isConnected: () => true }); mockCancelSubtaskBreakdown.mockResolvedValue(undefined); mockCreateTasksFromBreakdown.mockResolvedValue({ tasks: [{ id: "FN-101" }, { id: "FN-102" }] }); mockStartMissionInterview.mockResolvedValue({ sessionId: "mission-session-1" }); mockConnectMissionInterviewStream.mockReturnValue({ close: vi.fn(), isConnected: () => true }); mockCancelMissionInterview.mockResolvedValue(undefined); mockCreateMissionFromInterview.mockResolvedValue({ mission: { id: "MSN-001" }, slices: [], features: [], }); mockAcquireSessionLock.mockResolvedValue({ acquired: true, currentHolder: null }); mockReleaseSessionLock.mockResolvedValue(undefined); mockForceAcquireSessionLock.mockResolvedValue({ acquired: true, currentHolder: null }); mockConfirm.mockReset(); mockConfirm.mockResolvedValue(true); }); // ─── PlanningModeModal ─────────────────────────────────────────────── describe("PlanningModal re-entry", () => { const defaultProps = { isOpen: true, onClose: vi.fn(), onTaskCreated: vi.fn(), onTasksCreated: vi.fn(), tasks: [], }; it("reads persisted description from localStorage when no prop provided", async () => { mockGetPlanningDescription.mockReturnValue("Persisted planning description"); render(); await waitFor(() => { expect(mockGetPlanningDescription).toHaveBeenCalled(); }); // Verify the textarea has the persisted value const textarea = document.getElementById("initial-plan") as HTMLTextAreaElement; expect(textarea).toBeTruthy(); expect(textarea.value).toBe("Persisted planning description"); }); it("uses prop value instead of localStorage when initialPlan prop is provided", async () => { mockGetPlanningDescription.mockReturnValue("From localStorage"); render(); // Wait for auto-start (which reads the prop) await waitFor(() => { expect(mockStartPlanningStreaming).toHaveBeenCalledWith("From prop", undefined, undefined, { planningDepth: "medium", customQuestionCount: undefined, }, undefined); }); // localStorage should NOT be read since prop was provided expect(mockGetPlanningDescription).not.toHaveBeenCalled(); }); it("clears localStorage when planning session produces events", async () => { // Set up stream to trigger onQuestion which calls clearPlanningDescription mockConnectPlanningStream.mockImplementation((_sid, _pid, handlers) => { setTimeout(() => handlers.onQuestion({ id: "q1", type: "text", question: "Test?" }), 0); return { close: vi.fn(), isConnected: () => true }; }); render(); await waitFor(() => { expect(mockClearPlanningDescription).toHaveBeenCalled(); }); }); it("saves description to localStorage on cancel", async () => { mockConfirm.mockResolvedValue(true); const { unmount } = render(); // Type something in the textarea const textarea = document.getElementById("initial-plan") as HTMLTextAreaElement; await act(async () => { fireEvent.change(textarea, { target: { value: "My planning text" } }); }); // Click the close button const closeButton = screen.getByLabelText("Close"); await act(async () => { fireEvent.click(closeButton); }); expect(mockSavePlanningDescription).toHaveBeenCalledWith("My planning text", undefined); unmount(); }); it("does not save empty description to localStorage on cancel", async () => { mockConfirm.mockResolvedValue(true); const { unmount } = render(); // Click the close button without typing anything const closeButton = screen.getByLabelText("Close"); await act(async () => { fireEvent.click(closeButton); }); expect(mockSavePlanningDescription).not.toHaveBeenCalled(); unmount(); }); }); // ─── SubtaskBreakdownModal ─────────────────────────────────────────── describe("SubtaskBreakdownModal re-entry", () => { const defaultProps = { isOpen: true, onClose: vi.fn(), initialDescription: "", onTasksCreated: vi.fn(), }; it("reads persisted description from localStorage when no prop provided", async () => { mockGetSubtaskDescription.mockReturnValue("Persisted subtask description"); render(); await waitFor(() => { expect(mockGetSubtaskDescription).toHaveBeenCalled(); }); // Verify the persisted description is shown in the pre element await waitFor(() => { expect(screen.getByText("Persisted subtask description")).toBeInTheDocument(); }); }); it("uses prop value and starts breakdown immediately when initialDescription is provided", async () => { render( ); await waitFor(() => { expect(mockStartSubtaskBreakdown).toHaveBeenCalledWith("Build a complex feature", undefined); }); }); it("clears localStorage when subtasks are received", async () => { // Set up the stream to emit subtasks mockConnectSubtaskStream.mockImplementation((_sid, _pid, handlers) => { // Simulate subtasks arriving synchronously handlers.onSubtasks([{ id: "subtask-1", title: "First", description: "", suggestedSize: "M", dependsOn: [] }]); return { close: vi.fn(), isConnected: () => true }; }); render( ); await waitFor(() => { expect(mockClearSubtaskDescription).toHaveBeenCalled(); }); }); it("saves description to localStorage on close", async () => { mockConfirm.mockResolvedValue(true); // Set up the stream so the modal can start mockConnectSubtaskStream.mockImplementation((_sid, _pid, handlers) => { handlers.onSubtasks([{ id: "subtask-1", title: "First", description: "", suggestedSize: "M", dependsOn: [] }]); return { close: vi.fn(), isConnected: () => true }; }); const { unmount } = render( ); // Close the modal (resetState is called which saves to localStorage) const closeButton = screen.getByLabelText("Close"); await act(async () => { fireEvent.click(closeButton); }); expect(mockSaveSubtaskDescription).toHaveBeenCalledWith("Some description", undefined); unmount(); }); }); // ─── MissionInterviewModal ─────────────────────────────────────────── describe("MissionInterviewModal re-entry", () => { const defaultProps = { isOpen: true, onClose: vi.fn(), onMissionCreated: vi.fn(), }; it("reads persisted goal from localStorage when no prop provided", async () => { mockGetMissionGoal.mockReturnValue("Persisted mission goal"); render(); await waitFor(() => { expect(mockGetMissionGoal).toHaveBeenCalled(); }); // Verify the textarea has the persisted value const textarea = document.getElementById("mission-goal") as HTMLTextAreaElement; expect(textarea).toBeTruthy(); expect(textarea.value).toBe("Persisted mission goal"); }); it("uses prop value instead of localStorage when initialGoal prop is provided", async () => { mockGetMissionGoal.mockReturnValue("From localStorage"); render(); // Wait for auto-start await waitFor(() => { expect(mockStartMissionInterview).toHaveBeenCalledWith("From prop", undefined, undefined); }); // localStorage should NOT be read since prop was provided expect(mockGetMissionGoal).not.toHaveBeenCalled(); }); it("clears localStorage when interview starts successfully", async () => { render(); await waitFor(() => { expect(mockStartMissionInterview).toHaveBeenCalled(); }); // clearMissionGoal is called immediately after startMissionInterview expect(mockClearMissionGoal).toHaveBeenCalled(); }); it("saves goal to localStorage on cancel", async () => { mockConfirm.mockResolvedValue(true); const { unmount } = render(); // Type something in the textarea const textarea = document.getElementById("mission-goal") as HTMLTextAreaElement; await act(async () => { fireEvent.change(textarea, { target: { value: "My mission goal" } }); }); // Click the close button const closeButton = screen.getByLabelText("Close"); await act(async () => { fireEvent.click(closeButton); }); expect(mockSaveMissionGoal).toHaveBeenCalledWith("My mission goal", undefined); unmount(); }); it("does not save empty goal to localStorage on cancel", async () => { mockConfirm.mockResolvedValue(true); const { unmount } = render(); // Click the close button without typing anything const closeButton = screen.getByLabelText("Close"); await act(async () => { fireEvent.click(closeButton); }); expect(mockSaveMissionGoal).not.toHaveBeenCalled(); unmount(); }); }); // ─── Cross-modal storage independence ──────────────────────────────── describe("Storage independence", () => { it("each modal type uses independent persistence functions", () => { // Verify the mock functions are distinct (unit-level independence) expect(mockSavePlanningDescription).not.toBe(mockSaveSubtaskDescription); expect(mockSavePlanningDescription).not.toBe(mockSaveMissionGoal); expect(mockSaveSubtaskDescription).not.toBe(mockSaveMissionGoal); }); }); });