/* FNXC:TaskDetailTabs 2026-06-17-08:20: FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state. FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 — the footer "Actions" dropdown button name is matched EXACTLY (`{ name: "Actions" }`) throughout this file, not via a loose `/actions/i` regex. The now-universal Oversight overflow trigger's aria-label is "Oversight actions", which also matches `/actions/i` and made every such query ambiguous once the trigger stopped being a mobile-only affordance. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; // FNXC:Markdown 2026-06-23-03:30: Mock the heavy `mermaid` library so the shared // markdown pipeline's MermaidDiagram resolves without loading the real renderer. vi.mock("mermaid", () => ({ default: { initialize: vi.fn(), render: vi.fn().mockResolvedValue({ svg: "" }), }, })); import userEvent from "@testing-library/user-event"; import { makeTask, noop, noopDelete, noopMerge, noopMove, noopOpenDetail, noopRetry, mockConfirm, mockUsePluginUiSlots, expectBaseRule, readDashboardStylesSource, setupTaskDetailModalHooks, } from "./TaskDetailModal.test-helpers"; import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal"; import * as dashboardApi from "../../api"; import { FileBrowserProvider } from "../../context/FileBrowserContext"; setupTaskDetailModalHooks(); describe("TaskDetailModal", () => { describe("workflow timestamp badge", () => { const workflowPayload = { flagEnabled: true, defaultWorkflowId: "builtin:coding", workflows: [ { id: "builtin:coding", name: "Coding", columns: [], fields: [{ id: "risk", name: "Risk", type: "text" }] }, { id: "wf-docs", name: "Docs", columns: [] }, ], taskWorkflowIds: { "FN-101": "wf-docs" }, }; beforeEach(() => { vi.mocked(dashboardApi.fetchBoardWorkflows).mockReset(); vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValue({ flagEnabled: false, defaultWorkflowId: "", workflows: [], taskWorkflowIds: {}, }); }); function renderDetail(task = makeTask({ id: "FN-101", column: "todo", title: "Docs task" })) { return render( , ); } it("renders the resolved workflow name in the timestamp section instead of the title row", async () => { vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload); const { container } = renderDetail(); const badge = await screen.findByTestId("task-detail-workflow-badge"); expect(badge).toHaveTextContent("Docs"); expect(badge.closest(".detail-timestamps")).toBeTruthy(); expect(badge.closest(".detail-title-row")).toBeNull(); expect(container.querySelector(".detail-title-row .detail-workflow-badge")).toBeNull(); expect(screen.getAllByTestId("task-detail-workflow-badge")).toHaveLength(1); expect(screen.getByText("FN-101")).toBeInTheDocument(); expect(screen.getByText("Todo")).toBeInTheDocument(); expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalledTimes(1); }); it("uses the default workflow for tasks without explicit workflow assignment", async () => { vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload); renderDetail(makeTask({ id: "FN-default", column: "todo", title: "Default workflow task" })); expect(await screen.findByTestId("task-detail-workflow-badge")).toHaveTextContent("Coding"); }); it("hides the badge when an explicit workflow assignment cannot be resolved", async () => { vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce({ ...workflowPayload, taskWorkflowIds: { "FN-stale": "wf-deleted" }, }); renderDetail(makeTask({ id: "FN-stale", column: "todo", title: "Stale workflow task" })); await waitFor(() => expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalledTimes(1)); expect(screen.queryByTestId("task-detail-workflow-badge")).toBeNull(); }); it("updates the badge when a mounted task detail switches tasks", async () => { vi.mocked(dashboardApi.fetchBoardWorkflows) .mockResolvedValueOnce(workflowPayload) .mockResolvedValueOnce(workflowPayload); const props = { initialTab: "definition" as const, task: makeTask({ id: "FN-101", column: "todo", title: "Docs task" }), onMoveTask: noopMove, onDeleteTask: noopDelete, onMergeTask: noopMerge, onOpenDetail: noopOpenDetail, addToast: noop, }; const { rerender } = render(); expect(await screen.findByTestId("task-detail-workflow-badge")).toHaveTextContent("Docs"); rerender(); await waitFor(() => expect(screen.getByTestId("task-detail-workflow-badge")).toHaveTextContent("Coding")); }); it("clears the previous workflow badge while a mounted task switch reloads metadata", async () => { let resolveNextPayload: (payload: typeof workflowPayload) => void = () => undefined; vi.mocked(dashboardApi.fetchBoardWorkflows) .mockResolvedValueOnce(workflowPayload) .mockImplementationOnce(() => new Promise((resolve) => { resolveNextPayload = resolve; })); const props = { initialTab: "definition" as const, task: makeTask({ id: "FN-101", column: "todo", title: "Docs task" }), onMoveTask: noopMove, onDeleteTask: noopDelete, onMergeTask: noopMerge, onOpenDetail: noopOpenDetail, addToast: noop, }; const { rerender } = render(); expect(await screen.findByTestId("task-detail-workflow-badge")).toHaveTextContent("Docs"); rerender(); await waitFor(() => expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalledTimes(2)); expect(screen.queryByTestId("task-detail-workflow-badge")).toBeNull(); await act(async () => { resolveNextPayload(workflowPayload); }); expect(await screen.findByTestId("task-detail-workflow-badge")).toHaveTextContent("Coding"); }); it("hides the badge without workflow metadata and leaves no empty shell", async () => { vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce({ flagEnabled: false, defaultWorkflowId: "", workflows: [], taskWorkflowIds: {}, }); const { container } = renderDetail(); await waitFor(() => expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalledTimes(1)); expect(screen.queryByTestId("task-detail-workflow-badge")).toBeNull(); expect(container.querySelector(".detail-workflow-badge")).toBeNull(); }); it("renders the canonical badge beside the Updated timestamp in the mobile back-header variant", async () => { vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload); const { container } = render( , ); const badge = await screen.findByTestId("task-detail-workflow-badge"); const timestamps = container.querySelector(".detail-timestamps"); const updatedLabel = screen.getByText("Updated").closest(".detail-timestamp-item"); expect(badge).toHaveTextContent("Docs"); expect(badge.parentElement).toBe(timestamps); expect(updatedLabel?.nextElementSibling).toBe(badge); expect(screen.getAllByTestId("task-detail-workflow-badge")).toHaveLength(1); expect(screen.queryByTestId("task-detail-workflow-badge-mobile")).toBeNull(); expect(container.querySelector(".detail-title-row .detail-workflow-badge")).toBeNull(); expect(screen.getByRole("button", { name: "Back to task list" })).toBeInTheDocument(); }); }); it("renders clickable file links in markdown inline code while preserving code wrappers", async () => { const openFile = vi.fn(); render( , ); const fileLinks = screen.getAllByRole("button", { name: "packages/dashboard/app/App.tsx:12" }); expect(fileLinks.length).toBeGreaterThan(0); const code = fileLinks[0]?.closest("code"); expect(code).toBeTruthy(); expect(code?.querySelector("button.file-path-link")).toBe(fileLinks[0]); await userEvent.click(fileLinks[0]!); expect(openFile).toHaveBeenCalledWith("packages/dashboard/app/App.tsx", { line: 12, col: undefined }); }); /* FNXC:Markdown 2026-06-23-03:30: The task DESCRIPTION (spec/prompt) + SUMMARY now share the markdown pipeline's rehype-raw -> rehype-sanitize chain, so embedded raw HTML renders as real elements (not literal text), HTML comments drop, ", "", "```mermaid", "graph TD; A-->B;", "```", ].join("\n"); const { container } = render( , ); // Raw
/ renders as a real disclosure element. const details = container.querySelector(".markdown-body details"); expect(details).not.toBeNull(); expect(details?.querySelector("summary")?.textContent).toBe("Disclosure title"); expect(details?.textContent).toContain("Hidden detail body."); // HTML comment is dropped, never shown as literal text. expect(container.textContent).not.toContain("secret comment"); //