From a017b53e85ba482c5f7c257bc05e312965e7bf58 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 7 Jul 2026 08:49:52 -0700 Subject: [PATCH] test(dashboard): align app tests with FN-7352/7261/7234 + add fetchWorkflowOptionalSteps mock --- .../app/components/__tests__/AppModals.test.tsx | 6 +++++- .../__tests__/ChangesDiffModal.test.tsx | 9 ++++++++- .../TaskDetailModal.summary-tab.test.tsx | 6 +++++- .../__tests__/board-no-legacy-flash.test.tsx | 17 ++++++++++++++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/dashboard/app/components/__tests__/AppModals.test.tsx b/packages/dashboard/app/components/__tests__/AppModals.test.tsx index ddca365b2a..fc3609a920 100644 --- a/packages/dashboard/app/components/__tests__/AppModals.test.tsx +++ b/packages/dashboard/app/components/__tests__/AppModals.test.tsx @@ -590,7 +590,11 @@ describe("AppModals", () => { fireEvent.click(screen.getByTestId("task-detail-open-detail")); expect(pushStateSpy).toHaveBeenCalledTimes(1); - expect(mockModalManager.openDetailTask).toHaveBeenCalledWith({ id: "FN-2", title: "Nested" }, undefined); + /* + FNXC:TaskDetailNav 2026-07-07-09:15: + FN-7352 (route completed-task refine menus to detail) added a third `opts?: { origin?: DetailTaskOrigin }` argument to openDetailTask / openDetailTaskWithNav, so the modalManager call now carries three args (task, tab, opts). A task-to-task open with no explicit tab/origin passes (task, undefined, undefined). + */ + expect(mockModalManager.openDetailTask).toHaveBeenCalledWith({ id: "FN-2", title: "Nested" }, undefined, undefined); }); }); }); diff --git a/packages/dashboard/app/components/__tests__/ChangesDiffModal.test.tsx b/packages/dashboard/app/components/__tests__/ChangesDiffModal.test.tsx index 6f004595eb..a664b5a46e 100644 --- a/packages/dashboard/app/components/__tests__/ChangesDiffModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChangesDiffModal.test.tsx @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { loadAllAppCss } from "../../test/cssFixture"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { ChangesDiffModal, type NormalizedFile } from "../ChangesDiffModal"; +import { ModalDismissPreferenceProvider } from "../../hooks/useOverlayDismiss"; import type { MergeDetails } from "@fusion/core"; vi.mock("lucide-react", () => ({ @@ -381,8 +382,14 @@ describe("ChangesDiffModal", () => { it("calls onClose when clicking the modal overlay", () => { const onClose = vi.fn(); + /* + FNXC:ChangesDiffModal 2026-07-07-09:20: + FN-7261 (global modal dismissal setting) made backdrop dismissal default-off: useOverlayDismiss only closes when ModalDismissPreferenceProvider enables it. Wrap the render in the provider so the overlay-click dismiss path is exercised (matches AgentErrorDetailsModal.test.tsx). + */ const { container } = render( - , + + + , ); const overlay = container.querySelector(".modal-overlay"); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx index df77346e0d..8e059b84f4 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx @@ -92,7 +92,11 @@ describe("TaskDetailModal Summary tab", () => { expect(container.querySelector(".detail-tabs")?.firstElementChild?.textContent).toBe("Activity"); const summaryButton = screen.getByRole("button", { name: "Summary" }); expectButtonActive(summaryButton); - expect(screen.queryByRole("button", { name: "Chat" })).toBeNull(); + /* + FNXC:TaskDetailTabs 2026-07-07-09:25: + The planner-chat ("Chat") tab now renders unconditionally in the task-detail tab strip (both taskDetailChatFirst branches), so done tasks expose Activity, Chat, Summary, ... (see TaskDetailModal.definition-actions.test.tsx). Done tasks still land on Summary by default; Chat is present but not active. + */ + expect(screen.getByRole("button", { name: "Chat" })).toBeInTheDocument(); expect(screen.getByText("Completion summary")).toBeTruthy(); expect(screen.getByText("summary")).toBeTruthy(); expect(screen.getByText("What changed")).toBeTruthy(); diff --git a/packages/dashboard/app/components/__tests__/board-no-legacy-flash.test.tsx b/packages/dashboard/app/components/__tests__/board-no-legacy-flash.test.tsx index 814684c633..7dbfefd039 100644 --- a/packages/dashboard/app/components/__tests__/board-no-legacy-flash.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-no-legacy-flash.test.tsx @@ -10,6 +10,11 @@ import type { Task } from "@fusion/core"; const apiMocks = vi.hoisted(() => ({ fetchBoardWorkflows: vi.fn(), fetchWorkflowSteps: vi.fn(), + /* + FNXC:BoardNoLegacyFlash 2026-07-07-09:05: + ListView renders QuickEntryBox, whose quick-create path calls fetchWorkflowOptionalSteps (added FN-6304). The partial api mock must expose it or vitest throws "No fetchWorkflowOptionalSteps export" during render and the skeleton/legacy assertions never settle. + */ + fetchWorkflowOptionalSteps: vi.fn(), fetchNodes: vi.fn(), fetchTaskDetail: vi.fn(), batchUpdateTaskModels: vi.fn(), @@ -23,7 +28,7 @@ const apiMocks = vi.hoisted(() => ({ vi.mock("../../api", () => ({ fetchBoardWorkflows: apiMocks.fetchBoardWorkflows, fetchWorkflowSteps: apiMocks.fetchWorkflowSteps, - fetchNodes: apiMocks.fetchNodes, + fetchWorkflowOptionalSteps: apiMocks.fetchWorkflowOptionalSteps, fetchTaskDetail: apiMocks.fetchTaskDetail, batchUpdateTaskModels: apiMocks.batchUpdateTaskModels, promoteTask: apiMocks.promoteTask, @@ -187,6 +192,7 @@ describe("no legacy-board flash before workflow lanes load (FN-6776)", () => { beforeEach(() => { vi.clearAllMocks(); apiMocks.fetchWorkflowSteps.mockResolvedValue([]); + apiMocks.fetchWorkflowOptionalSteps.mockResolvedValue([]); apiMocks.fetchNodes.mockResolvedValue([]); apiMocks.fetchTaskDetail.mockResolvedValue(null); apiMocks.promoteTask.mockResolvedValue({}); @@ -276,14 +282,19 @@ describe("no legacy-board flash before workflow lanes load (FN-6776)", () => { expectSkeleton(surface); }); - it.each(["Board", "ListView"])("%s fetch error exits the skeleton to a terminal legacy layout", async (surface) => { + it.each(["Board", "ListView"])("%s keeps the skeleton on a failed first fetch instead of flashing legacy (non-authoritative failure)", async (surface) => { mockViewport(1024); apiMocks.fetchBoardWorkflows.mockRejectedValue(new Error("network")); renderSurface(surface); expectSkeleton(surface); - await waitFor(() => expectLegacyLayout(surface)); + /* + FNXC:BoardNoLegacyFlash 2026-07-07-09:30: + FN-7234 (preserve board workflow selections) made fetch failures non-authoritative: useBoardWorkflows keeps the current/cache-hydrated payload on a rejected fetch (empty .catch) rather than falling back to legacy. With no cached payload, boardWorkflows stays null so the skeleton persists — the board never flashes legacy on a failed first fetch. Recovery happens on the next visibility/focus/switcher-open re-fetch, not by dropping to legacy. + */ + await waitFor(() => expect(apiMocks.fetchBoardWorkflows).toHaveBeenCalled()); + expectSkeleton(surface); }); it.each(["Board", "ListView"])("%s does not leak cached workflow layouts across project switches", async (surface) => {