162 lines
5.4 KiB
TypeScript
162 lines
5.4 KiB
TypeScript
import { fireEvent, render, screen, 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";
|
|
|
|
const GROUPS = {
|
|
groups: [
|
|
{ id: 1, name: "default", safe_search: false },
|
|
{ id: 2, name: "kids", safe_search: true },
|
|
],
|
|
};
|
|
|
|
const CLIENTS = {
|
|
clients: [
|
|
{
|
|
id: 1,
|
|
ip: "192.168.1.10",
|
|
name: "laptop",
|
|
learned_name: "laptop-1.lan",
|
|
group_id: 1,
|
|
group: "default",
|
|
hand_edited: true,
|
|
first_seen: 1700000000,
|
|
last_seen: 1700003600,
|
|
},
|
|
{
|
|
id: 2,
|
|
ip: "192.168.1.11",
|
|
name: "",
|
|
learned_name: "kids-tablet.lan",
|
|
group_id: 2,
|
|
group: "kids",
|
|
hand_edited: false,
|
|
first_seen: 1700000000,
|
|
last_seen: 1700007200,
|
|
},
|
|
],
|
|
};
|
|
|
|
const PREFIXES = {
|
|
client_prefixes: [{ id: 1, prefix: "192.168.1.0/24", group_id: 2, group: "kids", priority: 100 }],
|
|
};
|
|
|
|
const VERSION = { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 };
|
|
|
|
function stubFetch(map: Record<string, unknown>) {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const key = `${init?.method ?? "GET"} ${String(input)}`;
|
|
const payload = map[key];
|
|
if (payload === undefined) {
|
|
return new Response(JSON.stringify({ error: `not stubbed: ${key}` }), { status: 404 });
|
|
}
|
|
return new Response(JSON.stringify(payload), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function renderClientsPage(map: Record<string, unknown>) {
|
|
stubFetch(map);
|
|
const queryClient = createQueryClient();
|
|
const router = createAppRouter(createMemoryHistory({ initialEntries: ["/clients"] }), queryClient);
|
|
render(
|
|
<AuthProvider>
|
|
<QueryClientProvider client={queryClient}>
|
|
<RouterProvider router={router} />
|
|
</QueryClientProvider>
|
|
</AuthProvider>,
|
|
);
|
|
await screen.findByRole("heading", { name: "Clients" });
|
|
}
|
|
|
|
const BASE = {
|
|
"GET /api/clients": CLIENTS,
|
|
"GET /api/client-prefixes": PREFIXES,
|
|
"GET /api/groups": GROUPS,
|
|
"GET /api/version": VERSION,
|
|
};
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
test("renders the client table with group names and one hand-edited badge", async () => {
|
|
await renderClientsPage(BASE);
|
|
|
|
expect(screen.getByText("192.168.1.10")).toBeTruthy();
|
|
expect(screen.getByText("192.168.1.11")).toBeTruthy();
|
|
expect(screen.getByText("laptop")).toBeTruthy();
|
|
expect(screen.getAllByText("edited")).toHaveLength(1);
|
|
expect(screen.getAllByRole("cell", { name: "kids" })).toHaveLength(1);
|
|
});
|
|
|
|
test("a named row shows the typed name and hides the learned one", async () => {
|
|
await renderClientsPage(BASE);
|
|
|
|
expect(screen.getByText("laptop")).toBeTruthy();
|
|
expect(screen.queryByText("laptop-1.lan")).toBeNull();
|
|
});
|
|
|
|
test("an unnamed row shows the learned name with the learned affordance", async () => {
|
|
await renderClientsPage(BASE);
|
|
|
|
// The cell holds the learned name followed by the tag, so the match is on
|
|
// the containing span rather than on a bare text node.
|
|
const learned = screen.getByText(
|
|
(content, element) => element?.tagName === "SPAN" && content.startsWith("kids-tablet.lan"),
|
|
);
|
|
// The affordance is text, not colour, so a screen reader announces it too.
|
|
expect(within(learned).getByText("learned")).toBeTruthy();
|
|
});
|
|
|
|
test("shows the DNS-activity empty state when there are no clients", async () => {
|
|
await renderClientsPage({ ...BASE, "GET /api/clients": { clients: [] } });
|
|
|
|
expect(screen.getByText(/rows appear automatically as devices on the network make dns queries/i)).toBeTruthy();
|
|
expect(screen.queryByRole("table")).toBeNull();
|
|
});
|
|
|
|
test("edit opens a dialog seeded with the client's name and group", async () => {
|
|
await renderClientsPage(BASE);
|
|
|
|
fireEvent.click(screen.getAllByRole("button", { name: "Edit" })[0]!);
|
|
// The dialog portals out of the table, so every field query is scoped to it.
|
|
const dialog = within(await screen.findByRole("dialog", { name: "Edit client 192.168.1.10" }));
|
|
expect((dialog.getByLabelText("Name") as HTMLInputElement).value).toBe("laptop");
|
|
// The RAC Select names its trigger with the value and then the label.
|
|
expect(dialog.getByRole("button", { name: /Group$/ }).textContent).toContain("default");
|
|
});
|
|
|
|
test("the group picker offers every group and reports the choice", async () => {
|
|
await renderClientsPage(BASE);
|
|
|
|
fireEvent.click(screen.getAllByRole("button", { name: "Edit" })[0]!);
|
|
const dialog = within(await screen.findByRole("dialog", { name: "Edit client 192.168.1.10" }));
|
|
fireEvent.click(dialog.getByRole("button", { name: /Group$/ }));
|
|
|
|
const options = await screen.findAllByRole("option");
|
|
expect(options.map((option) => option.textContent)).toEqual(["default", "kids"]);
|
|
|
|
fireEvent.click(screen.getByRole("option", { name: "kids" }));
|
|
expect(screen.getByRole("button", { name: /Group$/ }).textContent).toContain("kids");
|
|
});
|
|
|
|
test("prefix editor starts clean and dirties on add", async () => {
|
|
await renderClientsPage(BASE);
|
|
|
|
expect((screen.getByLabelText("Prefix 1") as HTMLInputElement).value).toBe("192.168.1.0/24");
|
|
const save = screen.getByRole("button", { name: "Save prefixes" }) as HTMLButtonElement;
|
|
expect(save.disabled).toBe(true);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Add prefix" }));
|
|
expect(save.disabled).toBe(false);
|
|
expect((screen.getByLabelText("Prefix 2") as HTMLInputElement).value).toBe("");
|
|
});
|