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
108 lines
4.5 KiB
TypeScript
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);
|
|
});
|