import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, expect, it, vi } from "vitest"; import { ANALYTICS_GROUPS, EMPTY_PANELS, TIMES } from "../test/analytics-fixtures.ts"; import { serve } from "../test/http.ts"; import { EMPTY_ROUTES, ROUTES } from "./analytics-routes.ts"; const [ONLY_GROUP] = ANALYTICS_GROUPS.groups; import { Analytics } from "./analytics.tsx"; // The states that are not "a populated page": nothing recorded, exactly one // of something, and a history long enough to have a direction. Split out of // analytics.test.tsx so both stay under the 250-line cap. afterEach(() => { vi.unstubAllGlobals(); }); it("renders every panel against a bot that has never run", async () => { // The state a fresh install is in. Every panel has to say so rather than // draw an axis over nothing. serve(EMPTY_ROUTES); render(); await waitFor(() => { expect(screen.getByTestId("chart-commands-empty")).toBeInTheDocument(); }); expect(screen.getByTestId("no-upstream")).toBeInTheDocument(); expect(screen.getByTestId("no-demand")).toBeInTheDocument(); expect(screen.getByTestId("no-pools")).toBeInTheDocument(); expect(screen.getByTestId("chart-calendar-empty")).toBeInTheDocument(); expect(screen.getByTestId("tiles")).toHaveTextContent("—"); }); it("asks the server for one group when one is picked", async () => { // A group id is 44 characters of base64, so it is picked and never typed. const fetcher = serve(ROUTES); render(); await waitFor(() => { expect(screen.getByTestId("group-picker")).toBeInTheDocument(); }); await userEvent.selectOptions(screen.getByTestId("group-picker"), ONLY_GROUP?.id ?? ""); await waitFor(() => { const asked = fetcher.mock.calls.map((call) => String(call[0])); expect(asked.some((url) => url.includes("group="))).toBe(true); }); }); it("agrees with itself about one of anything", async () => { // Singular. The footnote reads as prose and "1 uses over 1 days" is the // sort of thing that gets noticed every single time. const one = { days: 1, enough_for_a_trend: false, events: 1, first_day: "2026-08-29", last_day: "2026-08-29", sources: { daily_uses: 0, journald: 0, live: 1 }, }; serve({ ...EMPTY_ROUTES, "/api/analytics/demand": { body: { ...EMPTY_PANELS.demand, near_misses: [{ hits: 1, target: "!kot", variants: 1 }], window: one, }, }, "/api/analytics/overview": { body: { ...EMPTY_PANELS.overview, window: one } }, }); render(); await waitFor(() => { expect(screen.getByTestId("sources")).toHaveTextContent("1 recorded use over 1 day"); }); expect(screen.getByTestId("demand")).toHaveTextContent("1 near miss"); expect(screen.getByTestId("demand")).toHaveTextContent("1 spelling"); }); it("states a direction once there are enough days to have one", async () => { const long = { days: 40, enough_for_a_trend: true, events: 900, first_day: "2026-07-01", last_day: "2026-08-31", sources: { daily_uses: 0, journald: 0, live: 900 }, }; serve({ ...EMPTY_ROUTES, "/api/analytics/overview": { body: { ...EMPTY_PANELS.overview, window: long } }, }); render(); await waitFor(() => { expect(screen.getByTestId("sources")).toHaveTextContent("900 recorded uses over 40 days"); }); expect(screen.getByTestId("sources")).not.toHaveTextContent("Too few days"); }); it("says nothing about dates before there are any", async () => { serve(EMPTY_ROUTES); render(); await waitFor(() => { expect(screen.getByTestId("sources")).toBeInTheDocument(); }); // No parenthesised range at all rather than an empty one. expect(screen.getByTestId("sources")).not.toHaveTextContent("()"); }); it("lists an upstream host and a pool when there is one of each", async () => { serve(ROUTES); render(); await waitFor(() => { expect(screen.getByTestId("upstream")).toHaveTextContent("commons.example.test"); }); expect(screen.getByTestId("sessions")).toHaveTextContent("10 sittings"); }); it("goes back to every group when the picker is cleared", async () => { const fetcher = serve(ROUTES); render(); await waitFor(() => { expect(screen.getByTestId("group-picker")).toBeInTheDocument(); }); const picker = screen.getByTestId("group-picker"); await userEvent.selectOptions(picker, ONLY_GROUP?.id ?? ""); await waitFor(() => { expect(fetcher.mock.calls.some((call) => String(call[0]).includes("group="))).toBe(true); }); fetcher.mockClear(); await userEvent.selectOptions(picker, ""); await waitFor(() => { expect(fetcher.mock.calls.length).toBeGreaterThan(0); }); // The roll-up asks for no group at all rather than for an empty one. expect(fetcher.mock.calls.every((call) => !String(call[0]).includes("group="))).toBe(true); }); it("labels a weekday grid that is longer than a week", async () => { // The server sends exactly seven rows. If it ever sent more, they get a // number rather than an undefined label -- which is what the index // fallback is for, and the only way to reach it. serve({ ...ROUTES, "/api/analytics/time": { body: { ...TIMES, weekday_hours: [...TIMES.weekday_hours, Array.from({ length: 24 }, () => 2)], }, }, }); render(); await waitFor(() => { expect(screen.getByTestId("chart-weekday-hours")).toBeInTheDocument(); }); expect(screen.getByText("7")).toBeInTheDocument(); });