import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { GraphPage } from "../src/components/GraphPage.js"; function stubApi(responses: Record Response>): void { vi.stubGlobal( "fetch", vi.fn((input: RequestInfo | URL) => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; const path = url.split("?")[0] ?? url; return Promise.resolve(responses[path]?.() ?? new Response("missing", { status: 404 })); }), ); } beforeEach(() => { const context = { setTransform: vi.fn(), clearRect: vi.fn(), translate: vi.fn(), scale: vi.fn(), beginPath: vi.fn(), moveTo: vi.fn(), lineTo: vi.fn(), stroke: vi.fn(), arc: vi.fn(), fill: vi.fn(), fillText: vi.fn(), } as unknown as CanvasRenderingContext2D; vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(context); vi.stubGlobal( "requestAnimationFrame", vi.fn(() => 1), ); vi.stubGlobal("cancelAnimationFrame", vi.fn()); }); afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.restoreAllMocks(); window.history.replaceState(null, "", "/"); }); describe("GraphPage", () => { it("renders the reusable force graph and sorted approved free-label index", async () => { stubApi({ "/api/graph": () => Response.json({ nodes: [ { name: "vllm", unit_count: 3 }, { name: "sglang", unit_count: 2 }, ], links: [{ source: "vllm", target: "sglang", weight: 2 }], }), "/api/free-labels": () => Response.json({ free_labels: [ { name: "sglang", count: 2 }, { name: "vllm", count: 3 }, ], }), }); render(); await screen.findByText("All approved free labels"); expect(screen.getByLabelText("Force-directed graph of approved free labels")).toBeDefined(); const links = screen.getAllByRole("link"); expect(links.map((link) => link.textContent)).toEqual(["vllm", "sglang"]); const [firstLink] = links; if (firstLink === undefined) throw new Error("expected free-label link"); fireEvent.click(firstLink); expect(window.location.pathname).toBe("/graph/vllm"); }); it("keeps candidates out of the graph by showing the empty public state", async () => { stubApi({ "/api/graph": () => Response.json({ nodes: [], links: [] }), "/api/free-labels": () => Response.json({ free_labels: [] }), }); render(); await screen.findByText("No approved free labels yet."); }); it("surfaces graph request errors", async () => { stubApi({}); render(); await screen.findByText(/request failed: 404/); }); it("leaves modified index clicks to the browser", async () => { stubApi({ "/api/graph": () => Response.json({ nodes: [], links: [] }), "/api/free-labels": () => Response.json({ free_labels: [{ name: "vllm", count: 1 }] }), }); render(); const label = await screen.findByText("vllm"); const event = new MouseEvent("click", { bubbles: true, cancelable: true, ctrlKey: true }); event.preventDefault(); label.dispatchEvent(event); expect(window.location.pathname).toBe("/"); }); });