test(KB-082): complete Step 6 — Add/update tests for error field and retry
This commit is contained in:
@@ -178,6 +178,123 @@ describe("TaskCard failed status", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskCard error display", () => {
|
||||
const noopToast = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function makeTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: "KB-099",
|
||||
description: "Test task",
|
||||
column: "in-progress" as Column,
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders error message when task has failed status and error field", () => {
|
||||
const task = makeTask({
|
||||
status: "failed",
|
||||
error: "Build failed: cannot find module",
|
||||
});
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const errorElement = screen.getByText("Build failed: cannot find module");
|
||||
expect(errorElement).toBeDefined();
|
||||
});
|
||||
|
||||
it("truncates long error messages to 60 characters", () => {
|
||||
const longError = "This is a very long error message that should be truncated because it exceeds sixty characters";
|
||||
const task = makeTask({
|
||||
status: "failed",
|
||||
error: longError,
|
||||
});
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should show truncated text with ellipsis
|
||||
const truncatedText = "This is a very long error message that should be truncat…";
|
||||
const errorElement = screen.getByText(truncatedText);
|
||||
expect(errorElement).toBeDefined();
|
||||
});
|
||||
|
||||
it("does NOT render error section when task is not failed", () => {
|
||||
const task = makeTask({
|
||||
status: "executing",
|
||||
});
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const errorSection = document.querySelector(".card-error");
|
||||
expect(errorSection).toBeNull();
|
||||
});
|
||||
|
||||
it("does NOT render error section when task is failed but has no error message", () => {
|
||||
const task = makeTask({
|
||||
status: "failed",
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const errorSection = document.querySelector(".card-error");
|
||||
expect(errorSection).toBeNull();
|
||||
});
|
||||
|
||||
it("error section has tooltip with full error message", () => {
|
||||
const errorMessage = "Full error message for tooltip";
|
||||
const task = makeTask({
|
||||
status: "failed",
|
||||
error: errorMessage,
|
||||
});
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
const errorSection = document.querySelector(".card-error");
|
||||
expect(errorSection).toBeDefined();
|
||||
expect(errorSection?.getAttribute("title")).toBe(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskCard dependency tooltip", () => {
|
||||
/** Mirrors the data-tooltip computation from TaskCard.tsx */
|
||||
function computeDepTooltip(dependencies: string[]): string | undefined {
|
||||
|
||||
@@ -165,6 +165,105 @@ describe("TaskDetailModal", () => {
|
||||
expect(screen.queryByText("Retry")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders Retry button for failed tasks in any column (including done)", () => {
|
||||
const columns: Column[] = ["triage", "todo", "in-progress", "in-review", "done"];
|
||||
|
||||
for (const column of columns) {
|
||||
const { unmount } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ status: "failed", column })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
onRetryTask={noopRetry}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Retry")).toBeTruthy();
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders error alert when task has failed status and error field", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ status: "failed", error: "Build failed: cannot find module" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const errorAlert = container.querySelector(".detail-error-alert");
|
||||
expect(errorAlert).toBeTruthy();
|
||||
expect(screen.getByText("Task Failed")).toBeTruthy();
|
||||
expect(screen.getByText("Build failed: cannot find module")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does NOT render error alert when task is not failed", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ status: "executing" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const errorAlert = container.querySelector(".detail-error-alert");
|
||||
expect(errorAlert).toBeNull();
|
||||
});
|
||||
|
||||
it("does NOT render error alert when task is failed but has no error message", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ status: "failed", error: undefined })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const errorAlert = container.querySelector(".detail-error-alert");
|
||||
expect(errorAlert).toBeNull();
|
||||
});
|
||||
|
||||
it("calls onRetryTask when Retry button is clicked", async () => {
|
||||
const mockRetry = vi.fn().mockResolvedValue({});
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ status: "failed", column: "done" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
onRetryTask={mockRetry}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const retryButton = screen.getByText("Retry");
|
||||
fireEvent.click(retryButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRetry).toHaveBeenCalledWith("KB-099");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows description exactly once for a task without title", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
|
||||
Reference in New Issue
Block a user