feat(FN-1149): add send-to-background actions for AI planning modals
- Add Send to background header action to Planning Mode, Subtask Breakdown, and Mission Interview modals using a shared minimize icon pattern - Show the action only during active AI session states and keep it hidden in initial states - Implement background behavior to close the live stream connection and modal without canceling the underlying AI session - Add modal tests that verify button visibility rules and confirm send-to-background does not trigger cancel APIs - Add shared CSS styling for modal send-to-background controls, including hover and focus-visible states
This commit is contained in:
@@ -2,6 +2,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { MissionManager } from "../MissionManager";
|
||||
|
||||
const mockFetchAiSession = vi.fn();
|
||||
const mockCancelMissionInterview = vi.fn();
|
||||
const mockConnectMissionInterviewStream = vi.fn();
|
||||
|
||||
vi.mock("../../api", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../api")>("../../api");
|
||||
return {
|
||||
...actual,
|
||||
fetchAiSession: (...args: any[]) => mockFetchAiSession(...args),
|
||||
cancelMissionInterview: (...args: any[]) => mockCancelMissionInterview(...args),
|
||||
connectMissionInterviewStream: (...args: any[]) => mockConnectMissionInterviewStream(...args),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockMissions = [
|
||||
{
|
||||
@@ -109,6 +123,15 @@ describe("MissionManager", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = globalThis.fetch;
|
||||
mockFetchAiSession.mockReset();
|
||||
mockCancelMissionInterview.mockReset();
|
||||
mockConnectMissionInterviewStream.mockReset();
|
||||
mockFetchAiSession.mockResolvedValue(null);
|
||||
mockCancelMissionInterview.mockResolvedValue(undefined);
|
||||
mockConnectMissionInterviewStream.mockReturnValue({
|
||||
close: vi.fn(),
|
||||
isConnected: () => true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -346,6 +369,72 @@ describe("MissionManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hides send to background button when mission interview is in initial state", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
|
||||
|
||||
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Plan with AI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Plan with AI"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Plan Mission with AI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByLabelText("Send to background")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends mission interview to background without canceling the session", async () => {
|
||||
const closeSpy = vi.fn();
|
||||
mockConnectMissionInterviewStream.mockReturnValueOnce({
|
||||
close: closeSpy,
|
||||
isConnected: () => true,
|
||||
});
|
||||
mockFetchAiSession.mockResolvedValueOnce({
|
||||
id: "session-bg-1",
|
||||
type: "mission_interview",
|
||||
status: "generating",
|
||||
title: "Background mission",
|
||||
inputPayload: JSON.stringify({ missionTitle: "Background mission" }),
|
||||
conversationHistory: "[]",
|
||||
currentQuestion: null,
|
||||
result: null,
|
||||
thinkingOutput: "",
|
||||
error: null,
|
||||
projectId: null,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
globalThis.fetch = createFetchMock();
|
||||
|
||||
render(
|
||||
<MissionManager
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
resumeSessionId="session-bg-1"
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Plan Mission with AI")).toBeInTheDocument();
|
||||
expect(screen.getByText("Preparing next question...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Send to background"));
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mockCancelMissionInterview).not.toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Plan Mission with AI")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows milestone hierarchy in detail view", async () => {
|
||||
globalThis.fetch = createDetailFetchMock();
|
||||
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
Reference in New Issue
Block a user