milestone 32: task-shaped configuration, file mode as a rendering, config status api
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
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
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user