Files
nxdns/admin/src/features/clients/ClientDetailPage.test.tsx
T
mokhtar 7e0df5fd94
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
milestone 32: task-shaped configuration, file mode as a rendering, config status api
2026-08-22 22:42:50 +02:00

108 lines
4.5 KiB
TypeScript

import { fireEvent, screen, waitFor, within } from "@testing-library/react";
import { BASE, MANAGED_FILE, NEVER, renderAt } from "./testFixtures";
afterEach(() => {
vi.unstubAllGlobals();
});
test("a cold deep link fetches the list once and renders the client (D9)", async () => {
const { fetchMock } = renderAt("/clients/1", BASE);
await screen.findByRole("heading", { name: "laptop" });
expect(screen.getByText("192.168.1.10")).toBeTruthy();
const listCalls = fetchMock.mock.calls.filter(([input]) => String(input) === "/api/clients");
expect(listCalls).toHaveLength(1);
});
test("identity carries the name, its provenance and both timestamps", async () => {
renderAt("/clients/2", BASE);
// The unnamed row is the learned one, and Clients is where the marker shows.
const heading = await screen.findByRole("heading", { name: /kids-tablet\.lan/ });
expect(within(heading).getByText("learned")).toBeTruthy();
expect(screen.getByText(/This client appeared from DNS traffic/)).toBeTruthy();
expect(screen.getByText("First seen")).toBeTruthy();
expect(screen.getByText("Last seen")).toBeTruthy();
});
test("a hand-set row reads as edited under database authority", async () => {
renderAt("/clients/1", BASE);
await waitFor(() => expect(screen.getByText(/The name and group were set here/)).toBeTruthy());
});
test("the same row reads as declared under file authority", async () => {
renderAt("/clients/1", { ...BASE, "GET /api/config/status": MANAGED_FILE });
await screen.findByText(/Declared in \/etc\/nxdns\/config\.zon/);
});
test("an unresolved authority names the doubt instead of picking a provenance", async () => {
renderAt("/clients/1", { ...BASE, "GET /api/config/status": NEVER });
await screen.findByText(/Either declared in the configuration file or edited here/);
});
test("an id the list does not contain renders the missing-client state (D9)", async () => {
renderAt("/clients/99", BASE);
await screen.findByRole("heading", { name: "No such client" });
expect(screen.getByText(/no client with id 99/i)).toBeTruthy();
expect(screen.getByRole("link", { name: "← All clients" })).toBeTruthy();
});
test("policy links to the group that filters this client", async () => {
renderAt("/clients/2", BASE);
const link = await screen.findByRole("link", { name: "Group settings" });
expect(link.getAttribute("href")).toBe("/configuration/protection?tab=groups&group=2");
});
test("the Activity affordance is a real link, with bounds minted at interaction time", async () => {
const { router } = renderAt("/clients/1", BASE);
const link = await screen.findByRole("link", { name: /last 24 hours/i });
// A real <a href> is what makes middle-click, copy-link and open-in-new-tab
// work; the bounds must be in that href before the click, not minted by a
// handler the browser never runs on those gestures.
expect(link.tagName).toBe("A");
expect(link.getAttribute("href")).toMatch(/^\/activity\?/);
const before = Math.floor(Date.now() / 1000);
fireEvent.pointerDown(link);
fireEvent.click(link);
const after = Math.floor(Date.now() / 1000);
await waitFor(() => expect(router.state.location.pathname).toBe("/activity"));
const search = router.state.location.search as { mode: string; client: string; since: number; until: number };
expect(search.mode).toBe("history");
expect(search.client).toBe("192.168.1.10");
expect(search.until).toBeGreaterThanOrEqual(before - 1);
expect(search.until).toBeLessThanOrEqual(after + 1);
expect(search.until - search.since).toBe(24 * 60 * 60);
});
test("keyboard activation re-mints the bounds, not the focus that preceded it by hours", async () => {
const { router } = renderAt("/clients/1", BASE);
const link = await screen.findByRole("link", { name: /last 24 hours/i });
// Focus can be hours old by the time Enter lands, so the window it minted is
// stale; the keypress itself is what must be bracketed.
fireEvent.focus(link);
const pressedAt = Date.now() + 3 * 60 * 60 * 1000;
const clock = vi.spyOn(Date, "now").mockReturnValue(pressedAt);
try {
fireEvent.keyDown(link, { key: "Enter" });
// jsdom does not synthesize the click Enter produces on a real anchor.
fireEvent.click(link);
} finally {
clock.mockRestore();
}
await waitFor(() => expect(router.state.location.pathname).toBe("/activity"));
const search = router.state.location.search as { client: string; since: number; until: number };
expect(search.client).toBe("192.168.1.10");
expect(search.until).toBe(Math.floor(pressedAt / 1000));
expect(search.until - search.since).toBe(24 * 60 * 60);
});