FN-6848: refresh workflow dropdown counts on open
Refresh workflow dropdown counts from the latest board-workflows data whenever users open the selector. - Refactor Board and ListView workflow payload fetching into reusable refresh callbacks shared by mount, visibility, focus, SSE, and dropdown-open events. - Add a WorkflowSwitcher onOpen hook that fires only on closed-to-open click or keyboard transitions. - Cover Board, ListView, and WorkflowSwitcher refresh behavior with regression tests and document the refreshed dropdown counts. Files changed: docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/Board.tsx | 49 +++++++++++--------- packages/dashboard/app/components/ListView.tsx | 49 +++++++++++--------- .../dashboard/app/components/WorkflowSwitcher.tsx | 38 +++++++++++++--- .../app/components/__tests__/Board.test.tsx | 14 ++++++ .../app/components/__tests__/ListView.test.tsx | 39 ++++++++++++++++ .../components/__tests__/WorkflowSwitcher.test.tsx | 53 ++++++++++++++++++++++ 7 files changed, 195 insertions(+), 49 deletions(-) Fusion-Task-Id: FN-6848 Fusion-Task-Lineage: bb83cee9-4184-4094-89d6-f2cf2a4fbd46
This commit is contained in:
@@ -79,7 +79,7 @@ Features:
|
||||
- Task card header meta badges group priority, fast mode, agent-created provenance, and elapsed/created-time chips into one wrapping row; agent labels prefer `sourceMetadata.agentName` over raw agent IDs
|
||||
- Column ordering semantics: `todo` mirrors scheduler pickup order (priority descending, then oldest `createdAt`, then task ID); `triage`, `in-progress`, `in-review`, and `archived` remain priority-first with task-ID tie-breaks; `done` is ordered by most recent completion first (`columnMovedAt`, then `updatedAt`, then `createdAt` fallback)
|
||||
- On mobile, both default and workflow-mode boards fill the project viewport while the column strip remains the internal horizontal scroller with contained edge overscroll.
|
||||
- Board and List workflow switchers use a themed dropdown instead of a native select. The closed trigger shows the workflow name and chevron only; compact Todo / In Progress / Done counts derived from workflow column flags (excluding archived columns) appear while the dropdown is expanded, including on each workflow option. Built-in lanes with synthesized trait-less lifecycle columns fall back to canonical column ids (`todo`, `in-progress`, `done`, and `archived`) for those counts. Each option row also exposes an inline edit action, and a persistent **New workflow** footer stays visible below the scrollable option list. Those inline count badges intentionally use the same board column color tokens as cards: `--todo`, `--in-progress`, and `--done`.
|
||||
- Board and List workflow switchers use a themed dropdown instead of a native select. The closed trigger shows the workflow name and chevron only; compact Todo / In Progress / Done counts derived from workflow column flags (excluding archived columns) refresh each time the dropdown opens and appear while the dropdown is expanded, including on each workflow option. Built-in lanes with synthesized trait-less lifecycle columns fall back to canonical column ids (`todo`, `in-progress`, `done`, and `archived`) for those counts. Each option row also exposes an inline edit action, and a persistent **New workflow** footer stays visible below the scrollable option list. Those inline count badges intentionally use the same board column color tokens as cards: `--todo`, `--in-progress`, and `--done`.
|
||||
- When workflow columns are enabled, Board and List hydrate the last successful workflow-lane payload from a per-project session cache; cold loads show a neutral skeleton until settings and workflow metadata are known, avoiding a legacy single-lane flash.
|
||||
|
||||

|
||||
|
||||
@@ -390,34 +390,40 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
||||
setBoardWorkflowsState(cached ? { projectId, payload: cached } : null);
|
||||
}, [projectId, shouldHydrateBoardWorkflowsCache]);
|
||||
|
||||
/*
|
||||
FNXC:WorkflowControls 2026-06-21-00:00:
|
||||
Opening the workflow switcher must refresh the board-workflows payload because task workflow assignment changes do not emit workflow definition SSE events.
|
||||
Share this path with mount, visibility/focus, and workflow-definition SSE refetches so the stale-response guard and cache writes remain identical.
|
||||
*/
|
||||
const refreshBoardWorkflows = useCallback(() => {
|
||||
const seq = ++boardWorkflowsFetchSeqRef.current;
|
||||
fetchBoardWorkflows(projectId)
|
||||
.then((payload) => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload });
|
||||
writeBoardWorkflowsCache(projectId, payload);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload: { flagEnabled: false, defaultWorkflowId: "builtin:coding", workflows: [], taskWorkflowIds: {} } });
|
||||
}
|
||||
});
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
const runFetch = () => {
|
||||
const seq = ++boardWorkflowsFetchSeqRef.current;
|
||||
fetchBoardWorkflows(projectId)
|
||||
.then((payload) => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload });
|
||||
writeBoardWorkflowsCache(projectId, payload);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload: { flagEnabled: false, defaultWorkflowId: "builtin:coding", workflows: [], taskWorkflowIds: {} } });
|
||||
}
|
||||
});
|
||||
};
|
||||
runFetch();
|
||||
refreshBoardWorkflows();
|
||||
const onVisible = () => {
|
||||
if (typeof document === "undefined" || document.visibilityState === "visible") runFetch();
|
||||
if (typeof document === "undefined" || document.visibilityState === "visible") refreshBoardWorkflows();
|
||||
};
|
||||
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
|
||||
if (typeof window !== "undefined") window.addEventListener("focus", onVisible);
|
||||
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
||||
const unsubscribe = subscribeSse(`/api/events${query}`, {
|
||||
events: {
|
||||
"workflow:created": runFetch,
|
||||
"workflow:updated": runFetch,
|
||||
"workflow:deleted": runFetch,
|
||||
"workflow:created": refreshBoardWorkflows,
|
||||
"workflow:updated": refreshBoardWorkflows,
|
||||
"workflow:deleted": refreshBoardWorkflows,
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
@@ -427,7 +433,7 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
||||
if (typeof window !== "undefined") window.removeEventListener("focus", onVisible);
|
||||
unsubscribe();
|
||||
};
|
||||
}, [projectId]);
|
||||
}, [projectId, refreshBoardWorkflows]);
|
||||
|
||||
const handlePromote = useCallback(async (taskId: string) => {
|
||||
await promoteTask(taskId, projectId);
|
||||
@@ -576,6 +582,7 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
||||
value={selectedWorkflow.id}
|
||||
onChange={setSelectedWorkflowId}
|
||||
counts={workflowStatusCounts}
|
||||
onOpen={refreshBoardWorkflows}
|
||||
onEditWorkflow={onOpenWorkflowEditor}
|
||||
onCreateWorkflow={onCreateWorkflow}
|
||||
/>
|
||||
|
||||
@@ -430,34 +430,40 @@ export function ListView({
|
||||
setBoardWorkflowsState(cached ? { projectId, payload: cached } : null);
|
||||
}, [projectId, shouldHydrateBoardWorkflowsCache]);
|
||||
|
||||
/*
|
||||
FNXC:WorkflowControls 2026-06-21-00:00:
|
||||
Opening the workflow switcher must refresh the board-workflows payload because task workflow assignment changes do not emit workflow definition SSE events.
|
||||
Share this path with mount, visibility/focus, and workflow-definition SSE refetches so desktop sidebar and mobile toolbar counts cannot drift.
|
||||
*/
|
||||
const refreshBoardWorkflows = useCallback(() => {
|
||||
const seq = ++boardWorkflowsFetchSeqRef.current;
|
||||
fetchBoardWorkflows(projectId)
|
||||
.then((payload) => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload });
|
||||
writeBoardWorkflowsCache(projectId, payload);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload: { flagEnabled: false, defaultWorkflowId: "builtin:coding", workflows: [], taskWorkflowIds: {} } });
|
||||
}
|
||||
});
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
const runFetch = () => {
|
||||
const seq = ++boardWorkflowsFetchSeqRef.current;
|
||||
fetchBoardWorkflows(projectId)
|
||||
.then((payload) => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload });
|
||||
writeBoardWorkflowsCache(projectId, payload);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (seq === boardWorkflowsFetchSeqRef.current) {
|
||||
setBoardWorkflowsState({ projectId, payload: { flagEnabled: false, defaultWorkflowId: "builtin:coding", workflows: [], taskWorkflowIds: {} } });
|
||||
}
|
||||
});
|
||||
};
|
||||
runFetch();
|
||||
refreshBoardWorkflows();
|
||||
const onVisible = () => {
|
||||
if (typeof document === "undefined" || document.visibilityState === "visible") runFetch();
|
||||
if (typeof document === "undefined" || document.visibilityState === "visible") refreshBoardWorkflows();
|
||||
};
|
||||
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
|
||||
if (typeof window !== "undefined") window.addEventListener("focus", onVisible);
|
||||
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
||||
const unsubscribe = subscribeSse(`/api/events${query}`, {
|
||||
events: {
|
||||
"workflow:created": runFetch,
|
||||
"workflow:updated": runFetch,
|
||||
"workflow:deleted": runFetch,
|
||||
"workflow:created": refreshBoardWorkflows,
|
||||
"workflow:updated": refreshBoardWorkflows,
|
||||
"workflow:deleted": refreshBoardWorkflows,
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
@@ -466,7 +472,7 @@ export function ListView({
|
||||
if (typeof window !== "undefined") window.removeEventListener("focus", onVisible);
|
||||
unsubscribe();
|
||||
};
|
||||
}, [projectId]);
|
||||
}, [projectId, refreshBoardWorkflows]);
|
||||
|
||||
// Persist selection to localStorage
|
||||
useEffect(() => {
|
||||
@@ -1630,6 +1636,7 @@ export function ListView({
|
||||
value={selectedWorkflow.id}
|
||||
onChange={setSelectedWorkflowId}
|
||||
counts={workflowStatusCounts}
|
||||
onOpen={refreshBoardWorkflows}
|
||||
label={t("listView.workflowLabel", "Workflow")}
|
||||
onEditWorkflow={onOpenWorkflowEditor}
|
||||
onCreateWorkflow={onCreateWorkflow}
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface WorkflowSwitcherProps {
|
||||
value: string;
|
||||
onChange: (id: string) => void;
|
||||
counts: Map<string, WorkflowStatusCounts>;
|
||||
/** Fired each time the dropdown transitions from closed to open so consumers can refresh count data. */
|
||||
onOpen?: () => void;
|
||||
label?: string;
|
||||
onEditWorkflow?: (workflowId: string) => void;
|
||||
onCreateWorkflow?: () => void;
|
||||
@@ -42,8 +44,12 @@ function getCounts(counts: Map<string, WorkflowStatusCounts>, workflowId: string
|
||||
* FNXC:WorkflowSwitcher 2026-06-20-15:34:
|
||||
* Workflow edit and creation affordances moved into the shared dropdown so Board and ListView cannot leave separate toolbar icon shells behind.
|
||||
* Each option row owns a sibling edit button, and New workflow remains visible in a non-scrolling footer while long workflow lists scroll.
|
||||
*
|
||||
* FNXC:WorkflowSwitcher 2026-06-21-00:00:
|
||||
* Opening the dropdown must refresh workflow count data because task-to-workflow assignments do not emit board-workflows invalidation events.
|
||||
* Fire onOpen only on closed-to-open transitions so consumers can refetch without close-time calls or render loops.
|
||||
*/
|
||||
export function WorkflowSwitcher({ workflows, value, onChange, counts, label: labelProp, onEditWorkflow, onCreateWorkflow }: WorkflowSwitcherProps) {
|
||||
export function WorkflowSwitcher({ workflows, value, onChange, counts, onOpen, label: labelProp, onEditWorkflow, onCreateWorkflow }: WorkflowSwitcherProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const label = labelProp ?? t("workflowSwitcher.label", "Workflow");
|
||||
const todoLabel = t("workflowSwitcher.todo", "Todo");
|
||||
@@ -62,6 +68,7 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const onOpenRef = useRef(onOpen);
|
||||
|
||||
const selectedIndex = useMemo(() => Math.max(0, workflows.findIndex((workflow) => workflow.id === value)), [value, workflows]);
|
||||
const selectedWorkflow = workflows[selectedIndex] ?? workflows[0] ?? null;
|
||||
@@ -96,6 +103,10 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
|
||||
setDropdownPosition({ top, left, width, maxHeight });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onOpenRef.current = onOpen;
|
||||
}, [onOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setPortalRoot(document.body);
|
||||
}, []);
|
||||
@@ -141,6 +152,21 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
|
||||
}
|
||||
}, [highlightedIndex, isOpen]);
|
||||
|
||||
const openDropdown = useCallback(() => {
|
||||
if (isOpen) return;
|
||||
onOpenRef.current?.();
|
||||
setIsOpen(true);
|
||||
}, [isOpen]);
|
||||
|
||||
const toggleDropdown = useCallback(() => {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
onOpenRef.current?.();
|
||||
setIsOpen(true);
|
||||
}, [isOpen]);
|
||||
|
||||
const selectWorkflow = useCallback((workflowId: string) => {
|
||||
onChange(workflowId);
|
||||
setIsOpen(false);
|
||||
@@ -164,7 +190,7 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
openDropdown();
|
||||
} else {
|
||||
setHighlightedIndex((current) => (workflows.length ? (current + 1) % workflows.length : 0));
|
||||
}
|
||||
@@ -172,7 +198,7 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
openDropdown();
|
||||
} else {
|
||||
setHighlightedIndex((current) => (workflows.length ? (current - 1 + workflows.length) % workflows.length : 0));
|
||||
}
|
||||
@@ -184,7 +210,7 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
|
||||
const workflow = workflows[highlightedIndex];
|
||||
if (workflow) selectWorkflow(workflow.id);
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
openDropdown();
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
@@ -195,7 +221,7 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
|
||||
setIsOpen(false);
|
||||
break;
|
||||
}
|
||||
}, [highlightedIndex, isOpen, selectWorkflow, workflows]);
|
||||
}, [highlightedIndex, isOpen, openDropdown, selectWorkflow, workflows]);
|
||||
|
||||
if (!selectedWorkflow) return null;
|
||||
|
||||
@@ -310,7 +336,7 @@ export function WorkflowSwitcher({ workflows, value, onChange, counts, label: la
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={isOpen ? listboxId : undefined}
|
||||
aria-label={t("workflowSwitcher.triggerAria", "Select workflow. Current workflow: {{name}}", { name: selectedWorkflow.name })}
|
||||
onClick={() => setIsOpen((open) => !open)}
|
||||
onClick={toggleDropdown}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<span className="workflow-switcher-trigger-main">
|
||||
|
||||
@@ -1300,6 +1300,20 @@ describe("Board", () => {
|
||||
expect(screen.getByTestId("column-done").getAttribute("data-has-archive-all")).toBe("yes");
|
||||
expect(screen.getByTestId("column-todo").getAttribute("data-has-archive-all")).toBe("no");
|
||||
});
|
||||
|
||||
it("re-fetches board-workflows when the workflow switcher opens", async () => {
|
||||
enableFlag({ "FN-1": "builtin:coding" }, [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW]);
|
||||
renderBoard({ projectId: "proj-1", tasks: [mkTask({ id: "FN-1", column: "todo" })] });
|
||||
|
||||
const trigger = await screen.findByTestId("workflow-switcher");
|
||||
await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledTimes(1));
|
||||
fetchBoardWorkflowsMock.mockClear();
|
||||
|
||||
fireEvent.click(trigger);
|
||||
|
||||
expect(fetchBoardWorkflowsMock).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole("listbox", { name: "Workflow" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow:updated SSE invalidation (#1406)", () => {
|
||||
|
||||
@@ -658,6 +658,45 @@ describe("ListView", () => {
|
||||
expect(screen.queryAllByText("Backlog")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("re-fetches board-workflows when the workflow switcher opens", async () => {
|
||||
vi.mocked(fetchBoardWorkflows).mockResolvedValue({
|
||||
flagEnabled: true,
|
||||
defaultWorkflowId: "builtin:coding",
|
||||
workflows: [
|
||||
{
|
||||
id: "builtin:coding",
|
||||
name: "Coding",
|
||||
columns: [
|
||||
{ id: "todo", name: "Todo", flags: { hold: true } },
|
||||
{ id: "done", name: "Done", flags: { complete: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "wf-custom",
|
||||
name: "Custom Flow",
|
||||
columns: [
|
||||
{ id: "backlog", name: "Backlog", flags: { intake: true } },
|
||||
{ id: "done", name: "Done", flags: { complete: true } },
|
||||
],
|
||||
},
|
||||
],
|
||||
taskWorkflowIds: { "FN-001": "builtin:coding" },
|
||||
});
|
||||
|
||||
renderListView({
|
||||
tasks: [createMockTask({ id: "FN-001", column: "todo", title: "Workflow task" })],
|
||||
});
|
||||
|
||||
const trigger = await screen.findByTestId("workflow-switcher");
|
||||
await waitFor(() => expect(fetchBoardWorkflows).toHaveBeenCalledTimes(1));
|
||||
vi.mocked(fetchBoardWorkflows).mockClear();
|
||||
|
||||
fireEvent.click(trigger);
|
||||
|
||||
expect(fetchBoardWorkflows).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole("listbox", { name: "Workflow" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("re-homes a preserved-column task to the new workflow after workflow invalidation", async () => {
|
||||
const preservedWorkflow = {
|
||||
id: "wf-preserved",
|
||||
|
||||
@@ -58,6 +58,59 @@ describe("WorkflowSwitcher", () => {
|
||||
expect(screen.queryByRole("listbox", { name: "Workflow" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("fires onOpen only on click-driven closed-to-open transitions", () => {
|
||||
const onOpen = vi.fn();
|
||||
render(<WorkflowSwitcher workflows={workflows} value="coding" onChange={vi.fn()} counts={countMap()} onOpen={onOpen} />);
|
||||
|
||||
const trigger = screen.getByTestId("workflow-switcher");
|
||||
expect(onOpen).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(trigger);
|
||||
expect(onOpen).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole("listbox", { name: "Workflow" })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(trigger);
|
||||
expect(onOpen).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByRole("listbox", { name: "Workflow" })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(trigger);
|
||||
expect(onOpen).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it.each(["ArrowDown", "ArrowUp", "Enter", " "])("fires onOpen when %s opens the dropdown from the keyboard", (key) => {
|
||||
const onOpen = vi.fn();
|
||||
render(<WorkflowSwitcher workflows={workflows} value="coding" onChange={vi.fn()} counts={countMap()} onOpen={onOpen} />);
|
||||
|
||||
const trigger = screen.getByTestId("workflow-switcher");
|
||||
expect(onOpen).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyDown(trigger, { key });
|
||||
expect(onOpen).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole("listbox", { name: "Workflow" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not fire onOpen when Escape or outside mousedown closes and fires again after reopening", () => {
|
||||
const onOpen = vi.fn();
|
||||
render(<WorkflowSwitcher workflows={workflows} value="coding" onChange={vi.fn()} counts={countMap()} onOpen={onOpen} />);
|
||||
|
||||
const trigger = screen.getByTestId("workflow-switcher");
|
||||
fireEvent.click(trigger);
|
||||
expect(onOpen).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.keyDown(trigger, { key: "Escape" });
|
||||
expect(onOpen).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByRole("listbox", { name: "Workflow" })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(trigger);
|
||||
expect(onOpen).toHaveBeenCalledTimes(2);
|
||||
fireEvent.mouseDown(document.body);
|
||||
expect(onOpen).toHaveBeenCalledTimes(2);
|
||||
expect(screen.queryByRole("listbox", { name: "Workflow" })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.keyDown(trigger, { key: "ArrowDown" });
|
||||
expect(onOpen).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("calls onChange when an option is selected", () => {
|
||||
const onChange = vi.fn();
|
||||
render(<WorkflowSwitcher workflows={workflows} value="coding" onChange={onChange} counts={countMap()} />);
|
||||
|
||||
Reference in New Issue
Block a user