feat(FN-1410): add inline missions header parity with agents view
- Add MissionManager component updates with expanded header controls - Implement inline mission actions (interview, autopilot toggle) matching AgentsView style - Add MissionManager unit tests with comprehensive coverage - Add CSS styles for mission header alignment and interaction states
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
Zap,
|
||||
Activity,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { MissionInterviewModal } from "./MissionInterviewModal";
|
||||
@@ -1273,7 +1274,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
data-testid="mission-manager-dialog"
|
||||
>
|
||||
{/* ── Header ── */}
|
||||
<div className="mission-manager__header">
|
||||
<div className={`mission-manager__header${isInline ? " mission-manager__header--inline" : ""}`}>
|
||||
<div className="mission-manager__header-title">
|
||||
{selectedMission ? (
|
||||
<button
|
||||
@@ -1291,15 +1292,32 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
{selectedMission ? selectedMission.title : "Missions"}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={onClose}
|
||||
title="Close"
|
||||
aria-label="Close Mission Manager"
|
||||
data-testid="mission-close-btn"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
{isInline ? (
|
||||
/* Inline mode: show refresh button instead of modal close */
|
||||
<div className="mission-manager__header-controls">
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => loadMissions()}
|
||||
title="Refresh"
|
||||
disabled={loading}
|
||||
aria-label="Refresh missions"
|
||||
data-testid="mission-refresh-btn"
|
||||
>
|
||||
<RefreshCw size={16} className={loading ? "spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
/* Modal mode: show close button */
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={onClose}
|
||||
title="Close"
|
||||
aria-label="Close Mission Manager"
|
||||
data-testid="mission-close-btn"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Body ── */}
|
||||
|
||||
@@ -47,6 +47,7 @@ vi.mock("lucide-react", () => ({
|
||||
Activity: () => <span data-testid="activity-icon">Activity</span>,
|
||||
FileText: () => <span data-testid="file-text-icon">FileText</span>,
|
||||
Minimize2: () => <span data-testid="minimize-icon">Minimize2</span>,
|
||||
RefreshCw: ({ className }: any) => <span data-testid="refresh-icon" className={className}>Refresh</span>,
|
||||
}));
|
||||
|
||||
// Mock data
|
||||
@@ -1079,6 +1080,117 @@ describe("MissionManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Inline vs Modal Header Behavior ──────────────────────────────
|
||||
describe("inline vs modal header behavior", () => {
|
||||
it("renders with page-style header class when isInline is true", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
|
||||
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const header = document.querySelector(".mission-manager__header--inline");
|
||||
expect(header).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not show modal close button in inline mode", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
|
||||
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
// The modal close button should not be present in inline mode
|
||||
expect(screen.queryByTestId("mission-close-btn")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows modal close button in modal mode (isInline=false)", async () => {
|
||||
globalThis.fetch = createFetchMock();
|
||||
render(<MissionManager isOpen={true} isInline={false} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mission-close-btn")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows refresh button in inline mode header", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
|
||||
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mission-refresh-btn")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not show refresh button in modal mode", async () => {
|
||||
globalThis.fetch = createFetchMock();
|
||||
render(<MissionManager isOpen={true} isInline={false} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("mission-refresh-btn")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("refresh button triggers loadMissions when clicked in inline mode", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(mockApiResponse([]));
|
||||
globalThis.fetch = fetchMock;
|
||||
|
||||
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
// Wait for initial load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No missions yet. Create one to start planning.")).toBeDefined();
|
||||
});
|
||||
|
||||
// Reset fetch mock to track reload
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValueOnce(mockApiResponse([]));
|
||||
|
||||
// Click refresh button
|
||||
fireEvent.click(screen.getByTestId("mission-refresh-btn"));
|
||||
|
||||
// Verify missions were fetched again
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("inline mode header has inline class modifier for styling parity with agents view", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
|
||||
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const dialog = screen.getByTestId("mission-manager-dialog");
|
||||
expect(dialog.className).toContain("mission-manager--inline");
|
||||
});
|
||||
});
|
||||
|
||||
it("detail view in inline mode still shows back button", async () => {
|
||||
let callCount = 0;
|
||||
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
|
||||
if (url.includes("/health")) {
|
||||
return Promise.resolve(mockApiResponse(getMockMissionHealth("M-001")));
|
||||
}
|
||||
callCount++;
|
||||
if (callCount <= 1) {
|
||||
return Promise.resolve(mockApiResponse(mockMissions));
|
||||
}
|
||||
return Promise.resolve(mockApiResponse(mockMissionDetail));
|
||||
});
|
||||
|
||||
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Build Auth System")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByText("Build Auth System"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
|
||||
// Close button should still be absent in inline mode even in detail view
|
||||
expect(screen.queryByTestId("mission-close-btn")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("hides send to background button when mission interview is in initial state", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user