feat(FN-1624): add delete button to TaskCard for triage column
- Add delete button component to TaskCard for tasks in the triage column - Thread onDeleteTask prop through App → Board → Column → TaskCard - Add CSS styles for delete button with hover/active states - Add comprehensive tests for delete button visibility and click handling - Hide delete button for non-triage columns (todo, in-progress, etc.)
This commit is contained in:
@@ -47,6 +47,7 @@ vi.mock("lucide-react", () => ({
|
||||
CircleDot: ({ size }: { size?: number }) => <span data-testid="circle-dot-icon">⭕</span>,
|
||||
Target: ({ size }: { size?: number }) => <span data-testid="target-icon">🎯</span>,
|
||||
Bot: ({ size }: { size?: number }) => <span data-testid="bot-icon">🤖</span>,
|
||||
Trash2: ({ size }: { size?: number }) => <span data-testid="trash-icon">🗑️</span>,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -3833,3 +3834,178 @@ describe("TaskCard send-back functionality", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests for delete button functionality in TaskCard.
|
||||
*/
|
||||
describe("TaskCard delete button", () => {
|
||||
const noopToast = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("shows delete button for triage column tasks when onDeleteTask is provided", () => {
|
||||
const task = makeTask({ column: "triage" });
|
||||
const onDeleteTask = vi.fn().mockResolvedValue(task);
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
onDeleteTask={onDeleteTask}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteBtn = screen.getByRole("button", { name: /Delete task/i });
|
||||
expect(deleteBtn).toBeDefined();
|
||||
expect(deleteBtn.classList.contains("card-delete-btn")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show delete button for non-triage column tasks", () => {
|
||||
for (const column of ["todo", "in-progress", "in-review", "done", "archived"] as const) {
|
||||
const task = makeTask({ column });
|
||||
const onDeleteTask = vi.fn().mockResolvedValue(task);
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
onDeleteTask={onDeleteTask}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteBtn = screen.queryByRole("button", { name: /Delete task/i });
|
||||
expect(deleteBtn).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not show delete button when onDeleteTask is not provided", () => {
|
||||
const task = makeTask({ column: "triage" });
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteBtn = screen.queryByRole("button", { name: /Delete task/i });
|
||||
expect(deleteBtn).toBeNull();
|
||||
});
|
||||
|
||||
it("clicking delete button shows confirmation dialog and calls onDeleteTask on confirm", async () => {
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
const task = makeTask({ column: "triage", id: "FN-123" });
|
||||
const onDeleteTask = vi.fn().mockResolvedValue(task);
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
onDeleteTask={onDeleteTask}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteBtn = screen.getByRole("button", { name: /Delete task/i });
|
||||
fireEvent.click(deleteBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.confirm).toHaveBeenCalledWith("Delete FN-123?");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onDeleteTask).toHaveBeenCalledWith("FN-123");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(noopToast).toHaveBeenCalledWith("Deleted FN-123", "success");
|
||||
});
|
||||
});
|
||||
|
||||
it("clicking delete button does not call onDeleteTask when confirmation is cancelled", async () => {
|
||||
vi.spyOn(window, "confirm").mockReturnValue(false);
|
||||
const task = makeTask({ column: "triage", id: "FN-456" });
|
||||
const onDeleteTask = vi.fn().mockResolvedValue(task);
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
onDeleteTask={onDeleteTask}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteBtn = screen.getByRole("button", { name: /Delete task/i });
|
||||
fireEvent.click(deleteBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.confirm).toHaveBeenCalledWith("Delete FN-456?");
|
||||
});
|
||||
|
||||
expect(onDeleteTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delete button click does not propagate to card click (does not open detail)", async () => {
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
const task = makeTask({ column: "triage", id: "FN-789" });
|
||||
const onDeleteTask = vi.fn().mockResolvedValue(task);
|
||||
const onOpenDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={onOpenDetail}
|
||||
addToast={noopToast}
|
||||
onDeleteTask={onDeleteTask}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteBtn = screen.getByRole("button", { name: /Delete task/i });
|
||||
fireEvent.click(deleteBtn);
|
||||
|
||||
// Wait for the click to propagate
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
// onOpenDetail should not have been called because stopPropagation was used
|
||||
expect(onOpenDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows error toast when delete fails", async () => {
|
||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
const task = makeTask({ column: "triage", id: "FN-ERROR" });
|
||||
const onDeleteTask = vi.fn().mockRejectedValue(new Error("Network error"));
|
||||
const addToast = vi.fn();
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={addToast}
|
||||
onDeleteTask={onDeleteTask}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteBtn = screen.getByRole("button", { name: /Delete task/i });
|
||||
fireEvent.click(deleteBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addToast).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to delete FN-ERROR"),
|
||||
"error"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user