/** * The health strip on the Diagnostics page, through the real router. * * Its load contract is migrated whole from the deleted Overview status section: * a visible loading state before the first reading, an error row with Retry when * the first read fails, and a refetch failure that marks the conditions on * screen as the last reading rather than the current state. What is new is that * a condition explained on this page narrows this page instead of navigating. */ import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { QueryClientProvider } from "@tanstack/react-query"; import { RouterProvider, createMemoryHistory } from "@tanstack/react-router"; import { AuthProvider } from "@/auth/store"; import { createQueryClient } from "@/lib/queryClient"; import { createAppRouter } from "@/routes"; import { health } from "@/lib/healthFixture"; import type { Health } from "@/lib/types"; let healthBody: Health; let healthFails: boolean; let requested: string[]; /** Held open to keep a health request in flight while a test looks at the strip. */ let pendingHealth: Promise | null; function json(payload: unknown, status = 200): Response { return new Response(JSON.stringify(payload), { status, headers: { "content-type": "application/json" } }); } beforeEach(() => { healthBody = health(); healthFails = false; requested = []; pendingHealth = null; vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); requested.push(url); if (url === "/api/health") { if (pendingHealth !== null) await pendingHealth; return healthFails ? json({ error: "health unavailable" }, 400) : json(healthBody); } if (url === "/api/version") return json({ version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 }); if (url.startsWith("/api/diagnostics")) return json({ events: [], next_before: null, active: { warnings: 0, errors: 0 } }); return json({ error: "not stubbed" }, 404); }), ); }); afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); }); function renderDiagnostics(path = "/diagnostics") { const queryClient = createQueryClient(); const defaults = queryClient.getDefaultOptions(); queryClient.setDefaultOptions({ ...defaults, queries: { ...defaults.queries, retry: false } }); const router = createAppRouter(createMemoryHistory({ initialEntries: [path] }), queryClient); render( , ); return { router, queryClient }; } function strip(): HTMLElement { return screen.getByRole("list", { name: "Current status" }); } function fact(label: string): HTMLElement { // First match, not only match: a condition's label and the link it offers can // be the same word — Upstreams links to Upstreams — and the label comes first. const cell = within(strip()).getAllByText(label)[0]; const item = cell.closest("li"); if (item === null) throw new Error(`no health fact for ${label}`); return item; } test("the five conditions are stated in words, healthy ones without a way out", async () => { renderDiagnostics(); await waitFor(() => expect(strip()).toBeTruthy()); for (const [label, value] of [ ["Protection", "Active"], ["Upstreams", "Available"], ["Query history", "Recording"], ["Diagnostics", "Recording"], ["Storage", "OK"], ] as const) { expect(within(fact(label)).getByText(value)).toBeTruthy(); } expect(within(strip()).queryByRole("link")).toBeNull(); }); test("protection unavailable sends the reader to the sources, upstreams to the pool", async () => { healthBody = health({ protection: { state: "unavailable", until: null }, upstreams: { state: "unavailable", available: 0, total: 2 }, }); renderDiagnostics(); await waitFor(() => expect(strip()).toBeTruthy()); expect(within(fact("Protection")).getByRole("link", { name: "Blocklist sources" }).getAttribute("href")).toBe( "/configuration/protection?tab=sources", ); expect(within(fact("Upstreams")).getByRole("link", { name: "Upstreams" }).getAttribute("href")).toBe( "/configuration/resolution?tab=upstreams", ); }); test("a losing query log narrows this page to the disk, a failed writer to the query log", async () => { healthBody = health({ query_history: { state: "losing", dropped_total: 4, last_drop_s: null } }); const { router } = renderDiagnostics(); await waitFor(() => expect(strip()).toBeTruthy()); expect(within(fact("Query history")).getByText("4 queries dropped")).toBeTruthy(); fireEvent.click(within(fact("Query history")).getByRole("link", { name: "Disk diagnostics" })); await waitFor(() => expect(router.state.location.search).toEqual({ component: "disk" })); await waitFor(() => expect(requested.some((url) => url.includes("component=disk"))).toBe(true)); }); test("a filter link drops a time window that would hide the episodes it points at", async () => { healthBody = health({ disk: { state: "critical", free_bytes: 0 } }); const { router } = renderDiagnostics("/diagnostics?since=1000&until=2000&severity=error&state=resolved"); await waitFor(() => expect(strip()).toBeTruthy()); fireEvent.click(within(fact("Storage")).getByRole("link", { name: "Disk diagnostics" })); // Everything that could hide the episode goes with the bounds: `state=resolved` // would exclude the active disk episode this link exists to show, and an // `error` severity would exclude it whenever it is a warning. await waitFor(() => expect(router.state.location.search).toEqual({ component: "disk" })); }); test("an unavailable diagnostics store explains itself and offers no link into itself", async () => { healthBody = health({ diagnostics: { state: "unavailable", active_warnings: 0, active_errors: 0 } }); renderDiagnostics(); await waitFor(() => expect(strip()).toBeTruthy()); const row = fact("Diagnostics"); expect(within(row).getByText(/not being recorded/)).toBeTruthy(); expect(within(row).queryByRole("link")).toBeNull(); }); test("the strip says it is loading before the first reading, never empty conditions", async () => { // Through the route, which is the path that matters: the loader starts the // health request without waiting for it, so the page paints while the reading // is still in flight and the strip has to say so. let release = () => {}; pendingHealth = new Promise((resolve) => { release = resolve; }); renderDiagnostics(); expect(await screen.findByText("Loading status…")).toBeTruthy(); // Scoped to the page: "Protection" is also a nav destination now. expect(within(document.querySelector("main") as HTMLElement).queryByText("Protection")).toBeNull(); release(); await waitFor(() => expect(strip()).toBeTruthy()); expect(screen.queryByText("Loading status…")).toBeNull(); }); test("a failed first health read is an error row with Retry, not a healthy strip", async () => { healthFails = true; renderDiagnostics(); await screen.findByText("health unavailable"); expect(screen.queryByRole("list", { name: "Current status" })).toBeNull(); }); test("a reading that has gone stale says so rather than passing for current", async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); renderDiagnostics(); await vi.waitFor(() => expect(strip()).toBeTruthy()); expect(within(fact("Storage")).getByText("OK")).toBeTruthy(); // The next poll fails. The conditions on screen are the last that arrived and // must not keep passing for the current state. healthFails = true; await vi.advanceTimersByTimeAsync(11_000); await vi.waitFor(() => expect(screen.getByText(/last reading that arrived/)).toBeTruthy()); expect(within(fact("Storage")).getByText("OK")).toBeTruthy(); // Recovery clears the caption rather than leaving the page permanently unsure. healthFails = false; await vi.advanceTimersByTimeAsync(11_000); await vi.waitFor(() => expect(screen.queryByText(/last reading that arrived/)).toBeNull()); });