FN-7839: group done-card Archive and Revert actions into one dropdown
Replaces the standalone Archive button on done task cards with an Actions dropdown that groups Archive and Revert, mirroring the existing in-progress "Send back" menu pattern. - Add a card-done-actions dropdown (Actions trigger + menu) rendered for done cards, reusing card-send-back* styling - Move Archive into the dropdown menu; add Revert as a menu item when the task is revertable (isRevertable) - Only render the dropdown trigger when at least one action (archive/revert) is available, avoiding an empty button shell - Restrict the old inline card-revert-btn to archived cards only (done cards now use the dropdown) - Add outside-click handling and aria-haspopup/aria-expanded wiring for the new menu - Add i18n key tasks.doneActions - Update TaskCard tests to cover the dropdown (archive/revert menu items, empty-state omission) and mock useToast in board-mobile tests for isolated TaskCard renders - Add changeset for @runfusion/fusion (patch) Files changed: .changeset/fn-7839-done-card-actions-dropdown.md | 7 ++ packages/dashboard/app/components/TaskCard.tsx | 88 ++++++++++++++++++---- .../app/components/__tests__/TaskCard.test.tsx | 82 +++++++++++++++----- .../app/components/__tests__/board-mobile.test.tsx | 15 +++- 4 files changed, 157 insertions(+), 35 deletions(-) Fusion-Task-Id: FN-7839 Fusion-Task-Lineage: 1f8137cb-22b1-43dd-beaa-1bee0387d44b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7839-done-card-actions-dropdown.md
Normal file
7
.changeset/fn-7839-done-card-actions-dropdown.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Done task cards group Archive and Revert into one dropdown.
|
||||||
|
category: feature
|
||||||
|
dev: Reuses the in-progress "Send back" .card-send-back* dropdown pattern in TaskCard; new i18n key tasks.doneActions.
|
||||||
@@ -906,6 +906,7 @@ function TaskCardComponent({
|
|||||||
const [missionTitle, setMissionTitle] = useState<string | null>(null);
|
const [missionTitle, setMissionTitle] = useState<string | null>(null);
|
||||||
const [agentName, setAgentName] = useState<string | null>(null);
|
const [agentName, setAgentName] = useState<string | null>(null);
|
||||||
const [showSendBackMenu, setShowSendBackMenu] = useState(false);
|
const [showSendBackMenu, setShowSendBackMenu] = useState(false);
|
||||||
|
const [showDoneActionsMenu, setShowDoneActionsMenu] = useState(false);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
|
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
|
||||||
const [isRetrying, setIsRetrying] = useState(false);
|
const [isRetrying, setIsRetrying] = useState(false);
|
||||||
const [isPrCreateOpen, setIsPrCreateOpen] = useState(false);
|
const [isPrCreateOpen, setIsPrCreateOpen] = useState(false);
|
||||||
@@ -925,6 +926,7 @@ function TaskCardComponent({
|
|||||||
*/
|
*/
|
||||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const sendBackRef = useRef<HTMLDivElement>(null);
|
const sendBackRef = useRef<HTMLDivElement>(null);
|
||||||
|
const doneActionsRef = useRef<HTMLDivElement>(null);
|
||||||
const [isInViewport, setIsInViewport] = useState(false);
|
const [isInViewport, setIsInViewport] = useState(false);
|
||||||
const { badgeUpdates, subscribeToBadge, unsubscribeFromBadge } = useBadgeWebSocket(projectId);
|
const { badgeUpdates, subscribeToBadge, unsubscribeFromBadge } = useBadgeWebSocket(projectId);
|
||||||
const { agentsMap } = useAgentsMapCache(projectId);
|
const { agentsMap } = useAgentsMapCache(projectId);
|
||||||
@@ -969,6 +971,18 @@ function TaskCardComponent({
|
|||||||
return () => document.removeEventListener("click", handleClick);
|
return () => document.removeEventListener("click", handleClick);
|
||||||
}, [showSendBackMenu]);
|
}, [showSendBackMenu]);
|
||||||
|
|
||||||
|
// Close done-actions menu on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showDoneActionsMenu) return;
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (doneActionsRef.current && !doneActionsRef.current.contains(e.target as Node)) {
|
||||||
|
setShowDoneActionsMenu(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("click", handleClick);
|
||||||
|
return () => document.removeEventListener("click", handleClick);
|
||||||
|
}, [showDoneActionsMenu]);
|
||||||
|
|
||||||
// Fetch mission title when missionId is set
|
// Fetch mission title when missionId is set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!task.missionId) {
|
if (!task.missionId) {
|
||||||
@@ -2410,6 +2424,7 @@ function TaskCardComponent({
|
|||||||
const openContextMenuAt = useCallback((clientX: number, clientY: number) => {
|
const openContextMenuAt = useCallback((clientX: number, clientY: number) => {
|
||||||
if (!hasContextMenuActions || isEditing) return;
|
if (!hasContextMenuActions || isEditing) return;
|
||||||
setShowSendBackMenu(false);
|
setShowSendBackMenu(false);
|
||||||
|
setShowDoneActionsMenu(false);
|
||||||
setContextMenuPosition({
|
setContextMenuPosition({
|
||||||
x: Math.max(CONTEXT_MENU_VIEWPORT_MARGIN, Math.min(clientX, window.innerWidth - CONTEXT_MENU_VIEWPORT_MARGIN)),
|
x: Math.max(CONTEXT_MENU_VIEWPORT_MARGIN, Math.min(clientX, window.innerWidth - CONTEXT_MENU_VIEWPORT_MARGIN)),
|
||||||
y: Math.max(CONTEXT_MENU_VIEWPORT_MARGIN, Math.min(clientY, window.innerHeight - CONTEXT_MENU_VIEWPORT_MARGIN)),
|
y: Math.max(CONTEXT_MENU_VIEWPORT_MARGIN, Math.min(clientY, window.innerHeight - CONTEXT_MENU_VIEWPORT_MARGIN)),
|
||||||
@@ -2564,6 +2579,11 @@ function TaskCardComponent({
|
|||||||
setShowSendBackMenu((current) => !current);
|
setShowSendBackMenu((current) => !current);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleDoneActionsToggle = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowDoneActionsMenu((current) => !current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSendBackOptionClick = useCallback(async (e: React.MouseEvent, column: Column) => {
|
const handleSendBackOptionClick = useCallback(async (e: React.MouseEvent, column: Column) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowSendBackMenu(false);
|
setShowSendBackMenu(false);
|
||||||
@@ -3143,15 +3163,56 @@ function TaskCardComponent({
|
|||||||
<Trash2 size={12} />
|
<Trash2 size={12} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{task.column === "done" && onArchiveTask && (
|
{task.column === "done" && (onArchiveTask || (onRevertTask && isRevertable)) && (
|
||||||
<button
|
<div className="card-send-back card-done-actions" ref={doneActionsRef}>
|
||||||
className="card-archive-btn"
|
{/*
|
||||||
onClick={handleArchiveClick}
|
FNXC:BoardCardActions 2026-07-11-00:00 (FN-7839):
|
||||||
title={t("tasks.archiveTask", "Archive task")}
|
Done-card Archive + Revert are grouped behind one dropdown that mirrors the
|
||||||
aria-label={t("tasks.archiveTask", "Archive task")}
|
in-progress "Send back" control, replacing two standalone inline buttons.
|
||||||
>
|
Revert only appears when the task is revertable (isRevertable); the trigger
|
||||||
{t("tasks.archive", "Archive")}
|
only renders when at least one action is available so no empty shell is left.
|
||||||
</button>
|
Reuses .card-send-back* styling so no new one-off CSS/colors are introduced.
|
||||||
|
*/}
|
||||||
|
<button
|
||||||
|
className="card-send-back-btn"
|
||||||
|
onClick={handleDoneActionsToggle}
|
||||||
|
title={t("tasks.doneActions", "Actions")}
|
||||||
|
aria-label={t("tasks.doneActions", "Actions")}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={showDoneActionsMenu}
|
||||||
|
>
|
||||||
|
{t("tasks.doneActions", "Actions")}
|
||||||
|
<ChevronDown size={10} />
|
||||||
|
</button>
|
||||||
|
{showDoneActionsMenu && (
|
||||||
|
<div className="card-send-back-menu" role="menu">
|
||||||
|
{onArchiveTask && (
|
||||||
|
<button
|
||||||
|
className="card-send-back-menu-item"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={(e) => {
|
||||||
|
setShowDoneActionsMenu(false);
|
||||||
|
handleArchiveClick(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("tasks.archive", "Archive")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onRevertTask && isRevertable && (
|
||||||
|
<button
|
||||||
|
className="card-send-back-menu-item"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={(e) => {
|
||||||
|
setShowDoneActionsMenu(false);
|
||||||
|
handleRevertClick(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("tasks.revert", "Revert")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{task.column === "archived" && onUnarchiveTask && (
|
{task.column === "archived" && onUnarchiveTask && (
|
||||||
<button
|
<button
|
||||||
@@ -3165,14 +3226,15 @@ function TaskCardComponent({
|
|||||||
)}
|
)}
|
||||||
{/*
|
{/*
|
||||||
FNXC:TaskRevert 2026-07-05-00:00 (FN-7525):
|
FNXC:TaskRevert 2026-07-05-00:00 (FN-7525):
|
||||||
Inline Revert affordance for done/archived cards (parent FN-7501). Rendered
|
Inline Revert affordance for archived cards (parent FN-7501). Rendered
|
||||||
only when the task actually has a landed commit to revert (`isRevertable`)
|
only when the task actually has a landed commit to revert (`isRevertable`)
|
||||||
— omitted (not disabled) here to avoid an empty button shell on cards with
|
— omitted (not disabled) here to avoid an empty button shell on cards with
|
||||||
nothing to revert, matching the "omit inline / disable in menu" split called
|
nothing to revert, matching the "omit inline / disable in menu" split called
|
||||||
out in the task spec. Reuses `card-archive-btn`'s tokenized styling via a
|
out in the task spec. Done cards use the FN-7839 actions dropdown above.
|
||||||
shared class so no new one-off CSS/colors are introduced.
|
Reuses `card-archive-btn`'s tokenized styling via a shared class so no new
|
||||||
|
one-off CSS/colors are introduced.
|
||||||
*/}
|
*/}
|
||||||
{(task.column === "done" || task.column === "archived") && onRevertTask && isRevertable && (
|
{task.column === "archived" && onRevertTask && isRevertable && (
|
||||||
<button
|
<button
|
||||||
className="card-archive-btn card-revert-btn"
|
className="card-archive-btn card-revert-btn"
|
||||||
onClick={handleRevertClick}
|
onClick={handleRevertClick}
|
||||||
|
|||||||
@@ -1161,7 +1161,7 @@ describe("TaskCard", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides delete button for done tasks while keeping archive action", () => {
|
it("hides delete button for done tasks while keeping archive action in the actions dropdown", () => {
|
||||||
const onDeleteTask = vi.fn(async () => makeTask());
|
const onDeleteTask = vi.fn(async () => makeTask());
|
||||||
const onArchiveTask = vi.fn(async () => makeTask({ column: "archived" }));
|
const onArchiveTask = vi.fn(async () => makeTask({ column: "archived" }));
|
||||||
|
|
||||||
@@ -1176,7 +1176,8 @@ describe("TaskCard", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByLabelText("Delete task")).toBeNull();
|
expect(screen.queryByLabelText("Delete task")).toBeNull();
|
||||||
expect(screen.getByLabelText("Archive task")).toBeDefined();
|
expect(screen.getByRole("button", { name: "Actions" })).toBeDefined();
|
||||||
|
expect(screen.queryByLabelText("Archive task")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(["triage", "todo", "in-progress", "in-review"] as const)(
|
it.each(["triage", "todo", "in-progress", "in-review"] as const)(
|
||||||
@@ -1192,20 +1193,46 @@ describe("TaskCard", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByLabelText("Archive task")).toBeNull();
|
expect(screen.queryByLabelText("Archive task")).toBeNull();
|
||||||
|
expect(screen.queryByRole("button", { name: "Actions" })).toBeNull();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("renders archive action for done tasks", () => {
|
it("renders archive action for done tasks inside the actions dropdown", async () => {
|
||||||
|
const onArchiveTask = vi.fn(async () => makeTask({ column: "archived" }));
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={makeTask({ column: "done" })}
|
task={makeTask({ column: "done" })}
|
||||||
onOpenDetail={noop}
|
onOpenDetail={noop}
|
||||||
addToast={noop}
|
addToast={noop}
|
||||||
onArchiveTask={vi.fn(async () => makeTask({ column: "archived" }))}
|
onArchiveTask={onArchiveTask}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByLabelText("Archive task")).toBeDefined();
|
const actionsButton = screen.getByRole("button", { name: "Actions" });
|
||||||
|
expect(screen.queryByLabelText("Archive task")).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.click(actionsButton);
|
||||||
|
|
||||||
|
const menu = screen.getByRole("menu");
|
||||||
|
expect(within(menu).getByRole("menuitem", { name: "Archive" })).toBeDefined();
|
||||||
|
|
||||||
|
fireEvent.click(within(menu).getByRole("menuitem", { name: "Archive" }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(onArchiveTask).toHaveBeenCalledWith("FN-001"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render an empty done actions dropdown when no done action is available", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TaskCard
|
||||||
|
task={makeTask({ column: "done", mergeDetails: undefined })}
|
||||||
|
onOpenDetail={noop}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByRole("button", { name: "Actions" })).toBeNull();
|
||||||
|
expect(container.querySelector(".card-done-actions")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render archive action for archived tasks", () => {
|
it("does not render archive action for archived tasks", () => {
|
||||||
@@ -1220,28 +1247,34 @@ describe("TaskCard", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByLabelText("Archive task")).toBeNull();
|
expect(screen.queryByLabelText("Archive task")).toBeNull();
|
||||||
|
expect(screen.queryByRole("button", { name: "Actions" })).toBeNull();
|
||||||
expect(screen.getByLabelText("Unarchive task")).toBeDefined();
|
expect(screen.getByLabelText("Unarchive task")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:TaskRevert 2026-07-05-00:00 (FN-7525):
|
FNXC:TaskRevert 2026-07-05-00:00 (FN-7525):
|
||||||
Coverage for the Revert affordance: presence/absence on done + archived
|
Coverage for the Revert affordance: presence/absence on done + archived
|
||||||
cards (inline row + context menu), the disabled/omitted no-commit-to-revert
|
cards (done-actions dropdown, archived inline row, and context menu), the
|
||||||
guard, the auto→clean-success path, and the auto→conflict→confirm→AI-undo
|
disabled/omitted no-commit-to-revert guard, the auto→clean-success path,
|
||||||
fallback path.
|
and the auto→conflict→confirm→AI-undo fallback path.
|
||||||
*/
|
*/
|
||||||
describe("Revert affordance", () => {
|
describe("Revert affordance", () => {
|
||||||
it("renders the inline Revert button for a done card with a landed commit", () => {
|
it("renders Revert inside the done actions dropdown for a done card with a landed commit", () => {
|
||||||
render(
|
const { container } = render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={makeTask({ column: "done", mergeDetails: { commitSha: "abc123def456" } as any })}
|
task={makeTask({ column: "done", mergeDetails: { commitSha: "abc123def456" } as any })}
|
||||||
onOpenDetail={noop}
|
onOpenDetail={noop}
|
||||||
addToast={noop}
|
addToast={noop}
|
||||||
|
onArchiveTask={vi.fn(async () => makeTask({ column: "archived" }))}
|
||||||
onRevertTask={vi.fn(async () => ({ mode: "git", clean: true, revertCommitSha: "deadbeef" }) as any)}
|
onRevertTask={vi.fn(async () => ({ mode: "git", clean: true, revertCommitSha: "deadbeef" }) as any)}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByLabelText("Revert this task's changes")).toBeDefined();
|
expect(container.querySelector(".card-revert-btn")).toBeNull();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
const menu = screen.getByRole("menu");
|
||||||
|
expect(within(menu).getByRole("menuitem", { name: "Archive" })).toBeDefined();
|
||||||
|
expect(within(menu).getByRole("menuitem", { name: "Revert" })).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the inline Revert button for an archived card with a landed commit", () => {
|
it("renders the inline Revert button for an archived card with a landed commit", () => {
|
||||||
@@ -1269,17 +1302,22 @@ describe("TaskCard", () => {
|
|||||||
expect(screen.queryByLabelText("Revert this task's changes")).toBeNull();
|
expect(screen.queryByLabelText("Revert this task's changes")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits the inline Revert button when the task has no landed commit", () => {
|
it("omits Revert from the done actions dropdown when the task has no landed commit", () => {
|
||||||
render(
|
const { container } = render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={makeTask({ column: "done", mergeDetails: undefined })}
|
task={makeTask({ column: "done", mergeDetails: undefined })}
|
||||||
onOpenDetail={noop}
|
onOpenDetail={noop}
|
||||||
addToast={noop}
|
addToast={noop}
|
||||||
|
onArchiveTask={vi.fn(async () => makeTask({ column: "archived" }))}
|
||||||
onRevertTask={vi.fn(async () => ({ mode: "git", clean: true, revertCommitSha: "deadbeef" }) as any)}
|
onRevertTask={vi.fn(async () => ({ mode: "git", clean: true, revertCommitSha: "deadbeef" }) as any)}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByLabelText("Revert this task's changes")).toBeNull();
|
expect(container.querySelector(".card-revert-btn")).toBeNull();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
const menu = screen.getByRole("menu");
|
||||||
|
expect(within(menu).getByRole("menuitem", { name: "Archive" })).toBeDefined();
|
||||||
|
expect(within(menu).queryByRole("menuitem", { name: "Revert" })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a disabled Revert context-menu entry when the task has no landed commit", () => {
|
it("shows a disabled Revert context-menu entry when the task has no landed commit", () => {
|
||||||
@@ -1324,8 +1362,10 @@ describe("TaskCard", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByLabelText("Revert this task's changes"));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Revert" }));
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -1355,8 +1395,10 @@ describe("TaskCard", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByLabelText("Revert this task's changes"));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Revert" }));
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -3446,7 +3488,7 @@ describe("TaskCard", () => {
|
|||||||
expect(actionsContainer?.contains(editBtn)).toBe(true);
|
expect(actionsContainer?.contains(editBtn)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders archive button inside card-header-actions for done columns", () => {
|
it("renders done actions dropdown inside card-header-actions for done columns", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={makeTask({ column: "done", size: "L" })}
|
task={makeTask({ column: "done", size: "L" })}
|
||||||
@@ -3456,11 +3498,11 @@ describe("TaskCard", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
const actionsContainer = container.querySelector(".card-header-actions");
|
const actionsContainer = container.querySelector(".card-header-actions");
|
||||||
const archiveBtn = container.querySelector(".card-archive-btn");
|
const actionsButton = screen.getByRole("button", { name: "Actions" });
|
||||||
|
|
||||||
expect(actionsContainer).not.toBeNull();
|
expect(actionsContainer).not.toBeNull();
|
||||||
expect(archiveBtn).not.toBeNull();
|
expect(container.querySelector(".card-archive-btn")).toBeNull();
|
||||||
expect(actionsContainer?.contains(archiveBtn)).toBe(true);
|
expect(actionsContainer?.contains(actionsButton)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders in-review Move control inline in card-meta for overlap-blocked tasks", () => {
|
it("renders in-review Move control inline in card-meta for overlap-blocked tasks", () => {
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ import { fetchTaskDetail } from "../../api";
|
|||||||
import { InlineCreateCard } from "../InlineCreateCard";
|
import { InlineCreateCard } from "../InlineCreateCard";
|
||||||
import { TaskCard } from "../TaskCard";
|
import { TaskCard } from "../TaskCard";
|
||||||
|
|
||||||
|
// FNXC:TaskCardTestHarness 2026-07-11-00:00: RuntimeFallbackBadge calls useToast directly, so isolated TaskCard mobile renders need the hook mocked unless wrapped in ToastProvider.
|
||||||
|
vi.mock("../../hooks/useToast", () => ({
|
||||||
|
useToast: () => ({
|
||||||
|
addToast: vi.fn(),
|
||||||
|
removeToast: vi.fn(),
|
||||||
|
toasts: [],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("../../api", () => ({
|
||||||
fetchTaskDetail: vi.fn(),
|
fetchTaskDetail: vi.fn(),
|
||||||
uploadAttachment: vi.fn(),
|
uploadAttachment: vi.fn(),
|
||||||
@@ -432,7 +441,7 @@ describe("TaskCard mobile", () => {
|
|||||||
expectRuleToContain(mobileSection, ".card-revert-btn", "opacity: 1;");
|
expectRuleToContain(mobileSection, ".card-revert-btn", "opacity: 1;");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the Revert affordance on a done card at the mobile breakpoint", () => {
|
it("renders the Revert affordance inside the done actions dropdown at the mobile breakpoint", () => {
|
||||||
const task = createTask({ id: "FN-201", column: "done", mergeDetails: { commitSha: "abc123def456" } as any });
|
const task = createTask({ id: "FN-201", column: "done", mergeDetails: { commitSha: "abc123def456" } as any });
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
@@ -444,7 +453,9 @@ describe("TaskCard mobile", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(container.querySelector(".card-revert-btn")).toBeTruthy();
|
expect(container.querySelector(".card-revert-btn")).toBeNull();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
expect(screen.getByRole("menuitem", { name: "Revert" })).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the Revert affordance on an archived card at the mobile breakpoint", () => {
|
it("renders the Revert affordance on an archived card at the mobile breakpoint", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user