Implements swipe-back gesture support for mobile navigation across ChatView, MissionManager, and PlanningModeModal, wired through a shared navigation history context and covered by a comprehensive test suite (~548 lines of swipe-back tests). A small fixup ensures swipe-back behaves correctly when la Fusion-Task-Id: FN-4139
609 lines
22 KiB
TypeScript
609 lines
22 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { act, render, renderHook, screen, fireEvent, waitFor, within } from "@testing-library/react";
|
|
import * as api from "../../api";
|
|
import { PlanningModeModal } from "../PlanningModeModal";
|
|
import { TaskDetailModal } from "../TaskDetailModal";
|
|
|
|
vi.mock("../../hooks/useNavigationHistory", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../../hooks/useNavigationHistory")>();
|
|
return {
|
|
...actual,
|
|
useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }),
|
|
};
|
|
});
|
|
import { useSessionLock } from "../../hooks/useSessionLock";
|
|
import { getSessionTabId } from "../../utils/getSessionTabId";
|
|
import type { MergeResult } from "@fusion/core";
|
|
import {
|
|
mockStartPlanning,
|
|
mockStartPlanningStreaming,
|
|
mockCreatePlanningDraft,
|
|
mockConnectPlanningStream,
|
|
mockRespondToPlanning,
|
|
mockRetryPlanningSession,
|
|
mockCancelPlanning,
|
|
mockStopPlanningGeneration,
|
|
mockUpdatePlanningSessionDraft,
|
|
mockCreateTaskFromPlanning,
|
|
mockStartPlanningBreakdown,
|
|
mockCreateTasksFromPlanning,
|
|
mockFetchAiSession,
|
|
mockParseConversationHistory,
|
|
mockFetchModels,
|
|
mockAcquireSessionLock,
|
|
mockReleaseSessionLock,
|
|
mockForceAcquireSessionLock,
|
|
mockUploadAttachment,
|
|
mockDeleteAttachment,
|
|
mockUpdateTask,
|
|
mockPauseTask,
|
|
mockUnpauseTask,
|
|
mockFetchTaskDetail,
|
|
mockRequestSpecRevision,
|
|
mockApprovePlan,
|
|
mockRejectPlan,
|
|
mockRefineTask,
|
|
mockFetchAiSessions,
|
|
mockConfirm,
|
|
mockUseViewportMode,
|
|
mockUseMobileKeyboard,
|
|
mockTasks,
|
|
mockModels,
|
|
mockQuestion,
|
|
mockSummary,
|
|
mockTaskDetail,
|
|
MockEventSource,
|
|
getMediaBlocks,
|
|
mockViewport,
|
|
} from "./PlanningModeModal.test-helpers";
|
|
|
|
vi.mock("../../api", () => ({
|
|
startPlanning: (...args: any[]) => mockStartPlanning(...args),
|
|
startPlanningStreaming: (...args: any[]) => mockStartPlanningStreaming(...args),
|
|
createPlanningDraft: (...args: any[]) => mockCreatePlanningDraft(...args),
|
|
connectPlanningStream: (...args: any[]) => mockConnectPlanningStream(...args),
|
|
respondToPlanning: (...args: any[]) => mockRespondToPlanning(...args),
|
|
retryPlanningSession: (...args: any[]) => mockRetryPlanningSession(...args),
|
|
cancelPlanning: (...args: any[]) => mockCancelPlanning(...args),
|
|
stopPlanningGeneration: (...args: any[]) => mockStopPlanningGeneration(...args),
|
|
updatePlanningSessionDraft: (...args: any[]) => mockUpdatePlanningSessionDraft(...args),
|
|
createTaskFromPlanning: (...args: any[]) => mockCreateTaskFromPlanning(...args),
|
|
startPlanningBreakdown: (...args: any[]) => mockStartPlanningBreakdown(...args),
|
|
createTasksFromPlanning: (...args: any[]) => mockCreateTasksFromPlanning(...args),
|
|
fetchAiSession: (...args: any[]) => mockFetchAiSession(...args),
|
|
parseConversationHistory: (...args: any[]) => mockParseConversationHistory(...args),
|
|
acquireSessionLock: (...args: any[]) => mockAcquireSessionLock(...args),
|
|
releaseSessionLock: (...args: any[]) => mockReleaseSessionLock(...args),
|
|
forceAcquireSessionLock: (...args: any[]) => mockForceAcquireSessionLock(...args),
|
|
uploadAttachment: (...args: any[]) => mockUploadAttachment(...args),
|
|
deleteAttachment: (...args: any[]) => mockDeleteAttachment(...args),
|
|
updateTask: (...args: any[]) => mockUpdateTask(...args),
|
|
pauseTask: (...args: any[]) => mockPauseTask(...args),
|
|
unpauseTask: (...args: any[]) => mockUnpauseTask(...args),
|
|
fetchTaskDetail: (...args: any[]) => mockFetchTaskDetail(...args),
|
|
requestSpecRevision: (...args: any[]) => mockRequestSpecRevision(...args),
|
|
approvePlan: (...args: any[]) => mockApprovePlan(...args),
|
|
rejectPlan: (...args: any[]) => mockRejectPlan(...args),
|
|
refineTask: (...args: any[]) => mockRefineTask(...args),
|
|
fetchSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }),
|
|
fetchModels: (...args: any[]) => mockFetchModels(...args),
|
|
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({}),
|
|
fetchAiSessions: (...args: any[]) => mockFetchAiSessions(...args),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useConfirm", () => ({
|
|
useConfirm: () => ({ confirm: mockConfirm }),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useViewportMode", () => ({
|
|
useViewportMode: () => mockUseViewportMode(),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useMobileKeyboard", () => ({
|
|
useMobileKeyboard: (...args: any[]) => mockUseMobileKeyboard(...args),
|
|
}));
|
|
|
|
describe("PlanningModeModal", () => {
|
|
const mockOnClose = vi.fn();
|
|
const mockOnTaskCreated = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockConfirm.mockReset();
|
|
mockConfirm.mockResolvedValue(true);
|
|
MockEventSource.reset();
|
|
vi.stubGlobal("EventSource", MockEventSource as any);
|
|
window.sessionStorage.clear();
|
|
// Default to desktop viewport; mobile-specific tests override per-test.
|
|
mockViewport("desktop");
|
|
|
|
// Default mock for streaming
|
|
mockStartPlanningStreaming.mockResolvedValue({ sessionId: "session-123" });
|
|
// Server's createDraftSession always returns the placeholder title; the
|
|
// real summarized title only arrives later via blur/close summarize or
|
|
// when the session transitions out of draft. Mirror that in the mock so
|
|
// the sidebar render rule (preview while title === placeholder) behaves
|
|
// realistically in tests.
|
|
mockCreatePlanningDraft.mockResolvedValue({ sessionId: "draft-123", title: "New planning session" });
|
|
mockRetryPlanningSession.mockResolvedValue({ success: true, sessionId: "session-123" });
|
|
mockStartPlanningBreakdown.mockResolvedValue({ sessionId: "session-123", subtasks: [] });
|
|
mockFetchAiSession.mockResolvedValue(null);
|
|
mockFetchAiSessions.mockResolvedValue([]);
|
|
mockParseConversationHistory.mockImplementation((raw: string) => {
|
|
if (!raw) return [];
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|
|
mockFetchModels.mockResolvedValue({
|
|
models: mockModels,
|
|
favoriteProviders: [],
|
|
favoriteModels: [],
|
|
resolvedPlanningProvider: "openai",
|
|
resolvedPlanningModelId: "gpt-4o",
|
|
});
|
|
mockAcquireSessionLock.mockResolvedValue({ acquired: true, currentHolder: null });
|
|
mockReleaseSessionLock.mockResolvedValue(undefined);
|
|
mockForceAcquireSessionLock.mockResolvedValue(undefined);
|
|
mockCancelPlanning.mockResolvedValue(undefined);
|
|
mockUpdatePlanningSessionDraft.mockResolvedValue({ ok: true });
|
|
mockStopPlanningGeneration.mockResolvedValue({ success: true });
|
|
|
|
// Default: simulate receiving a question after a brief delay
|
|
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
|
setTimeout(() => {
|
|
handlers.onQuestion?.(mockQuestion);
|
|
}, 10);
|
|
|
|
return {
|
|
close: vi.fn(),
|
|
isConnected: vi.fn().mockReturnValue(true),
|
|
};
|
|
});
|
|
});
|
|
|
|
describe("Initial view", () => {
|
|
it("renders the initial input view when open", () => {
|
|
const { container } = render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText("Planning Mode")).toBeDefined();
|
|
expect(screen.getByPlaceholderText(/e.g., Build a user authentication/)).toBeDefined();
|
|
expect(container.querySelector(".planning-modal-body")).not.toBeNull();
|
|
expect(container.querySelector(".planning-modal-body")?.classList.contains("modal-body")).toBe(false);
|
|
expect(container.querySelector(".planning-examples-label")?.textContent).toBe("Try an example:");
|
|
});
|
|
|
|
it("does not render when closed", () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={false}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByText("Planning Mode")).toBeNull();
|
|
});
|
|
|
|
it("mobile close path blurs focused input and resets viewport scroll", () => {
|
|
mockViewport("mobile");
|
|
const scrollToSpy = vi.spyOn(window, "scrollTo").mockImplementation(() => undefined);
|
|
const rafSpy = vi
|
|
.spyOn(window, "requestAnimationFrame")
|
|
.mockImplementation((callback: FrameRequestCallback) => {
|
|
callback(0);
|
|
return 1;
|
|
});
|
|
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/) as HTMLTextAreaElement;
|
|
act(() => {
|
|
textarea.focus();
|
|
});
|
|
const blurSpy = vi.spyOn(textarea, "blur");
|
|
|
|
act(() => {
|
|
fireEvent.click(screen.getByRole("button", { name: "Close" }));
|
|
});
|
|
|
|
expect(blurSpy).toHaveBeenCalledTimes(1);
|
|
expect(scrollToSpy).toHaveBeenCalledWith(0, 0);
|
|
expect(rafSpy).toHaveBeenCalled();
|
|
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("hides send to background button in initial state", () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByLabelText("Send to background")).toBeNull();
|
|
});
|
|
|
|
it("enables start button when text is entered", () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
const startButton = screen.getByText("Start Planning");
|
|
expect(startButton.closest("button")?.hasAttribute("disabled")).toBe(true);
|
|
|
|
const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/);
|
|
fireEvent.change(textarea, { target: { value: "Test plan" } });
|
|
|
|
expect(startButton.closest("button")?.hasAttribute("disabled")).toBe(false);
|
|
});
|
|
|
|
it("shows example chips", () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByText(/Build a user authentication/)).toBeDefined();
|
|
});
|
|
|
|
it("renders planning model dropdown in initial view", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByRole("button", { name: "Advanced planning settings" })).toBeDefined();
|
|
expect(screen.queryByRole("button", { name: "Planning Model" })).toBeNull();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Advanced planning settings" }));
|
|
|
|
const modelTrigger = screen.getByRole("button", { name: "Planning Model" });
|
|
expect(modelTrigger).toBeDefined();
|
|
|
|
await waitFor(() => {
|
|
expect(mockFetchModels).toHaveBeenCalledTimes(1);
|
|
expect(screen.getByText("openai/gpt-4o")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
it("shows resolved default model badge and switches to override badge when selected", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(mockFetchModels).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Advanced planning settings" }));
|
|
expect(screen.getByText("openai/gpt-4o")).toBeDefined();
|
|
fireEvent.click(screen.getByRole("button", { name: "Planning Model" }));
|
|
fireEvent.click(screen.getByRole("option", { name: /Claude Sonnet 4.5/ }));
|
|
|
|
expect(screen.getByText("anthropic/claude-sonnet-4-5")).toBeDefined();
|
|
});
|
|
|
|
it("passes selected planning model to startPlanningStreaming", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(mockFetchModels).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Advanced planning settings" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Planning Model" }));
|
|
fireEvent.click(screen.getByRole("option", { name: /Claude Sonnet 4.5/ }));
|
|
|
|
const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/);
|
|
fireEvent.change(textarea, { target: { value: "Build auth system" } });
|
|
fireEvent.click(screen.getByText("Start Planning"));
|
|
|
|
await waitFor(() => {
|
|
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, {
|
|
planningModelProvider: "anthropic",
|
|
planningModelId: "claude-sonnet-4-5",
|
|
}, {
|
|
planningDepth: "medium",
|
|
customQuestionCount: undefined,
|
|
}, undefined);
|
|
});
|
|
});
|
|
|
|
it("keeps advanced disclosure collapsed by default and reveals controls when expanded", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
const disclosureButton = screen.getByRole("button", { name: "Advanced planning settings" });
|
|
expect(disclosureButton).toBeDefined();
|
|
|
|
const disclosure = disclosureButton.closest(".onboarding-disclosure");
|
|
expect(disclosure).not.toBeNull();
|
|
const disclosureScope = within(disclosure as HTMLElement);
|
|
|
|
expect(disclosureButton.getAttribute("aria-expanded")).toBe("false");
|
|
expect(disclosureScope.queryByRole("button", { name: "Planning Model" })).toBeNull();
|
|
expect(disclosureScope.queryByText(/Selects which model runs the planning interview/)).toBeNull();
|
|
|
|
fireEvent.click(disclosureButton);
|
|
expect(disclosureButton.getAttribute("aria-expanded")).toBe("true");
|
|
|
|
expect(disclosureScope.getByRole("button", { name: "Planning Model" })).toBeDefined();
|
|
await waitFor(() => {
|
|
expect(disclosureScope.getByText("openai/gpt-4o")).toBeDefined();
|
|
});
|
|
expect(disclosureScope.getByText(/Selects which model runs the planning interview/)).toBeDefined();
|
|
expect(disclosureScope.getByText(/Plan size sets default interview depth/)).toBeDefined();
|
|
expect(disclosureScope.getByRole("button", { name: "Small" })).toBeDefined();
|
|
expect(disclosureScope.getByRole("button", { name: "Medium" }).getAttribute("aria-pressed")).toBe("true");
|
|
expect(disclosureScope.getByRole("button", { name: "Large" })).toBeDefined();
|
|
expect(disclosureScope.getByLabelText("Questions")).toBeDefined();
|
|
});
|
|
|
|
it("updates selected depth and sends custom question count", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Advanced planning settings" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Large" }));
|
|
fireEvent.change(screen.getByLabelText("Questions"), { target: { value: "7" } });
|
|
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
|
|
target: { value: "Build auth system" },
|
|
});
|
|
fireEvent.click(screen.getByText("Start Planning"));
|
|
|
|
await waitFor(() => {
|
|
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined, {
|
|
planningDepth: "large",
|
|
customQuestionCount: 7,
|
|
}, undefined);
|
|
});
|
|
});
|
|
|
|
it("calls startPlanningStreaming without model override when none selected", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/);
|
|
fireEvent.change(textarea, { target: { value: "Build auth system" } });
|
|
fireEvent.click(screen.getByText("Start Planning"));
|
|
|
|
await waitFor(() => {
|
|
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined, {
|
|
planningDepth: "medium",
|
|
customQuestionCount: undefined,
|
|
}, undefined);
|
|
});
|
|
});
|
|
|
|
it("auto-creates a draft after typing and reuses it when starting", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>,
|
|
);
|
|
|
|
const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/);
|
|
fireEvent.change(textarea, { target: { value: "Build a detailed auth system plan" } });
|
|
|
|
await waitFor(() => {
|
|
expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1);
|
|
});
|
|
expect(mockCreatePlanningDraft).toHaveBeenCalledWith(
|
|
"Build a detailed auth system plan",
|
|
undefined,
|
|
undefined,
|
|
);
|
|
|
|
// Sidebar shows the inputPayload-derived preview for draft rows so
|
|
// multiple drafts are distinguishable, not the placeholder title that
|
|
// createDraftSession returns. The text also appears in the textarea
|
|
// value, so scope the query to the sidebar item title element.
|
|
const sidebarItem = document.querySelector(".planning-sidebar-item-title");
|
|
expect(sidebarItem?.textContent).toBe("Build a detailed auth system plan");
|
|
|
|
fireEvent.change(textarea, { target: { value: "Build a detailed auth system plan with extras" } });
|
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
expect(mockCreatePlanningDraft).toHaveBeenCalledTimes(1);
|
|
|
|
fireEvent.click(screen.getByText("Start Planning"));
|
|
await waitFor(() => {
|
|
expect(mockStartPlanningStreaming).toHaveBeenCalledWith(
|
|
"Build a detailed auth system plan with extras",
|
|
undefined,
|
|
undefined,
|
|
{
|
|
planningDepth: "medium",
|
|
customQuestionCount: undefined,
|
|
},
|
|
"draft-123",
|
|
);
|
|
});
|
|
});
|
|
|
|
it("auto-starts planning when initialPlan prop is provided", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
initialPlan="Build a login system from new task dialog"
|
|
/>
|
|
);
|
|
|
|
// Wait for startPlanningStreaming to be called (allow time for setTimeout in useEffect)
|
|
await waitFor(() => {
|
|
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build a login system from new task dialog", undefined, undefined, {
|
|
planningDepth: "medium",
|
|
customQuestionCount: undefined,
|
|
}, undefined);
|
|
}, { timeout: 2000 });
|
|
|
|
// Should transition to question view
|
|
await waitFor(() => {
|
|
expect(screen.getByText("What is the scope?")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
it("sets initial plan text in textarea when initialPlan prop is provided", async () => {
|
|
render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
initialPlan="Pre-filled plan from new task"
|
|
/>
|
|
);
|
|
|
|
// The auto-start should happen with the initial plan (allow time for setTimeout in useEffect)
|
|
await waitFor(() => {
|
|
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Pre-filled plan from new task", undefined, undefined, {
|
|
planningDepth: "medium",
|
|
customQuestionCount: undefined,
|
|
}, undefined);
|
|
}, { timeout: 2000 });
|
|
});
|
|
});
|
|
|
|
describe("modal height constraint regression", () => {
|
|
it("desktop planning modal max-height accounts for overlay padding", async () => {
|
|
const { container } = render(
|
|
<PlanningModeModal
|
|
isOpen={true}
|
|
onClose={mockOnClose}
|
|
onTaskCreated={mockOnTaskCreated}
|
|
onTasksCreated={vi.fn()}
|
|
tasks={mockTasks}
|
|
/>
|
|
);
|
|
|
|
const modal = container.querySelector(".planning-modal");
|
|
expect(modal).toBeTruthy();
|
|
|
|
const { loadAllAppCss } = await import("../../test/cssFixture");
|
|
const css = loadAllAppCss();
|
|
|
|
const blockMatch = css.match(
|
|
/\.planning-modal\s*\{[^}]*max-height:\s*([^;]+);/,
|
|
);
|
|
expect(blockMatch).toBeTruthy();
|
|
|
|
const maxHeightValue = blockMatch![1].trim();
|
|
expect(maxHeightValue).toContain("calc(");
|
|
expect(maxHeightValue).toContain("100dvh");
|
|
expect(maxHeightValue).toContain("--overlay-padding-top");
|
|
});
|
|
|
|
it("uses planning-scoped disclosure overrides to remove inherited content indent", async () => {
|
|
const { loadAllAppCssBaseOnly } = await import("../../test/cssFixture");
|
|
const css = loadAllAppCssBaseOnly();
|
|
|
|
const blockMatch = css.match(
|
|
/\.planning-advanced-disclosure\s+\.onboarding-disclosure-content\s*\{[^}]*\}/,
|
|
);
|
|
expect(blockMatch).toBeTruthy();
|
|
expect(blockMatch![0]).toContain("padding-inline-start: 0;");
|
|
expect(blockMatch![0]).toContain("justify-content: center;");
|
|
});
|
|
|
|
it("keeps mobile question view top spacing compact", async () => {
|
|
const { loadAllAppCss } = await import("../../test/cssFixture");
|
|
const css = loadAllAppCss();
|
|
const mobileBlocks = getMediaBlocks(css, "@media (max-width: 768px)");
|
|
const mobileCss = mobileBlocks.join("\n");
|
|
|
|
expect(mobileCss).toContain(".planning-question-scroll");
|
|
expect(mobileCss).toContain("padding-top: var(--space-sm);");
|
|
expect(mobileCss).toContain("gap: var(--space-md);");
|
|
});
|
|
});
|
|
|
|
});
|