Files
kandev/apps/web/components/task/simple/components/status-picker.test.tsx
T
2026-05-16 20:07:57 +01:00

156 lines
4.5 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { StateProvider } from "@/components/state-provider";
import { TaskOptimisticContextProvider } from "@/hooks/use-optimistic-task-mutation";
import { ApiError } from "@/lib/api/client";
import { StatusPicker, formatPendingApproversMessage } from "./status-picker";
import type { Task } from "@/app/office/tasks/[id]/types";
const updateTaskMock = vi.hoisted(() => vi.fn().mockResolvedValue({ ok: true }));
const toastErrorMock = vi.hoisted(() => vi.fn());
vi.mock("@/lib/api/domains/office-extended-api", async () => {
const actual = await vi.importActual<typeof import("@/lib/api/domains/office-extended-api")>(
"@/lib/api/domains/office-extended-api",
);
return {
...actual,
updateTask: updateTaskMock,
};
});
vi.mock("sonner", async () => {
const actual = await vi.importActual<typeof import("sonner")>("sonner");
return {
...actual,
toast: { ...actual.toast, error: toastErrorMock },
};
});
import { updateTask } from "@/lib/api/domains/office-extended-api";
afterEach(() => {
cleanup();
vi.clearAllMocks();
updateTaskMock.mockResolvedValue({ ok: true });
});
const TRIGGER_TEST_ID = "status-picker-trigger";
const task: Task = {
id: "t-1",
workspaceId: "ws-1",
identifier: "TASK-1",
title: "First task",
status: "todo",
priority: "medium",
labels: [],
blockedBy: [],
blocking: [],
children: [],
reviewers: [],
approvers: [],
decisions: [],
createdBy: "user",
createdAt: "2026-05-01T00:00:00Z",
updatedAt: "2026-05-01T00:00:00Z",
};
function Wrapper({ children }: { children: ReactNode }) {
const ctx = {
task,
applyPatch: vi.fn(),
restore: vi.fn(),
};
return (
<StateProvider>
<TaskOptimisticContextProvider value={ctx}>{children}</TaskOptimisticContextProvider>
</StateProvider>
);
}
describe("StatusPicker", () => {
it("renders the current status label on the trigger", () => {
render(
<Wrapper>
<StatusPicker task={task} />
</Wrapper>,
);
const trigger = screen.getByTestId(TRIGGER_TEST_ID);
expect(trigger.getAttribute("aria-haspopup")).toBe("listbox");
expect(trigger.textContent).toContain("Todo");
});
it("calls updateTask with the new status when an option is selected", async () => {
render(
<Wrapper>
<StatusPicker task={task} />
</Wrapper>,
);
fireEvent.click(screen.getByTestId(TRIGGER_TEST_ID));
const option = await screen.findByTestId("status-picker-option-in_progress");
await act(async () => {
fireEvent.click(option);
});
expect(updateTask).toHaveBeenCalledWith("t-1", { status: "in_progress" });
});
it("does not call the API when the same status is re-selected", async () => {
render(
<Wrapper>
<StatusPicker task={task} />
</Wrapper>,
);
fireEvent.click(screen.getByTestId(TRIGGER_TEST_ID));
const option = await screen.findByTestId("status-picker-option-todo");
await act(async () => {
fireEvent.click(option);
});
expect(updateTask).not.toHaveBeenCalled();
});
it("toasts the formatted approvers message on a 409 gate response", async () => {
updateTaskMock.mockRejectedValueOnce(
new ApiError("approvals pending", 409, {
error: "approvals pending",
pending_approvers: [
{ agent_profile_id: "a1", name: "CEO" },
{ agent_profile_id: "a2", name: "Eng Lead" },
],
status: "in_review",
}),
);
render(
<Wrapper>
<StatusPicker task={task} />
</Wrapper>,
);
fireEvent.click(screen.getByTestId(TRIGGER_TEST_ID));
const option = await screen.findByTestId("status-picker-option-done");
await act(async () => {
fireEvent.click(option);
});
expect(toastErrorMock).toHaveBeenCalledWith(
"Cannot mark done: awaiting approval from CEO, Eng Lead",
);
});
});
describe("formatPendingApproversMessage", () => {
it("joins names with commas", () => {
expect(
formatPendingApproversMessage([
{ agent_profile_id: "a", name: "CEO" },
{ agent_profile_id: "b", name: "Eng Lead" },
]),
).toBe("Cannot mark done: awaiting approval from CEO, Eng Lead");
});
it("falls back to the agent id when name is missing", () => {
expect(formatPendingApproversMessage([{ agent_profile_id: "agent-x" }])).toBe(
"Cannot mark done: awaiting approval from agent-x",
);
});
});