Files
kandev/apps/web/components/task/task-item.test.tsx
T
2026-06-14 21:40:52 +01:00

130 lines
5.1 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import type { ComponentProps } from "react";
import { StateProvider } from "@/components/state-provider";
import { TaskItem } from "./task-item";
import { TooltipProvider } from "@kandev/ui/tooltip";
const REVIEW_ICON_TEST_ID = "task-state-review";
const RUNNING_ICON_TEST_ID = "task-state-running";
const WAITING_FOR_INPUT_ICON_TEST_ID = "task-state-waiting-for-input";
const PENDING_PERMISSION_ICON_TEST_ID = "task-state-pending-permission";
const AGENT_ERROR_ICON_TEST_ID = "task-agent-error-icon";
const PREPARING_PHASE = "preparing";
const PURPLE_SPINNER_CLASS = "text-purple-500";
const SPIN_CLASS = "animate-spin";
const SLOW_SPIN_CLASS = "[animation-duration:2s]";
afterEach(() => cleanup());
function renderTaskItem(props: Partial<ComponentProps<typeof TaskItem>> = {}) {
return render(
<StateProvider>
<TooltipProvider>
<TaskItem title="Needs answer" state="REVIEW" {...props} />
</TooltipProvider>
</StateProvider>,
);
}
function expectPreparingSpinner(): void {
const icon = screen.getByTestId(RUNNING_ICON_TEST_ID);
expect(icon.getAttribute("data-loading-phase")).toBe(PREPARING_PHASE);
expect(icon.classList.contains(PURPLE_SPINNER_CLASS)).toBe(true);
expect(icon.classList.contains(SPIN_CLASS)).toBe(true);
expect(icon.classList.contains(SLOW_SPIN_CLASS)).toBe(true);
}
describe("TaskItem status icon", () => {
it("keeps the review check when the session is idle after a turn", () => {
renderTaskItem({ sessionState: "WAITING_FOR_INPUT" });
expect(screen.queryByTestId(REVIEW_ICON_TEST_ID)).not.toBeNull();
expect(screen.queryByTestId(WAITING_FOR_INPUT_ICON_TEST_ID)).toBeNull();
});
it("shows question icon when a clarification is pending", () => {
renderTaskItem({ sessionState: "WAITING_FOR_INPUT", hasPendingClarification: true });
expect(screen.queryByTestId(WAITING_FOR_INPUT_ICON_TEST_ID)).not.toBeNull();
expect(screen.queryByTestId(REVIEW_ICON_TEST_ID)).toBeNull();
});
it("shows question icon when task state is waiting for input", () => {
renderTaskItem({ state: "WAITING_FOR_INPUT", hasPendingClarification: false });
expect(screen.queryByTestId(WAITING_FOR_INPUT_ICON_TEST_ID)).not.toBeNull();
expect(screen.queryByTestId(REVIEW_ICON_TEST_ID)).toBeNull();
});
it("shows alert icon when a permission request is pending", () => {
renderTaskItem({ sessionState: "WAITING_FOR_INPUT", hasPendingPermission: true });
expect(screen.queryByTestId(PENDING_PERMISSION_ICON_TEST_ID)).not.toBeNull();
expect(screen.queryByTestId(REVIEW_ICON_TEST_ID)).toBeNull();
expect(screen.queryByTestId(WAITING_FOR_INPUT_ICON_TEST_ID)).toBeNull();
});
it("prefers clarification icon over permission icon when both are pending", () => {
renderTaskItem({
sessionState: "WAITING_FOR_INPUT",
hasPendingClarification: true,
hasPendingPermission: true,
});
expect(screen.queryByTestId(WAITING_FOR_INPUT_ICON_TEST_ID)).not.toBeNull();
expect(screen.queryByTestId(PENDING_PERMISSION_ICON_TEST_ID)).toBeNull();
});
it("shows a slower purple spinner while the workflow is scheduling", () => {
renderTaskItem({ state: "SCHEDULING" });
expectPreparingSpinner();
});
it("shows a slower purple spinner while the session is starting before progress", () => {
renderTaskItem({ state: "TODO", sessionState: "STARTING" });
expectPreparingSpinner();
});
it("shows a slower purple spinner for in-progress tasks while the session is starting", () => {
renderTaskItem({ state: "IN_PROGRESS", sessionState: "STARTING" });
expectPreparingSpinner();
});
it("does not show preparing spinner for a review task whose session transiently hits STARTING", () => {
renderTaskItem({ state: "REVIEW", sessionState: "STARTING" });
expect(screen.queryByTestId(REVIEW_ICON_TEST_ID)).not.toBeNull();
expect(screen.queryByTestId(RUNNING_ICON_TEST_ID)).toBeNull();
});
it("keeps running tasks on the normal running spinner", () => {
renderTaskItem({ state: "IN_PROGRESS", sessionState: "RUNNING" });
const icon = screen.getByTestId(RUNNING_ICON_TEST_ID);
expect(icon.getAttribute("data-loading-phase")).toBe("running");
expect(icon.classList.contains("text-yellow-500")).toBe(true);
expect(icon.classList.contains(SPIN_CLASS)).toBe(true);
expect(icon.classList.contains(PURPLE_SPINNER_CLASS)).toBe(false);
});
it("keeps the review check for completed review tasks", () => {
renderTaskItem({ sessionState: "COMPLETED" });
expect(screen.queryByTestId(REVIEW_ICON_TEST_ID)).not.toBeNull();
expect(screen.queryByTestId(WAITING_FOR_INPUT_ICON_TEST_ID)).toBeNull();
});
it("shows a separate agent error icon when the task has retained error details", () => {
renderTaskItem({ agentErrorMessage: "peer disconnected before response" });
const errorIcon = screen.getByTestId(AGENT_ERROR_ICON_TEST_ID);
expect(errorIcon).not.toBeNull();
expect(errorIcon.classList.contains("cursor-help")).toBe(true);
expect(screen.queryByTestId(REVIEW_ICON_TEST_ID)).not.toBeNull();
});
});