import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { BoardWorkflowDefinition, BoardWorkflowsPayload } from "../../api"; import { filterTasksByGraphWorkflowSelection, GraphWorkflowSwitcherSlot, type GraphWorkflowSelection } from "../GraphWorkflowSwitcherSlot"; const fetchBoardWorkflowsMock = vi.fn(); const subscribeSseMock = vi.fn(() => vi.fn()); vi.mock("../../api", () => ({ fetchBoardWorkflows: (...args: unknown[]) => fetchBoardWorkflowsMock(...args), })); vi.mock("../../sse-bus", () => ({ subscribeSse: (...args: unknown[]) => subscribeSseMock(...args), })); const DEFAULT_WORKFLOW: BoardWorkflowDefinition = { id: "builtin:coding", name: "Coding", columns: [], }; const CUSTOM_WORKFLOW: BoardWorkflowDefinition = { id: "wf-review", name: "Review", columns: [], }; function workflowPayload(overrides: Partial = {}): BoardWorkflowsPayload { return { flagEnabled: true, defaultWorkflowId: DEFAULT_WORKFLOW.id, workflows: [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW], taskWorkflowIds: {}, ...overrides, }; } function appendHeaderWorkflowSlot() { const slot = document.createElement("div"); slot.id = "header-workflow-slot"; slot.className = "header-workflow-slot"; document.body.appendChild(slot); return slot; } beforeEach(() => { sessionStorage.clear(); fetchBoardWorkflowsMock.mockReset(); subscribeSseMock.mockClear(); fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload()); vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null); }); afterEach(() => { document.getElementById("header-workflow-slot")?.remove(); vi.restoreAllMocks(); }); describe("filterTasksByGraphWorkflowSelection", () => { it("uses task workflow assignments with default fallback for graph scoping", () => { const tasks = [ { id: "FN-default" }, { id: "FN-unassigned" }, { id: "FN-review" }, { id: "FN-unknown" }, ]; const selection: GraphWorkflowSelection = { boardWorkflows: workflowPayload({ taskWorkflowIds: { "FN-review": CUSTOM_WORKFLOW.id, "FN-unknown": "wf-missing", }, }), selectedWorkflow: DEFAULT_WORKFLOW, }; expect(filterTasksByGraphWorkflowSelection(tasks, "project-graph", selection).map((task) => task.id)).toEqual([ "FN-default", "FN-unassigned", "FN-unknown", ]); expect(filterTasksByGraphWorkflowSelection(tasks, "project-graph", { ...selection, selectedWorkflow: CUSTOM_WORKFLOW }).map((task) => task.id)).toEqual([ "FN-review", ]); }); it("preserves unfiltered graph tasks without a project or workflow payload", () => { const tasks = [{ id: "FN-a" }, { id: "FN-b" }]; const selection: GraphWorkflowSelection = { boardWorkflows: workflowPayload({ taskWorkflowIds: { "FN-b": CUSTOM_WORKFLOW.id } }), selectedWorkflow: CUSTOM_WORKFLOW, }; expect(filterTasksByGraphWorkflowSelection(tasks, undefined, selection)).toBe(tasks); expect(filterTasksByGraphWorkflowSelection(tasks, "project-graph", null)).toBe(tasks); }); }); describe("GraphWorkflowSwitcherSlot", () => { it("portals the shared workflow switcher into the header workflow slot", async () => { const headerSlot = appendHeaderWorkflowSlot(); const onWorkflowSelectionChange = vi.fn(); render(); const selector = await screen.findByTestId("workflow-switcher"); expect(headerSlot.contains(selector)).toBe(true); expect(headerSlot.querySelector(".board-workflow-toolbar")).not.toBeNull(); await waitFor(() => { expect(onWorkflowSelectionChange).toHaveBeenLastCalledWith({ boardWorkflows: workflowPayload(), selectedWorkflow: DEFAULT_WORKFLOW, }); }); }); it("refreshes the board-workflows payload when the dropdown opens", async () => { appendHeaderWorkflowSlot(); render(); const selector = await screen.findByTestId("workflow-switcher"); await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledTimes(1)); fireEvent.click(selector); await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledTimes(2)); expect(screen.getByRole("listbox", { name: "Workflow" })).toBeInTheDocument(); }); it("reports selection changes so App can scope graph tasks", async () => { appendHeaderWorkflowSlot(); const onWorkflowSelectionChange = vi.fn(); render(); fireEvent.click(await screen.findByTestId("workflow-switcher")); fireEvent.click(screen.getByTestId("workflow-switcher-option-wf-review")); await waitFor(() => { const lastSelection = onWorkflowSelectionChange.mock.calls.at(-1)?.[0] as GraphWorkflowSelection | null; expect(lastSelection?.selectedWorkflow.id).toBe("wf-review"); }); }); it("forwards dropdown edit workflow ids to the graph editor launcher", async () => { appendHeaderWorkflowSlot(); const onOpenWorkflowEditor = vi.fn(); render(); fireEvent.click(await screen.findByTestId("workflow-switcher")); fireEvent.click(screen.getByTestId("workflow-switcher-edit-wf-review")); expect(onOpenWorkflowEditor).toHaveBeenCalledWith("wf-review"); }); it("renders no dropdown shell when the header slot is absent", async () => { render(); await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalled()); expect(screen.queryByTestId("workflow-switcher")).toBeNull(); expect(document.querySelector(".board-workflow-toolbar")).toBeNull(); }); it("renders no dropdown shell when workflow mode is disabled, empty, or not switchable", async () => { const headerSlot = appendHeaderWorkflowSlot(); const { rerender } = render(); fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload({ flagEnabled: false, workflows: [] })); rerender(); await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledWith("project-disabled-next")); expect(screen.queryByTestId("workflow-switcher")).toBeNull(); expect(headerSlot.childElementCount).toBe(0); fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload({ workflows: [] })); rerender(); await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledWith("project-empty")); expect(screen.queryByTestId("workflow-switcher")).toBeNull(); expect(headerSlot.childElementCount).toBe(0); fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload({ workflows: [DEFAULT_WORKFLOW] })); rerender(); await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledWith("project-single")); expect(screen.queryByTestId("workflow-switcher")).toBeNull(); expect(headerSlot.childElementCount).toBe(0); }); });