milestone 30: overview as a dashboard, explicit health contract, period aggregations
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 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 Blocklists, upstreams to Upstreams", 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: "Blocklists" }).getAttribute("href")).toBe(
|
||||
"/blocklists",
|
||||
);
|
||||
expect(within(fact("Upstreams")).getByRole("link", { name: "Upstreams" }).getAttribute("href")).toBe("/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();
|
||||
expect(screen.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());
|
||||
});
|
||||
Reference in New Issue
Block a user