/** * @vitest-environment happy-dom */ import type { ReactNode } from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; // Mock next-intl vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useTimeZone: () => "UTC", })); // Mock recharts to expose color props via data-* attributes vi.mock("recharts", async (importOriginal) => { const React = await import("react"); const actual = await importOriginal(); return { ...actual, ResponsiveContainer: ({ children }: { children: ReactNode }) => React.createElement("div", { "data-testid": "recharts-responsive" }, children), AreaChart: ({ children, data }: { children: ReactNode; data: unknown[] }) => React.createElement( "div", { "data-testid": "recharts-areachart", "data-points": data?.length || 0 }, children ), Area: ({ stroke, fill, dataKey }: { stroke: string; fill: string; dataKey: string }) => React.createElement("div", { "data-testid": `recharts-area-${dataKey}`, "data-stroke": stroke, "data-fill": fill, }), CartesianGrid: () => null, XAxis: () => null, YAxis: () => null, }; }); // Mock chart.tsx to expose ChartStyle for testing vi.mock("@/components/ui/chart", async () => { const React = await import("react"); const actual = await vi.importActual("@/components/ui/chart"); return { ...actual, ChartContainer: ({ children, config, className, }: { children: ReactNode; config: Record; className?: string; }) => React.createElement( "div", { "data-testid": "chart-container", "data-config": JSON.stringify(config), className, }, children ), ChartTooltip: () => null, }; }); import { LatencyChart } from "@/app/[locale]/dashboard/availability/_components/provider/latency-chart"; import type { ProviderAvailabilitySummary } from "@/lib/availability"; const mockProviders: ProviderAvailabilitySummary[] = [ { provider: "test-provider", uptime: 99.5, totalRequests: 100, successRequests: 99, failedRequests: 1, avgLatencyMs: 150, p50LatencyMs: 120, p95LatencyMs: 200, p99LatencyMs: 350, timeBuckets: [ { bucketStart: "2024-01-01T10:00:00Z", totalRequests: 50, successRequests: 49, failedRequests: 1, avgLatencyMs: 140, p50LatencyMs: 110, p95LatencyMs: 190, p99LatencyMs: 340, }, { bucketStart: "2024-01-01T11:00:00Z", totalRequests: 50, successRequests: 50, failedRequests: 0, avgLatencyMs: 160, p50LatencyMs: 130, p95LatencyMs: 210, p99LatencyMs: 360, }, ], }, ]; function renderComponent(providers: ProviderAvailabilitySummary[]) { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); act(() => { root.render(); }); return { container, unmount: () => { act(() => root.unmount()); container.remove(); }, }; } describe("LatencyChart color bindings", () => { beforeEach(() => { document.body.innerHTML = ""; }); afterEach(() => { vi.clearAllMocks(); }); test("ChartConfig uses var(--chart-*) without hsl wrapper", () => { const { container, unmount } = renderComponent(mockProviders); const chartContainer = container.querySelector('[data-testid="chart-container"]'); expect(chartContainer).toBeTruthy(); const configStr = chartContainer?.getAttribute("data-config"); expect(configStr).toBeTruthy(); const config = JSON.parse(configStr!); // Colors should use var(--chart-*) directly, NOT hsl(var(--chart-*)) expect(config.p50.color).toBe("var(--chart-2)"); expect(config.p95.color).toBe("var(--chart-4)"); expect(config.p99.color).toBe("var(--chart-1)"); // Ensure no hsl wrapper expect(config.p50.color).not.toMatch(/^hsl\(/); expect(config.p95.color).not.toMatch(/^hsl\(/); expect(config.p99.color).not.toMatch(/^hsl\(/); unmount(); }); test("Area stroke uses var(--color-) CSS variable", () => { const { container, unmount } = renderComponent(mockProviders); const areaP50 = container.querySelector('[data-testid="recharts-area-p50"]'); const areaP95 = container.querySelector('[data-testid="recharts-area-p95"]'); const areaP99 = container.querySelector('[data-testid="recharts-area-p99"]'); expect(areaP50).toBeTruthy(); expect(areaP95).toBeTruthy(); expect(areaP99).toBeTruthy(); // Stroke should use var(--color-) injected by ChartContainer expect(areaP50?.getAttribute("data-stroke")).toBe("var(--color-p50)"); expect(areaP95?.getAttribute("data-stroke")).toBe("var(--color-p95)"); expect(areaP99?.getAttribute("data-stroke")).toBe("var(--color-p99)"); unmount(); }); test("Area fill references gradient with correct ID pattern", () => { const { container, unmount } = renderComponent(mockProviders); const areaP50 = container.querySelector('[data-testid="recharts-area-p50"]'); const areaP95 = container.querySelector('[data-testid="recharts-area-p95"]'); const areaP99 = container.querySelector('[data-testid="recharts-area-p99"]'); // Fill should reference gradient URL expect(areaP50?.getAttribute("data-fill")).toMatch(/url\(#fillP50\)/); expect(areaP95?.getAttribute("data-fill")).toMatch(/url\(#fillP95\)/); expect(areaP99?.getAttribute("data-fill")).toMatch(/url\(#fillP99\)/); unmount(); }); test("renders no data message when providers have no time buckets with requests", () => { const emptyProviders: ProviderAvailabilitySummary[] = [ { provider: "empty-provider", uptime: 0, totalRequests: 0, successRequests: 0, failedRequests: 0, avgLatencyMs: 0, p50LatencyMs: 0, p95LatencyMs: 0, p99LatencyMs: 0, timeBuckets: [], }, ]; const { container, unmount } = renderComponent(emptyProviders); // Should show no data message expect(container.textContent).toContain("noData"); // Should not render chart expect(container.querySelector('[data-testid="chart-container"]')).toBeNull(); unmount(); }); });