Release / guard (push) Successful in 39s
Gates / frontend (push) Failing after 2m25s
Gates / package (push) Skipped
Gates / container (push) Skipped
Gates / frontend (push) Failing after 1m59s
Gates / package (push) Skipped
Gates / container (push) Skipped
Gates / test-aarch64 (push) Successful in 8m18s
CI / gates (push) Failing after 11m34s
Gates / test (push) Successful in 2m18s
Gates / test-aarch64 (push) Successful in 7m23s
Release / gates (push) Failing after 18m51s
Release / publish (push) Skipped
Gates / test (push) Successful in 2m36s
health strip marks, menu ticks, chip crosses, the search magnifier, the checkbox check and the back arrows all come from @phosphor-icons/react now, pinned exactly and entered in the license ledger. changelog and version bump for 0.0.15, including the bundle budget raise to 900000 bytes.
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);
|
|
});
|