Files
AgentHub/client/src/components/__tests__/StatusBadge.test.tsx
T
nntrivi2001 03f1673fce feat(dashboard): lazy routes, Needs-you strip, URL state, exact numbers
- Workspace, SettingsHub, SessionDetail and the Workflow sub-tab are lazy
  (main bundle 2,166 kB -> ~920 kB), wrapped in RouteErrorBoundary: a stale
  chunk after a self-update reloads once (vite:preloadError too), any other
  error shows a Reload panel instead of a blank page.
- Live refresh: lib/throttle.ts + hooks/useLiveRefresh.ts replace trailing
  debounces that froze under a steady event stream; refresh skips hidden
  tabs and refetches after a WebSocket reconnect. Monitor loads only while
  shown, with one batched GET /api/agents?session_ids= instead of N+1; a
  failing endpoint is an inline error, not a blank Dashboard.
- NeedsYouStrip on Monitor and above Analytics. Tabs, filters, page, sort
  and search live in the URL (Dashboard, Sessions, Activity, SessionDetail).
- Sessions: tool / workspace / date-range filters, CSV/JSON export
  (formula-neutralised), focusable row links, Open in workspace (the dead
  /run links are gone), stale responses dropped, error + Retry.
- Fixes: "NaNs" durations, 30/7-day charts skipping idle days, weekday
  labels west of UTC (now averages), cache hit rate denominator, billable
  tokens vs cache reads, exact tool-call counts, "To" date = end of day,
  invalid ?from= no longer throws, recent list no longer shrinks to 15,
  stable message keys, transcript switch races.
- Light-theme-unreadable bg-black code panels and dark-only DAG fills use
  tokens; status configs and chart colours use theme tokens; Health tab
  and other literals translated.
- Server (additive): GET /api/agents?session_ids=, GET /api/sessions
  awaiting=1&from=&to=, stats tool_call_count/distinct_tool_count, event
  rows carry session/agent labels.
2026-10-05 13:22:04 +07:00

164 lines
7.2 KiB
TypeScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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(<AgentStatusBadge status="waiting" />);
expect(screen.getByText("Waiting")).toBeInTheDocument();
});
it("should render working status", () => {
render(<AgentStatusBadge status="working" />);
expect(screen.getByText("Working")).toBeInTheDocument();
});
it("should render completed status", () => {
render(<AgentStatusBadge status="completed" />);
expect(screen.getByText("Completed")).toBeInTheDocument();
});
it("should render error status", () => {
render(<AgentStatusBadge status="error" />);
expect(screen.getByText("Error")).toBeInTheDocument();
});
it("should apply pulse animation for working status by default", () => {
const { container } = render(<AgentStatusBadge status="working" />);
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(<AgentStatusBadge status="working" />);
const dot = container.querySelector(".animate-pulse-dot");
expect(dot).toBeInTheDocument();
});
it("should apply pulse animation for waiting status by default", () => {
const { container } = render(<AgentStatusBadge status="waiting" />);
const dot = container.querySelector(".animate-pulse-dot");
expect(dot).toBeInTheDocument();
});
it("should respect explicit pulse=false override", () => {
const { container } = render(<AgentStatusBadge status="working" pulse={false} />);
const dot = container.querySelector(".animate-pulse-dot");
expect(dot).not.toBeInTheDocument();
});
it("should respect explicit pulse=true override", () => {
const { container } = render(<AgentStatusBadge status="waiting" pulse={true} />);
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(<AgentStatusBadge status="waiting" />);
expect(screen.getByText("Waiting")).toBeInTheDocument();
const dot = container.querySelector(".animate-pulse-dot");
expect(dot).toBeInTheDocument();
expect(container.querySelector(".bg-status-warning")).toBeInTheDocument();
});
});
describe("SessionStatusBadge", () => {
it("should render active status", () => {
render(<SessionStatusBadge status="active" />);
expect(screen.getByText("Active")).toBeInTheDocument();
});
it("should render completed status", () => {
render(<SessionStatusBadge status="completed" />);
expect(screen.getByText("Completed")).toBeInTheDocument();
});
it("should render error status", () => {
render(<SessionStatusBadge status="error" />);
expect(screen.getByText("Error")).toBeInTheDocument();
});
it("should render abandoned status", () => {
render(<SessionStatusBadge status="abandoned" />);
expect(screen.getByText("Abandoned")).toBeInTheDocument();
});
it("should render waiting status with pulsing yellow dot", () => {
const { container } = render(<SessionStatusBadge status="waiting" />);
expect(screen.getByText("Waiting")).toBeInTheDocument();
const dot = container.querySelector(".animate-pulse-dot");
expect(dot).toBeInTheDocument();
expect(container.querySelector(".bg-status-warning")).toBeInTheDocument();
});
});
describe("awaiting-reason suffix", () => {
it("renders the reason label next to Waiting on AgentStatusBadge", () => {
render(<AgentStatusBadge status="waiting" reason="notification" />);
expect(screen.getByText("Waiting")).toBeInTheDocument();
expect(screen.getByText("Needs input")).toBeInTheDocument();
});
it("renders the reason label next to Waiting on SessionStatusBadge", () => {
render(<SessionStatusBadge status="waiting" reason="stop" />);
expect(screen.getByText("Waiting")).toBeInTheDocument();
expect(screen.getByText("Turn done")).toBeInTheDocument();
});
it("ignores the reason on non-waiting statuses", () => {
render(<AgentStatusBadge status="working" reason="notification" />);
expect(screen.queryByText("Needs input")).not.toBeInTheDocument();
render(<SessionStatusBadge status="active" reason="stop" />);
expect(screen.queryByText("Turn done")).not.toBeInTheDocument();
});
it("renders no suffix when reason is null/omitted", () => {
render(<AgentStatusBadge status="waiting" reason={null} />);
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(<AgentStatusBadge status="waiting" reason="interrupted" />);
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", () => {
render(<AgentStatusBadge status="waiting" reason="notification" />);
expect(screen.getByText("Needs input").closest("span.rounded-full")).toHaveClass(
"text-status-warning"
);
render(<AgentStatusBadge status="waiting" reason="stop" />);
expect(screen.getByText("Turn done").closest("span.rounded-full")).not.toHaveClass(
"text-status-warning"
);
});
it("compact mode suppresses the inline chip but keeps the hover tooltip", () => {
const { container } = render(
<AgentStatusBadge status="waiting" reason="notification" compact />
);
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(<SessionStatusBadge status="waiting" reason="stop" compact />);
expect(screen.getByText("Waiting")).toBeInTheDocument();
expect(screen.queryByText("Turn done")).not.toBeInTheDocument();
});
});