FN-5895: fix MissionManager interview resume behavior
Keep MissionManager mission interview state and detail rendering in sync while stabilizing the failing test coverage. - reload assertion-linked feature data when mission details or milestone panels load - keep selected mission detail state synchronized after mission update events instead of forcing a full mission reload - hide the sidebar edit form while the selected mission is already being edited in the detail pane - add a non-destructive Send to background action for resume-launched mission interview modals - tighten MissionManager tests to target the detail pane and slice-scoped controls that now render alongside sidebar content Files changed: docs/missions.md | 2 +- .../app/components/MissionInterviewModal.tsx | 30 ++++++++ .../dashboard/app/components/MissionManager.tsx | 77 +++++++++++-------- .../components/__tests__/MissionManager.test.tsx | 88 +++++++++++++++------- 4 files changed, 137 insertions(+), 60 deletions(-) Fusion-Task-Id: FN-5895 Fusion-Task-Lineage: d018a945-33a7-4471-9636-35bc36dc88a9
This commit is contained in:
@@ -159,7 +159,7 @@ The dashboard supports mission planning workflows where you can:
|
||||
- Track progress at each layer
|
||||
- Persisted missions with `interviewState: "in_progress"` remain visible as interview-styled mission cards in the main mission list so planning work does not disappear after reloads
|
||||
- Resume in-progress mission interview sessions directly from separate transient session rows in the main missions list (`mission_interview` sessions in `generating`, `awaiting_input`, `error`, or `complete`) before a mission record is created; `complete` means the plan summary is ready for review/approval but has not been converted into a mission yet
|
||||
- Mission interview closes are non-destructive: the modal now uses a single close action for header close, backdrop click, and Escape. Closing preserves the in-progress `mission_interview` session, and Missions re-fetches project-scoped transient rows (including on the mobile stacked Missions view) so resume/retry remains discoverable without losing persisted `interviewState: "in_progress"` mission cards. Deletion remains an explicit sidebar action.
|
||||
- Mission interview closes are non-destructive: the modal now uses a single close action for header close, backdrop click, and Escape. Closing preserves the in-progress `mission_interview` session, and Missions re-fetches project-scoped transient rows (including on the mobile stacked Missions view) so resume/retry remains discoverable without losing persisted `interviewState: "in_progress"` mission cards. Resume-launched interview modals also expose a **Send to background** action that performs the same non-destructive park without cancelling the session. Deletion remains an explicit sidebar action.
|
||||
- Mission interview, milestone interview, and slice interview agents have read-only board visibility via `fn_task_list` and `fn_task_get`, so they can reference active backlog context and avoid duplicating in-flight tasks while asking planning questions
|
||||
|
||||
### Mission Interview Drafts
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
Trash2,
|
||||
RefreshCw,
|
||||
Lock,
|
||||
Minimize2,
|
||||
} from "lucide-react";
|
||||
import { ConversationHistory } from "./ConversationHistory";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
@@ -78,6 +79,8 @@ interface MissionInterviewModalProps {
|
||||
projectId?: string;
|
||||
initialGoal?: string;
|
||||
resumeSessionId?: string;
|
||||
onSendToBackground?: () => void;
|
||||
showSendToBackgroundButton?: boolean;
|
||||
}
|
||||
|
||||
interface QuestionResponse {
|
||||
@@ -105,6 +108,8 @@ export function MissionInterviewModal({
|
||||
projectId,
|
||||
initialGoal: initialGoalProp,
|
||||
resumeSessionId,
|
||||
onSendToBackground,
|
||||
showSendToBackgroundButton = false,
|
||||
}: MissionInterviewModalProps) {
|
||||
useMobileScrollLock(isOpen);
|
||||
const [missionGoal, setMissionGoal] = useState("");
|
||||
@@ -127,6 +132,7 @@ export function MissionInterviewModal({
|
||||
const trackedLockSessionRef = useRef<string | null>(null);
|
||||
const [lockSessionId, setLockSessionId] = useState<string | null>(resumeSessionId ?? null);
|
||||
const sessionTabId = useMemo(() => getSessionTabId(), []);
|
||||
const canSendToBackground = showSendToBackgroundButton && view.type !== "initial";
|
||||
const {
|
||||
isLockedByOther,
|
||||
takeControl,
|
||||
@@ -529,6 +535,20 @@ export function MissionInterviewModal({
|
||||
onClose();
|
||||
}, [missionGoal, onClose, projectId, view.type]);
|
||||
|
||||
const handleSendToBackground = useCallback(() => {
|
||||
streamConnectionRef.current?.close();
|
||||
streamConnectionRef.current = null;
|
||||
overlayMouseDownOnSelfRef.current = false;
|
||||
setIsReconnecting(false);
|
||||
setIsRetrying(false);
|
||||
setIsCreating(false);
|
||||
if (onSendToBackground) {
|
||||
onSendToBackground();
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
}, [onClose, onSendToBackground]);
|
||||
|
||||
// Escape key handler
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
@@ -736,6 +756,16 @@ export function MissionInterviewModal({
|
||||
<h3>Plan Mission with AI</h3>
|
||||
</div>
|
||||
<div className="modal-header-actions">
|
||||
{canSendToBackground && (
|
||||
<button
|
||||
className="modal-send-to-background"
|
||||
onClick={handleSendToBackground}
|
||||
title="Send to background"
|
||||
aria-label="Send to background"
|
||||
>
|
||||
<Minimize2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
<button className="modal-close" onClick={handleClose} aria-label="Close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
@@ -783,6 +783,37 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
});
|
||||
}, [loadMissionInterviewDraftRows, loadPendingInterviewSessions]);
|
||||
|
||||
const loadAssertionsForMilestone = useCallback(async (milestoneId: string) => {
|
||||
try {
|
||||
const assertions = await fetchAssertions(milestoneId, projectId);
|
||||
const linkedFeatureEntries = await Promise.all(
|
||||
assertions.map(async (assertion): Promise<readonly [string, MissionFeature[]]> => {
|
||||
try {
|
||||
const features = await fetchFeaturesForAssertion(assertion.id, projectId);
|
||||
return [assertion.id, features] as const;
|
||||
} catch {
|
||||
return [assertion.id, []] as const;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
setAssertionsByMilestone((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(milestoneId, assertions);
|
||||
return next;
|
||||
});
|
||||
setLinkedFeaturesByAssertion((prev) => {
|
||||
const next = new Map(prev);
|
||||
for (const [assertionId, features] of linkedFeatureEntries) {
|
||||
next.set(assertionId, features);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
} catch {
|
||||
// Silently fail - assertions are optional
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
// Detect pending mission interview sessions for resume prompt
|
||||
useEffect(() => {
|
||||
if (!isActive) return;
|
||||
@@ -1034,13 +1065,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
});
|
||||
|
||||
// Load assertions and validation rollup for the selected milestone.
|
||||
fetchAssertions(nextSelectedMilestoneId, projectId).then((assertions) => {
|
||||
setAssertionsByMilestone((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(nextSelectedMilestoneId, assertions);
|
||||
return next;
|
||||
});
|
||||
}).catch(() => { /* silently fail */ });
|
||||
void loadAssertionsForMilestone(nextSelectedMilestoneId);
|
||||
fetchMilestoneValidation(nextSelectedMilestoneId, projectId).then((rollup) => {
|
||||
setValidationRollupByMilestone((prev) => {
|
||||
const next = new Map(prev);
|
||||
@@ -1058,7 +1083,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}, [addToast, projectId]);
|
||||
}, [addToast, loadAssertionsForMilestone, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || !selectedMilestoneId) {
|
||||
@@ -1297,24 +1322,27 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
const handleMissionUpdated = (rawEvent: Event) => {
|
||||
refreshHealth();
|
||||
|
||||
// Update mission status in the list to keep badges in sync
|
||||
const messageEvent = rawEvent as MessageEvent<string>;
|
||||
if (messageEvent.data) {
|
||||
try {
|
||||
const updatedMission = JSON.parse(messageEvent.data);
|
||||
const updatedMission = JSON.parse(messageEvent.data) as Partial<MissionWithSummary> & { id?: string };
|
||||
if (updatedMission?.id) {
|
||||
setMissions((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === updatedMission.id ? { ...m, ...updatedMission } : m
|
||||
)
|
||||
);
|
||||
setSelectedMission((prev) =>
|
||||
prev && prev.id === updatedMission.id
|
||||
? normalizeMissionHierarchy({ ...prev, ...updatedMission })
|
||||
: prev
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore invalid payloads
|
||||
}
|
||||
}
|
||||
|
||||
void loadMissions();
|
||||
refreshMissionSidebar();
|
||||
|
||||
// Reload the selected mission detail to reflect updated mission state (autopilot, status, etc.)
|
||||
@@ -1754,13 +1782,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
if (isExpanding) {
|
||||
next.add(milestoneId);
|
||||
// Load assertions and validation rollup when expanding milestone
|
||||
fetchAssertions(milestoneId, projectId).then((assertions) => {
|
||||
setAssertionsByMilestone((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(milestoneId, assertions);
|
||||
return next;
|
||||
});
|
||||
}).catch(() => { /* silently fail */ });
|
||||
void loadAssertionsForMilestone(milestoneId);
|
||||
fetchMilestoneValidation(milestoneId, projectId).then((rollup) => {
|
||||
setValidationRollupByMilestone((prev) => {
|
||||
const next = new Map(prev);
|
||||
@@ -1773,7 +1795,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [projectId]);
|
||||
}, [loadAssertionsForMilestone, projectId]);
|
||||
|
||||
// Slice handlers
|
||||
const handleCreateSlice = useCallback((milestoneId: string) => {
|
||||
@@ -2017,19 +2039,6 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
|
||||
// ── Assertion handlers ──
|
||||
|
||||
const loadAssertionsForMilestone = useCallback(async (milestoneId: string) => {
|
||||
try {
|
||||
const assertions = await fetchAssertions(milestoneId, projectId);
|
||||
setAssertionsByMilestone((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(milestoneId, assertions);
|
||||
return next;
|
||||
});
|
||||
} catch {
|
||||
// Silently fail - assertions are optional
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const loadValidationRollup = useCallback(async (milestoneId: string) => {
|
||||
try {
|
||||
const rollup = await fetchMilestoneValidation(milestoneId, projectId);
|
||||
@@ -4455,7 +4464,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
{renderMissionListItems(standardMissions)}
|
||||
|
||||
{/* Edit mission form */}
|
||||
{editingMissionId && (
|
||||
{editingMissionId && selectedMission?.id !== editingMissionId && (
|
||||
<div className="mission-form-card">
|
||||
<input
|
||||
type="text"
|
||||
@@ -4805,6 +4814,8 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
key={interviewModalKey}
|
||||
isOpen={showInterviewModal}
|
||||
onClose={handleInterviewModalClose}
|
||||
onSendToBackground={handleInterviewModalClose}
|
||||
showSendToBackgroundButton={interviewLaunchMode === "resume"}
|
||||
onMissionCreated={() => {
|
||||
loadMissions();
|
||||
addToast("Mission created from AI interview", "success");
|
||||
|
||||
@@ -2928,9 +2928,12 @@ describe("MissionManager", () => {
|
||||
fireEvent.click(screen.getByText("Generated Mission"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Generated Milestone")).toBeDefined();
|
||||
expect(screen.getByText("Generated Slice")).toBeDefined();
|
||||
expect(screen.getByText("Generated Feature")).toBeDefined();
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
|
||||
expect(detailPane).toBeTruthy();
|
||||
expect(within(detailPane as HTMLElement).getByText("Generated Milestone")).toBeDefined();
|
||||
const generatedSlice = within(detailPane as HTMLElement).getByText("Generated Slice").closest(".mission-slice");
|
||||
expect(generatedSlice).toBeTruthy();
|
||||
expect(within(generatedSlice as HTMLElement).getByText("Generated Feature")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3173,7 +3176,8 @@ describe("MissionManager", () => {
|
||||
|
||||
await waitForDetailLoaded();
|
||||
|
||||
fireEvent.click(screen.getAllByLabelText("Edit mission")[0]);
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
|
||||
fireEvent.click(within(detailPane).getAllByLabelText("Edit mission")[0]);
|
||||
|
||||
const targetBranchInput = await screen.findByLabelText("Mission target branch");
|
||||
expect(targetBranchInput).toHaveValue("develop");
|
||||
@@ -3207,11 +3211,12 @@ describe("MissionManager", () => {
|
||||
|
||||
await waitForDetailLoaded();
|
||||
|
||||
fireEvent.click(screen.getAllByLabelText("Edit mission")[0]);
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
|
||||
fireEvent.click(within(detailPane).getAllByLabelText("Edit mission")[0]);
|
||||
|
||||
const targetBranchInput = await screen.findByLabelText("Mission target branch");
|
||||
fireEvent.change(targetBranchInput, { target: { value: " release/2026.05 " } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Update" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /Update/ }));
|
||||
|
||||
await waitFor(() => {
|
||||
const patchCall = fetchMock.mock.calls.find(
|
||||
@@ -3633,12 +3638,18 @@ describe("MissionManager", () => {
|
||||
});
|
||||
fireEvent.click(screen.getByText("Triage Test Mission"));
|
||||
|
||||
let featureRow: HTMLElement;
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Feature")).toBeDefined();
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
|
||||
expect(detailPane).toBeTruthy();
|
||||
const testSlice = within(detailPane as HTMLElement).getByText("Test Slice").closest(".mission-slice");
|
||||
expect(testSlice).toBeTruthy();
|
||||
featureRow = within(testSlice as HTMLElement).getByText("Test Feature").closest(".mission-feature") as HTMLElement;
|
||||
expect(featureRow).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click triage button
|
||||
fireEvent.click(screen.getByTitle("Triage — create task"));
|
||||
fireEvent.click(within(featureRow!).getByTitle("Triage — create task"));
|
||||
|
||||
// Preview should appear
|
||||
await waitFor(() => {
|
||||
@@ -3657,12 +3668,18 @@ describe("MissionManager", () => {
|
||||
});
|
||||
fireEvent.click(screen.getByText("Triage Test Mission"));
|
||||
|
||||
let featureRow: HTMLElement;
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Feature")).toBeDefined();
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
|
||||
expect(detailPane).toBeTruthy();
|
||||
const testSlice = within(detailPane as HTMLElement).getByText("Test Slice").closest(".mission-slice");
|
||||
expect(testSlice).toBeTruthy();
|
||||
featureRow = within(testSlice as HTMLElement).getByText("Test Feature").closest(".mission-feature") as HTMLElement;
|
||||
expect(featureRow).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click triage button to show preview
|
||||
fireEvent.click(screen.getByTitle("Triage — create task"));
|
||||
fireEvent.click(within(featureRow!).getByTitle("Triage — create task"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Enriched Description Preview")).toBeDefined();
|
||||
@@ -3687,12 +3704,18 @@ describe("MissionManager", () => {
|
||||
});
|
||||
fireEvent.click(screen.getByText("Triage Test Mission"));
|
||||
|
||||
let featureRow: HTMLElement;
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Feature")).toBeDefined();
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
|
||||
expect(detailPane).toBeTruthy();
|
||||
const testSlice = within(detailPane as HTMLElement).getByText("Test Slice").closest(".mission-slice");
|
||||
expect(testSlice).toBeTruthy();
|
||||
featureRow = within(testSlice as HTMLElement).getByText("Test Feature").closest(".mission-feature") as HTMLElement;
|
||||
expect(featureRow).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click triage button to show preview
|
||||
fireEvent.click(screen.getByTitle("Triage — create task"));
|
||||
fireEvent.click(within(featureRow!).getByTitle("Triage — create task"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Enriched Description Preview")).toBeDefined();
|
||||
@@ -3723,12 +3746,18 @@ describe("MissionManager", () => {
|
||||
});
|
||||
fireEvent.click(screen.getByText("Triage Test Mission"));
|
||||
|
||||
let featureRow: HTMLElement;
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Feature")).toBeDefined();
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
|
||||
expect(detailPane).toBeTruthy();
|
||||
const testSlice = within(detailPane as HTMLElement).getByText("Test Slice").closest(".mission-slice");
|
||||
expect(testSlice).toBeTruthy();
|
||||
featureRow = within(testSlice as HTMLElement).getByText("Test Feature").closest(".mission-feature") as HTMLElement;
|
||||
expect(featureRow).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click triage button - should fall back to direct triage
|
||||
fireEvent.click(screen.getByTitle("Triage — create task"));
|
||||
fireEvent.click(within(featureRow!).getByTitle("Triage — create task"));
|
||||
|
||||
// Should call triageFeature directly
|
||||
await waitFor(() => {
|
||||
@@ -3938,7 +3967,8 @@ describe("MissionManager", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Autopilot Mission")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByText("Autopilot Mission"));
|
||||
const sidebar = document.querySelector(".mission-manager__sidebar") as HTMLElement;
|
||||
fireEvent.click(within(sidebar).getByText("Autopilot Mission"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Autopilot")).toBeDefined();
|
||||
@@ -4292,8 +4322,9 @@ describe("MissionManager", () => {
|
||||
await waitFor(() => expect(screen.getByText("Build Auth System")).toBeDefined());
|
||||
const sidebar = document.querySelector(".mission-manager__sidebar") as HTMLElement;
|
||||
fireEvent.click(within(sidebar).getByText("Build Auth System"));
|
||||
await waitFor(() => expect(screen.getByLabelText("Delete mission")).toBeDefined());
|
||||
fireEvent.click(screen.getByLabelText("Delete mission"));
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
|
||||
await waitFor(() => expect(within(detailPane).getAllByLabelText("Delete mission")[0]).toBeDefined());
|
||||
fireEvent.click(within(detailPane).getAllByLabelText("Delete mission")[0]);
|
||||
await waitFor(() => expect(document.querySelector(".mission-manager__detail-pane .mission-confirm-panel")).toBeTruthy());
|
||||
});
|
||||
|
||||
@@ -4453,8 +4484,9 @@ describe("MissionManager", () => {
|
||||
|
||||
await waitFor(() => expect(screen.getByText("Build Auth System")).toBeInTheDocument());
|
||||
fireEvent.click(within(document.querySelector(".mission-manager__sidebar") as HTMLElement).getByText("Build Auth System"));
|
||||
await waitFor(() => expect(screen.getByLabelText("Delete mission")).toBeInTheDocument());
|
||||
fireEvent.click(screen.getByLabelText("Delete mission"));
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
|
||||
await waitFor(() => expect(within(detailPane).getAllByLabelText("Delete mission")[0]).toBeInTheDocument());
|
||||
fireEvent.click(within(detailPane).getAllByLabelText("Delete mission")[0]);
|
||||
|
||||
await waitFor(() => expect(document.querySelector(".mission-manager__detail-pane .mission-confirm-panel")).toBeTruthy());
|
||||
});
|
||||
@@ -5058,7 +5090,7 @@ describe("MissionManager", () => {
|
||||
if (url.includes("/events")) return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url, mockMissionEvents)));
|
||||
if (url.includes("/health")) return Promise.resolve(mockApiResponse(getMockMissionHealth(extractMissionId(url) ?? "M-001")));
|
||||
if (url.includes("/autopilot")) return Promise.resolve(mockApiResponse(mockAutopilotStatus));
|
||||
if (url.includes("/milestones/MS-001/assertions/CA-ENF-1/features")) {
|
||||
if (url.includes("/missions/assertions/CA-ENF-1/features")) {
|
||||
return Promise.resolve(mockApiResponse([{ id: "F-001", title: "User model" }]));
|
||||
}
|
||||
if (url.includes("/milestones/MS-001/assertions")) return Promise.resolve(mockApiResponse([assertion]));
|
||||
@@ -5074,7 +5106,9 @@ describe("MissionManager", () => {
|
||||
fireEvent.click(await screen.findByText("Build Auth System"));
|
||||
await waitForDetailLoaded();
|
||||
|
||||
expect(await screen.findByTestId("mission-assertion-enforcement-CA-ENF-1")).toHaveTextContent("Enforced gate");
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mission-assertion-enforcement-CA-ENF-1")).toHaveTextContent("Enforced gate");
|
||||
});
|
||||
|
||||
const noAssertionMission = JSON.parse(JSON.stringify(missionDetail)) as typeof missionDetail;
|
||||
noAssertionMission.milestones[0].slices[0].features[0].id = "F-INFO-1";
|
||||
@@ -5169,7 +5203,7 @@ describe("MissionManager", () => {
|
||||
|
||||
const milestone = screen.getByText("Database Schema").closest(".mission-milestone");
|
||||
expect(milestone).toBeTruthy();
|
||||
const acceptanceLabel = within(milestone as HTMLElement).getByText("Acceptance:");
|
||||
const acceptanceLabel = within(milestone as HTMLElement).getAllByText("Acceptance:")[0];
|
||||
expect(acceptanceLabel.tagName).toBe("STRONG");
|
||||
expect(within(milestone as HTMLElement).getByText("MILESTONE_BOLD").tagName).toBe("STRONG");
|
||||
|
||||
@@ -5179,7 +5213,7 @@ describe("MissionManager", () => {
|
||||
expect((slice as HTMLElement).querySelectorAll("li")).toHaveLength(1);
|
||||
expect(within(slice as HTMLElement).getByText("VERIFY_BULLET")).toBeInTheDocument();
|
||||
|
||||
const feature = screen.getByText("User model").closest(".mission-feature");
|
||||
const feature = within(slice as HTMLElement).getByText("User model").closest(".mission-feature");
|
||||
expect(feature).toBeTruthy();
|
||||
expect(within(feature as HTMLElement).getByText("FEATURE_DESC_BOLD").tagName).toBe("STRONG");
|
||||
expect(within(feature as HTMLElement).getByText("FEATURE_AC_BOLD").tagName).toBe("STRONG");
|
||||
@@ -5201,7 +5235,8 @@ describe("MissionManager", () => {
|
||||
|
||||
fireEvent.click(screen.getByText("Build Auth System"));
|
||||
await waitForDetailLoaded();
|
||||
fireEvent.click(screen.getByLabelText("Edit mission"));
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
|
||||
fireEvent.click(within(detailPane).getAllByLabelText("Edit mission")[0]);
|
||||
|
||||
const strategySelect = await screen.findByLabelText("Mission branch strategy");
|
||||
expect(strategySelect).toBeInTheDocument();
|
||||
@@ -5229,12 +5264,13 @@ describe("MissionManager", () => {
|
||||
|
||||
fireEvent.click(screen.getByText("Build Auth System"));
|
||||
await waitForDetailLoaded();
|
||||
fireEvent.click(screen.getByLabelText("Edit mission"));
|
||||
const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
|
||||
fireEvent.click(within(detailPane).getAllByLabelText("Edit mission")[0]);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Mission target branch"), { target: { value: "release/2026" } });
|
||||
fireEvent.change(screen.getByLabelText("Mission branch strategy"), { target: { value: "custom-new" } });
|
||||
fireEvent.change(await screen.findByLabelText("Mission branch name"), { target: { value: "feature/mission-custom" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Update" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /Update/ }));
|
||||
|
||||
await waitFor(() => {
|
||||
const patchCall = fetchSpy.mock.calls.find(([input, init]) =>
|
||||
|
||||
Reference in New Issue
Block a user