diff --git a/packages/dashboard/app/components/Column.tsx b/packages/dashboard/app/components/Column.tsx index d3d5afb43a..81070b309b 100644 --- a/packages/dashboard/app/components/Column.tsx +++ b/packages/dashboard/app/components/Column.tsx @@ -732,44 +732,32 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, ) : ( <> {visibleTasks.map((task) => ( -
- - {isHoldColumn && onPromote && ( - - )} -
+ ))} {shouldPaginate && hiddenTaskCount > 0 && ( )} + {onPromote && ( + + )} {showInReviewMoveControl && !metaRowVisible && renderInReviewMoveControl()} )} diff --git a/packages/dashboard/app/components/__tests__/Column.test.tsx b/packages/dashboard/app/components/__tests__/Column.test.tsx index aa3d7db572..a35a560086 100644 --- a/packages/dashboard/app/components/__tests__/Column.test.tsx +++ b/packages/dashboard/app/components/__tests__/Column.test.tsx @@ -9,9 +9,17 @@ import type { Task, Column as ColumnType } from "@fusion/core"; const taskCardRenderSpy = vi.fn(); vi.mock("../TaskCard", () => ({ - TaskCard: React.memo(({ task, workflowStepNameLookup }: { task: Task; workflowStepNameLookup?: ReadonlyMap }) => { + TaskCard: React.memo(({ task, workflowStepNameLookup, onPromote, isPromoting }: { task: Task; workflowStepNameLookup?: ReadonlyMap; onPromote?: (taskId: string) => Promise; isPromoting?: boolean }) => { taskCardRenderSpy(task.id); - return
; + return ( +
+ {onPromote && ( + + )} +
+ ); }), })); vi.mock("../WorktreeGroup", () => ({ @@ -192,7 +200,7 @@ describe("Column workflow mode (U9)", () => { tasks={[{ ...makeTask("FN-7"), column: "hold-col" as ColumnType }]} />, ); - expect(screen.getByTestId("promote-FN-7")).toBeDefined(); + expect(screen.getByTestId("card-promote-FN-7")).toBeDefined(); }); it("#1410: clears the inline capacity banner when the task list changes via SSE", async () => { @@ -213,7 +221,7 @@ describe("Column workflow mode (U9)", () => { ); // Trigger a capacity-exhausted promote → inline banner appears. - fireEvent.click(screen.getByTestId("promote-FN-7")); + fireEvent.click(screen.getByTestId("card-promote-FN-7")); await waitFor(() => expect(screen.getByTestId("column-inline-feedback")).toBeDefined()); expect(screen.getByTestId("column-inline-feedback").textContent).toContain("capacity"); diff --git a/packages/dashboard/app/components/__tests__/Lane.test.tsx b/packages/dashboard/app/components/__tests__/Lane.test.tsx index cf9601b7e9..5706d662c6 100644 --- a/packages/dashboard/app/components/__tests__/Lane.test.tsx +++ b/packages/dashboard/app/components/__tests__/Lane.test.tsx @@ -8,7 +8,15 @@ import type { BoardWorkflowDefinition } from "../../api"; // Keep the test focused on Lane + Column (real) — mock the leaf TaskCard and // the confirm hook, matching the Column test harness. vi.mock("../TaskCard", () => ({ - TaskCard: ({ task }: { task: Task }) =>
, + TaskCard: ({ task, onPromote, isPromoting }: { task: Task; onPromote?: (taskId: string) => Promise; isPromoting?: boolean }) => ( +
+ {onPromote && ( + + )} +
+ ), })); vi.mock("../WorktreeGroup", () => ({ WorktreeGroup: () =>
})); vi.mock("../QuickEntryBox", () => ({ QuickEntryBox: () =>
})); @@ -122,7 +130,7 @@ describe("Lane", () => { it("shows a Promote button on hold-column cards and calls onPromote", async () => { const props = baseProps(); render(); - const promoteBtn = screen.getByTestId("promote-FN-7"); + const promoteBtn = screen.getByTestId("card-promote-FN-7"); expect(promoteBtn).toBeDefined(); fireEvent.click(promoteBtn); await waitFor(() => expect(props.onPromote).toHaveBeenCalledWith("FN-7")); @@ -134,12 +142,12 @@ describe("Lane", () => { details: { code: "capacity-exhausted", messageKey: "board.rejection.capacityExhausted", retryable: true }, }); render(); - fireEvent.click(screen.getByTestId("promote-FN-8")); + fireEvent.click(screen.getByTestId("card-promote-FN-8")); await waitFor(() => expect(screen.getByTestId("column-inline-feedback")).toBeDefined()); // No toast was used for the inline capacity feedback. expect(props.addToast).not.toHaveBeenCalled(); // Button re-enabled after the call resolves. - await waitFor(() => expect((screen.getByTestId("promote-FN-8") as HTMLButtonElement).disabled).toBe(false)); + await waitFor(() => expect((screen.getByTestId("card-promote-FN-8") as HTMLButtonElement).disabled).toBe(false)); }); it("prevents the drop (no-move) when canDropTask returns a rejection key", () => { diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index bf658d3d1f..dafe500367 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -24,6 +24,7 @@ vi.mock("lucide-react", () => ({ RotateCw: () => null, Zap: () => , AlertTriangle: () => null, + ArrowUpRight: () => null, })); vi.mock("../ProviderIcon", () => ({ @@ -4336,6 +4337,75 @@ describe("TaskCard mission badge", () => { }); }); + it("renders a promote action when onPromote is provided", () => { + const onPromote = vi.fn().mockResolvedValue(undefined); + + render( + , + ); + + const promoteButton = screen.getByTestId("card-promote-FN-777"); + expect(promoteButton).toBeDefined(); + expect(promoteButton.textContent).toContain("Promote"); + }); + + it("calls onPromote without opening the card when promote is clicked", () => { + const onPromote = vi.fn().mockResolvedValue(undefined); + const onOpenDetail = vi.fn(); + + render( + , + ); + + fireEvent.click(screen.getByTestId("card-promote-FN-778")); + + expect(onPromote).toHaveBeenCalledWith("FN-778"); + expect(onOpenDetail).not.toHaveBeenCalled(); + }); + + it("disables the promote action and shows loading copy while promoting", () => { + const onPromote = vi.fn().mockResolvedValue(undefined); + + render( + , + ); + + const promoteButton = screen.getByTestId("card-promote-FN-779") as HTMLButtonElement; + expect(promoteButton.disabled).toBe(true); + expect(promoteButton.textContent).toContain("Promoting…"); + + fireEvent.click(promoteButton); + expect(onPromote).not.toHaveBeenCalled(); + }); + + it("does not render a promote action when onPromote is omitted", () => { + render( + , + ); + + expect(screen.queryByTestId("card-promote-FN-780")).toBeNull(); + }); + it("shows mission title in title attribute", async () => { vi.mocked(fetchMission).mockResolvedValue({ id: "M-TITLE",