Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s
199 lines
7.9 KiB
TypeScript
199 lines
7.9 KiB
TypeScript
/**
|
|
* 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<void> | 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(
|
|
<AuthProvider>
|
|
<QueryClientProvider client={queryClient}>
|
|
<RouterProvider router={router} />
|
|
</QueryClientProvider>
|
|
</AuthProvider>,
|
|
);
|
|
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<void>((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());
|
|
});
|