import { fireEvent, render, screen, waitFor } 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"; import type { LocalRecord, LocalRecordInput } from "@/lib/types"; let records: LocalRecord[]; let fetchMock: ReturnType; function json(payload: unknown, status = 200): Response { return new Response(JSON.stringify(payload), { status, headers: { "content-type": "application/json" } }); } function createFetchMock() { return vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); const method = init?.method ?? "GET"; if (url === "/api/local-records" && method === "GET") return json({ local_records: records }); if (url === "/api/local-records" && method === "POST") { const body = JSON.parse(String(init?.body)) as LocalRecordInput; const created: LocalRecord = { id: 99, ttl: body.ttl ?? 300, ...body }; records = [...records, created]; return json(created, 201); } if (url.startsWith("/api/local-records/") && method === "DELETE") { const id = Number(url.slice("/api/local-records/".length)); records = records.filter((record) => record.id !== id); return new Response(null, { status: 204 }); } if (url.startsWith("/api/forward-zones/") && method === "DELETE") return new Response(null, { status: 204 }); if (url === "/api/forward-zones" && method === "GET") { return json({ forward_zones: [{ id: 7, zone: "lan.home", resolver: "udp://192.168.1.1:53" }] }); } return json({ error: "not stubbed" }, 404); }); } beforeEach(() => { records = [{ id: 1, name: "nas.lan.home", rtype: "A", value: "192.168.1.10", ttl: 300 }]; fetchMock = createFetchMock(); vi.stubGlobal("fetch", fetchMock); }); afterEach(() => { vi.unstubAllGlobals(); }); function renderPage() { const queryClient = createQueryClient(); const router = createAppRouter(createMemoryHistory({ initialEntries: ["/local-dns"] }), queryClient); render( , ); } test("renders the records table and switches to the forward zones tab", async () => { renderPage(); await screen.findByRole("heading", { name: "Local DNS" }); await screen.findByText("nas.lan.home"); expect(screen.getByText("192.168.1.10")).toBeTruthy(); fireEvent.click(screen.getByRole("tab", { name: "Forward zones" })); await screen.findByText("lan.home"); expect(screen.getByText("udp://192.168.1.1:53")).toBeTruthy(); }); test("the arrow keys move between tabs", async () => { renderPage(); await screen.findByText("nas.lan.home"); const tablist = screen.getByRole("tablist", { name: "Local DNS" }); const records = screen.getByRole("tab", { name: "Records" }); expect(records.getAttribute("aria-selected")).toBe("true"); fireEvent.keyDown(tablist, { key: "ArrowRight" }); const zones = screen.getByRole("tab", { name: "Forward zones" }); expect(zones.getAttribute("aria-selected")).toBe("true"); expect(screen.getByRole("tab", { name: "Records" }).getAttribute("aria-selected")).toBe("false"); await screen.findByText("lan.home"); fireEvent.keyDown(tablist, { key: "ArrowLeft" }); expect(screen.getByRole("tab", { name: "Records" }).getAttribute("aria-selected")).toBe("true"); await screen.findByText("nas.lan.home"); }); test("creates a record: POST body per LocalRecordInput, list refreshes", async () => { renderPage(); await screen.findByText("nas.lan.home"); fireEvent.click(screen.getByRole("button", { name: "Add record" })); fireEvent.change(screen.getByLabelText("Name"), { target: { value: "printer.lan.home" } }); // The record type is a RAC Select now: open the listbox, then pick. fireEvent.click(screen.getByRole("button", { name: /Type$/ })); fireEvent.click(await screen.findByRole("option", { name: "AAAA" })); fireEvent.change(screen.getByLabelText("Value"), { target: { value: "fd00::11" } }); fireEvent.click(screen.getByRole("button", { name: "Save" })); await screen.findByText("printer.lan.home"); const post = fetchMock.mock.calls.find( ([input, init]) => init?.method === "POST" && String(input) === "/api/local-records", ); expect(post).toBeTruthy(); expect(JSON.parse(String(post?.[1]?.body))).toEqual({ name: "printer.lan.home", rtype: "AAAA", value: "fd00::11" }); }); test("cancelling the record delete dialog sends no request", async () => { renderPage(); await screen.findByText("nas.lan.home"); fireEvent.click(screen.getByRole("button", { name: "Delete" })); const dialog = await screen.findByRole("alertdialog"); expect(dialog.textContent).toContain('Delete record "nas.lan.home"?'); fireEvent.click(screen.getByRole("button", { name: "Cancel" })); await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull()); expect(fetchMock.mock.calls.some(([, init]) => init?.method === "DELETE")).toBe(false); expect(screen.getByText("nas.lan.home")).toBeTruthy(); }); test("confirming the record delete dialog issues the DELETE", async () => { renderPage(); await screen.findByText("nas.lan.home"); fireEvent.click(screen.getByRole("button", { name: "Delete" })); await screen.findByRole("alertdialog"); fireEvent.click(screen.getByRole("button", { name: "Delete" })); await waitFor(() => expect( fetchMock.mock.calls.some( ([input, init]) => init?.method === "DELETE" && String(input) === "/api/local-records/1", ), ).toBe(true), ); await waitFor(() => expect(screen.queryByText("nas.lan.home")).toBeNull()); }); test("the forward zone delete dialog names the zone and confirms", async () => { renderPage(); await screen.findByText("nas.lan.home"); fireEvent.click(screen.getByRole("tab", { name: "Forward zones" })); await screen.findByText("lan.home"); fireEvent.click(screen.getByRole("button", { name: "Delete" })); const dialog = await screen.findByRole("alertdialog"); expect(dialog.textContent).toContain('Delete forward zone "lan.home"?'); fireEvent.click(screen.getByRole("button", { name: "Cancel" })); await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull()); expect(fetchMock.mock.calls.some(([, init]) => init?.method === "DELETE")).toBe(false); fireEvent.click(screen.getByRole("button", { name: "Delete" })); await screen.findByRole("alertdialog"); fireEvent.click(screen.getByRole("button", { name: "Delete" })); await waitFor(() => expect( fetchMock.mock.calls.some( ([input, init]) => init?.method === "DELETE" && String(input) === "/api/forward-zones/7", ), ).toBe(true), ); });