/** * @file StatusBadge.test.tsx * @description Unit tests for the StatusBadge component, which includes AgentStatusBadge and SessionStatusBadge. These components are responsible for displaying the status of agents and sessions in the dashboard. The tests cover rendering of different statuses, application of pulse animation based on status, respect for explicit pulse overrides, and the awaiting-reason suffix (icon + short label + hover tooltip) that explains WHY a row is in the Waiting state. The tests use React Testing Library and Vitest for assertions and mocking. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { AgentStatusBadge, SessionStatusBadge } from "../StatusBadge"; describe("AgentStatusBadge", () => { it("should render waiting status", () => { render(); expect(screen.getByText("Waiting")).toBeInTheDocument(); }); it("should render working status", () => { render(); expect(screen.getByText("Working")).toBeInTheDocument(); }); it("should render completed status", () => { render(); expect(screen.getByText("Completed")).toBeInTheDocument(); }); it("should render error status", () => { render(); expect(screen.getByText("Error")).toBeInTheDocument(); }); it("should apply pulse animation for working status by default", () => { const { container } = render(); const dot = container.querySelector(".animate-pulse-dot"); expect(dot).toBeInTheDocument(); }); it("should not apply pulse for connected status (now working - has pulse)", () => { const { container } = render(); const dot = container.querySelector(".animate-pulse-dot"); expect(dot).toBeInTheDocument(); }); it("should apply pulse animation for waiting status by default", () => { const { container } = render(); const dot = container.querySelector(".animate-pulse-dot"); expect(dot).toBeInTheDocument(); }); it("should respect explicit pulse=false override", () => { const { container } = render(); const dot = container.querySelector(".animate-pulse-dot"); expect(dot).not.toBeInTheDocument(); }); it("should respect explicit pulse=true override", () => { const { container } = render(); const dot = container.querySelector(".animate-pulse-dot"); expect(dot).toBeInTheDocument(); }); it("should render waiting status with yellow dot and pulse by default", () => { const { container } = render(); expect(screen.getByText("Waiting")).toBeInTheDocument(); const dot = container.querySelector(".animate-pulse-dot"); expect(dot).toBeInTheDocument(); expect(container.querySelector(".bg-yellow-400")).toBeInTheDocument(); }); }); describe("SessionStatusBadge", () => { it("should render active status", () => { render(); expect(screen.getByText("Active")).toBeInTheDocument(); }); it("should render completed status", () => { render(); expect(screen.getByText("Completed")).toBeInTheDocument(); }); it("should render error status", () => { render(); expect(screen.getByText("Error")).toBeInTheDocument(); }); it("should render abandoned status", () => { render(); expect(screen.getByText("Abandoned")).toBeInTheDocument(); }); it("should render waiting status with pulsing yellow dot", () => { const { container } = render(); expect(screen.getByText("Waiting")).toBeInTheDocument(); const dot = container.querySelector(".animate-pulse-dot"); expect(dot).toBeInTheDocument(); expect(container.querySelector(".bg-yellow-400")).toBeInTheDocument(); }); }); describe("awaiting-reason suffix", () => { it("renders the reason label next to Waiting on AgentStatusBadge", () => { render(); expect(screen.getByText("Waiting")).toBeInTheDocument(); expect(screen.getByText("Needs input")).toBeInTheDocument(); }); it("renders the reason label next to Waiting on SessionStatusBadge", () => { render(); expect(screen.getByText("Waiting")).toBeInTheDocument(); expect(screen.getByText("Turn done")).toBeInTheDocument(); }); it("ignores the reason on non-waiting statuses", () => { render(); expect(screen.queryByText("Needs input")).not.toBeInTheDocument(); render(); expect(screen.queryByText("Turn done")).not.toBeInTheDocument(); }); it("renders no suffix when reason is null/omitted", () => { render(); expect(screen.getByText("Waiting")).toBeInTheDocument(); expect(screen.queryByText("Needs input")).not.toBeInTheDocument(); expect(screen.queryByText("Turn done")).not.toBeInTheDocument(); }); it("shows the full reason description in a tooltip on hover", () => { const { container } = render(); expect(screen.getByText("Interrupted")).toBeInTheDocument(); // Tip attaches its handlers to the wrapper element and portals the tooltip // body into document.body. fireEvent.mouseEnter(container.firstElementChild!, { clientX: 10, clientY: 10 }); expect(screen.getByText(/The last turn was interrupted/)).toBeInTheDocument(); }); it("marks urgent reasons with the hotter amber tint", () => { const { container } = render(); expect(container.querySelector(".text-amber-300")).toBeInTheDocument(); const { container: calm } = render(); expect(calm.querySelector(".text-amber-300")).not.toBeInTheDocument(); }); it("compact mode suppresses the inline chip but keeps the hover tooltip", () => { const { container } = render( ); expect(screen.getByText("Waiting")).toBeInTheDocument(); expect(screen.queryByText("Needs input")).not.toBeInTheDocument(); fireEvent.mouseEnter(container.firstElementChild!, { clientX: 10, clientY: 10 }); expect(screen.getByText(/Blocked on a permission prompt/)).toBeInTheDocument(); }); it("compact mode works on SessionStatusBadge too", () => { render(); expect(screen.getByText("Waiting")).toBeInTheDocument(); expect(screen.queryByText("Turn done")).not.toBeInTheDocument(); }); });