Files
fusion/packages/dashboard/app/__tests__/graph-workflow-header.test.tsx
gsxdsm d7f3c7093e FN-6941: add graph workflow filtering
Adds a Graph view header workflow dropdown that filters dependency graph tasks by the selected workflow.

- Portal the workflow switcher into the Graph view header and clear selection when unmounted.
- Scope plugin graph task context using workflow assignments and default workflow fallback.
- Cover the header integration, task filtering, portal lifecycle, and disabled workflow mode.
- Document Graph workflow filtering and add the published package changeset.

Files changed:
 .changeset/fn-6941-graph-workflow-dropdown.md      |   7 +
 docs/dashboard-guide.md                            |   1 +
 packages/dashboard/app/App.tsx                     |   4 +
 .../app/__tests__/graph-workflow-header.test.tsx   | 127 +++++++++++++++
 .../app/components/GraphWorkflowSwitcherSlot.tsx   | 110 +++++++++++++
 .../__tests__/GraphWorkflowSwitcherSlot.test.tsx   | 173 +++++++++++++++++++++
 .../app/components/dashboard/MainContent.tsx       |  23 ++-
 .../dashboard/app/components/dashboard/types.ts    |   3 +
 8 files changed, 446 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-6941

Fusion-Task-Lineage: a77bc99e-00fe-42fe-a976-d88ffd803388
2026-06-24 23:32:51 -07:00

128 lines
4.7 KiB
TypeScript

import { useState } from "react";
import { fireEvent, render, screen, waitFor, within } 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 "../components/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 REVIEW_WORKFLOW: BoardWorkflowDefinition = {
id: "wf-review",
name: "Review",
columns: [],
};
const TASKS = [
{ id: "FN-default", title: "Default task" },
{ id: "FN-unassigned", title: "Unassigned task" },
{ id: "FN-review", title: "Review task" },
{ id: "FN-unknown", title: "Unknown task" },
];
function workflowPayload(overrides: Partial<BoardWorkflowsPayload> = {}): BoardWorkflowsPayload {
return {
flagEnabled: true,
defaultWorkflowId: DEFAULT_WORKFLOW.id,
workflows: [DEFAULT_WORKFLOW, REVIEW_WORKFLOW],
taskWorkflowIds: {
"FN-review": REVIEW_WORKFLOW.id,
"FN-unknown": "wf-missing",
},
...overrides,
};
}
function GraphPluginContextHarness({ projectId = "project-graph" }: { projectId?: string }) {
const [selection, setSelection] = useState<GraphWorkflowSelection | null>(null);
const pluginTasks = filterTasksByGraphWorkflowSelection(TASKS, projectId, selection);
return (
<>
<div id="header-workflow-slot" data-testid="header-workflow-slot" />
<GraphWorkflowSwitcherSlot projectId={projectId} onWorkflowSelectionChange={setSelection} />
<ul data-testid="graph-plugin-context-tasks">
{pluginTasks.map((task) => (
<li key={task.id} data-testid={`graph-context-task-${task.id}`}>{task.title}</li>
))}
</ul>
</>
);
}
beforeEach(() => {
sessionStorage.clear();
fetchBoardWorkflowsMock.mockReset();
subscribeSseMock.mockClear();
fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload());
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Graph workflow header integration", () => {
it("ports the dropdown into the header and scopes graph plugin tasks by selected workflow", async () => {
render(<GraphPluginContextHarness />);
const headerSlot = screen.getByTestId("header-workflow-slot");
const selector = await screen.findByTestId("workflow-switcher");
expect(headerSlot.contains(selector)).toBe(true);
const contextTasks = screen.getByTestId("graph-plugin-context-tasks");
await waitFor(() => {
expect(within(contextTasks).getByTestId("graph-context-task-FN-default")).toBeInTheDocument();
expect(within(contextTasks).getByTestId("graph-context-task-FN-unassigned")).toBeInTheDocument();
expect(within(contextTasks).queryByTestId("graph-context-task-FN-review")).toBeNull();
expect(within(contextTasks).queryByTestId("graph-context-task-FN-unknown")).toBeNull();
});
fireEvent.click(selector);
fireEvent.click(screen.getByTestId("workflow-switcher-option-wf-review"));
await waitFor(() => {
expect(within(contextTasks).getByTestId("graph-context-task-FN-review")).toBeInTheDocument();
expect(within(contextTasks).queryByTestId("graph-context-task-FN-default")).toBeNull();
expect(within(contextTasks).queryByTestId("graph-context-task-FN-unassigned")).toBeNull();
expect(within(contextTasks).queryByTestId("graph-context-task-FN-unknown")).toBeNull();
});
});
it("keeps graph plugin tasks unfiltered when workflow mode is disabled or no project is selected", async () => {
fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload({ flagEnabled: false, workflows: [] }));
const { rerender } = render(<GraphPluginContextHarness />);
await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledWith("project-graph"));
expect(screen.queryByTestId("workflow-switcher")).toBeNull();
for (const task of TASKS) {
expect(screen.getByTestId(`graph-context-task-${task.id}`)).toBeInTheDocument();
}
fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload());
rerender(<GraphPluginContextHarness projectId="" />);
await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledWith(""));
for (const task of TASKS) {
expect(screen.getByTestId(`graph-context-task-${task.id}`)).toBeInTheDocument();
}
});
});