/** * @file StatusBar.test.tsx * @description Tests for the shell's bottom status bar: the live/disconnected * state with the version (click opens the connection details), the running and * needs-you SESSION counts, today's spend (`date=` on the pricing cost call) * with its tokens and a per-model hover list, and on the Workspace page the * open workspace's and the shown session's totals; the right-end quota chips * (fullest window per tool) and their hover panel. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi } from "vitest"; import { act, render, screen, waitFor } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import userEvent from "@testing-library/user-event"; const M = 1_000_000; function today() { const d = new Date(); const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } vi.mock("../../lib/api", () => ({ api: { aiTools: { iconUrl: (id: string) => `/api/ai-tools/${id}/icon` }, sessions: { // 3 active sessions, 1 of them waiting on the user. list: vi.fn().mockImplementation(async (p: { awaiting?: boolean }) => ({ sessions: [], total: p.awaiting ? 1 : 3, })), }, pricing: { totalCost: vi.fn().mockImplementation(async (opts?: { lane?: number; date?: string }) => ({ total_cost: opts?.lane ? 123.456 : opts?.date === today() ? 1.234 : 10, breakdown: opts?.lane ? [] : [ // Two buckets of one model fold into one line. { model: "claude-opus-4-8", input_tokens: 1 * M, output_tokens: 0, cache_read_tokens: 10 * M, cache_write_tokens: 0, cost: 1, matched_rule: null, }, { model: "claude-opus-4-8", speed: "fast", input_tokens: 0, output_tokens: 1 * M, cache_read_tokens: 0, cache_write_tokens: 0, cost: 0.2, matched_rule: null, }, { model: "claude-haiku-4-5", input_tokens: 300_000, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, cost: 0.034, matched_rule: null, }, ], daily_costs: [], })), quota: vi.fn().mockResolvedValue({ tools: [ { id: "claude", active: true, label: "Work", account: "me@example.com", plan: "max", expiresAt: new Date(Date.now() + 3 * 3600_000 + 30_000).toISOString(), windows: [ { label: "5h", usedPercent: 95, resetsAt: new Date(Date.now() + 41 * 60_000 + 30_000).toISOString(), }, { label: "wk", usedPercent: 32, resetsAt: null }, ], }, { id: "claude", active: false, label: "Personal", account: "home@example.com", expiresAt: "2020-01-01T00:00:00.000Z", windows: [], error: "EPROFILEEXPIRED", }, { id: "codex", account: "dev@example.com", expiresAt: "2020-01-01T00:00:00.000Z", windows: [], error: "EUPSTREAM", }, { id: "antigravity", active: true, windows: [ { group: "Gemini Models", label: "5h", usedPercent: 0, resetsAt: null }, { group: "Gemini Models", label: "wk", usedPercent: 25, resetsAt: null }, { group: "Claude and GPT models", label: "5h", usedPercent: 50, resetsAt: null }, ], }, { id: "cursor", active: true, account: "c@example.com", plan: "pro", windows: [{ label: "mo", usedPercent: 40, resetsAt: null }], }, ], checkedAt: "", }), sessionCost: vi.fn().mockImplementation(async (id: string) => ({ total_cost: id === "s-live" ? 4.5 : 7, breakdown: [], daily_costs: [], })), }, budgets: { status: vi.fn().mockResolvedValue({ budgets: [ { id: "b1", name: "Quiet", config: { limit_usd: 100 }, spend: 10, pct: 10, crossed: [] }, { id: "b2", name: "Monthly", config: { limit_usd: 100 }, spend: 85, pct: 85, crossed: [80], }, ], }), }, }, })); import { api } from "../../lib/api"; import { StatusBar } from "../StatusBar"; import { LIVE_RUNS_EVENT } from "../WorkspaceTree"; const at = (url: string, ui: JSX.Element) => render({ui}); describe("StatusBar", () => { it("shows the connection with the version, the counts and today's spend", async () => { const user = userEvent.setup(); const onConnectionClick = vi.fn(); at("/", ); const conn = screen.getByRole("button", { name: /Live/ }); expect(conn).toHaveTextContent(`v${__APP_VERSION__}`); const cost = await screen.findByText("Today $1.23 (12.3M Tokens)"); expect(api.pricing.totalCost).toHaveBeenCalledWith({ date: today() }); expect(cost).toHaveAttribute( "title", "Claude Opus 4.8: 12.0M · $1.20\nClaude Haiku 4.5: 300.0K · $0.03" ); await screen.findByText("1 need you"); expect(screen.getByTestId("status-bar-counts").textContent).toBe("2 running · 1 need you"); await user.click(conn); expect(onConnectionClick).toHaveBeenCalled(); // Polite live region: the connection and who is waiting, nothing that ticks. expect(screen.getByRole("status")).toHaveTextContent("Live · 1 need you"); }); it("flags the budget furthest past a threshold, linking to the cost analytics", async () => { at("/", {}} />); const chip = await screen.findByTestId("status-bar-budget"); expect(chip.textContent).toBe("Budget 85%"); expect(chip.getAttribute("href")).toBe("/?tab=analytics&sub=cost"); expect(chip.getAttribute("title")).toBe("Monthly: $85.00 of $100.00 this period"); }); it("says Disconnected when the socket is down", () => { at("/", {}} />); expect(screen.getByRole("button", { name: /Disconnected/ })).toBeInTheDocument(); expect(screen.getByRole("status")).toHaveTextContent("Disconnected"); }); it("off the Workspace page shows no workspace or session cost", async () => { at("/sessions", {}} />); await screen.findByText(/^Today \$1\.23/); expect(screen.queryByTestId("status-bar-workspace-cost")).toBeNull(); expect(screen.queryByTestId("status-bar-session-cost")).toBeNull(); }); it("shows the open workspace's total and the replayed session's", async () => { at( "/workspace?lane=3&session=s-old", {}} /> ); expect(await screen.findByText("Workspace $123.46 (0 Tokens)")).toBeInTheDocument(); expect(await screen.findByText(/^Session \$7\.00/)).toBeInTheDocument(); expect(api.pricing.totalCost).toHaveBeenCalledWith({ lane: 3 }); }); it("takes a focused pane's session from the page's run poll", async () => { at( "/workspace?lane=3&run=agenthub-lane-3-1", {}} /> ); await screen.findByText(/^Workspace \$123\.46/); expect(screen.queryByTestId("status-bar-session-cost")).toBeNull(); act(() => { window.dispatchEvent( new CustomEvent(LIVE_RUNS_EVENT, { detail: [{ id: "agenthub-lane-3-1", laneId: 3, status: "running", sessionId: "s-live" }], }) ); }); expect(await screen.findByText(/^Session \$4\.50/)).toBeInTheDocument(); }); it("shows each tool's fullest quota window and the details on hover", async () => { const user = userEvent.setup(); at("/", {}} />); const chips = await screen.findByTestId("status-bar-quota"); expect(chips).toHaveTextContent("Claude95% used"); expect(chips.textContent?.match(/Claude/g)).toHaveLength(1); expect(screen.getByText("95% used")).toHaveClass("text-status-danger"); await user.hover(chips); const panel = screen.getByTestId("status-bar-quota-panel"); const [work, personal, codex, agy, cursor] = screen.getAllByTestId("status-bar-quota-account"); expect(work).toHaveTextContent("Workmaxin use"); expect(work).toHaveTextContent("me@example.com · Sign-in expires in 3h 0m"); expect(work).toHaveTextContent("5h95%Resets in 41m"); expect(work).toHaveTextContent("Week32%"); expect(personal).toHaveTextContent("home@example.com · Sign-in expired"); expect(personal).toHaveTextContent("Check quota in Claude Code → Profiles"); expect(personal).not.toHaveTextContent("in use"); expect(codex).toHaveTextContent("dev@example.com"); expect(codex).toHaveTextContent("Sign-in expired"); expect(panel).toHaveTextContent("Could not read the quota"); expect(agy).toHaveTextContent("Gemini Models5h0%Week25%Claude and GPT models5h50%"); expect(chips).toHaveTextContent("Antigravity50% used"); expect(cursor).toHaveTextContent("c@example.compro"); expect(cursor).toHaveTextContent("Month40%"); expect(chips).toHaveTextContent("Cursor40% used"); // Moving onto the panel keeps it open, so it can be read; leaving it closes it. await user.unhover(chips); await user.hover(panel); await new Promise((r) => setTimeout(r, 300)); expect(screen.getByTestId("status-bar-quota-panel")).toBeInTheDocument(); // Only the footer button refreshes: it spins and says so until the read lands. const same = await api.pricing.quota(false); let answer!: (v: unknown) => void; vi.mocked(api.pricing.quota) .mockClear() .mockReturnValueOnce(new Promise((r) => (answer = r)) as never); const refresh = screen.getByTestId("status-bar-quota-refresh"); await user.click(refresh); expect(api.pricing.quota).toHaveBeenLastCalledWith(true); expect(refresh).toHaveTextContent("Refreshing…"); expect(refresh).toHaveAttribute("aria-busy", "true"); await user.click(refresh); expect(api.pricing.quota).toHaveBeenCalledTimes(1); await act(async () => answer(same)); // The spin outlasts an instant answer (a throttled refresh is served from // cache), so the label returns after the minimum spin. await waitFor(() => expect(screen.getByTestId("status-bar-quota-refresh")).toHaveTextContent(/^Refresh$/) ); expect(screen.getByTestId("status-bar-quota-panel")).toBeInTheDocument(); await user.unhover(panel); await waitFor(() => expect(screen.queryByTestId("status-bar-quota-panel")).toBeNull()); vi.mocked(api.pricing.quota).mockClear(); await user.click(chips); expect(api.pricing.quota).toHaveBeenLastCalledWith(true); }); });