import { fireEvent, screen, waitFor, within } from "@testing-library/react"; import { DATABASE, contentArea, renderPage, stubApi, type Call } from "./testFixtures"; /** * Resolution in database mode: the upstream pool, the local records and the * forward zones, each rehomed from its own page onto a tab of one. */ let calls: Call[]; afterEach(() => { vi.unstubAllGlobals(); }); function writes(): Call[] { return calls; } async function openResolution(tab?: string, options: Parameters[1] = {}) { calls = stubApi(DATABASE, options); const suffix = tab === undefined ? "" : `?tab=${tab}`; return renderPage(`/configuration/resolution${suffix}`, "Resolution"); } /** The row Delete opens the dialog; the dialog's own Delete is the confirm. */ async function openDeleteDialog(index = 0) { fireEvent.click(screen.getAllByRole("button", { name: "Delete" })[index]!); return await screen.findByRole("alertdialog"); } test("the upstream pool is the default tab and lists every field", async () => { await openResolution(); expect(await screen.findByText("udp://1.1.1.1:53")).toBeTruthy(); expect(screen.getByText("tls://9.9.9.9:853")).toBeTruthy(); expect(screen.getByText("dns.quad9.net")).toBeTruthy(); expect((screen.getByLabelText("udp://1.1.1.1:53 enabled") as HTMLInputElement).checked).toBe(true); expect((screen.getByLabelText("tls://9.9.9.9:853 enabled") as HTMLInputElement).checked).toBe(false); expect(screen.getByRole("heading", { name: "Add upstream" })).toBeTruthy(); expect(screen.getByText(/applies to the next query/)).toBeTruthy(); }); test("adding an upstream posts every field", async () => { await openResolution(); await screen.findByRole("heading", { name: "Add upstream" }); fireEvent.change(screen.getByLabelText("URL"), { target: { value: "udp://8.8.8.8:53" } }); fireEvent.change(screen.getByLabelText("Priority"), { target: { value: "150" } }); fireEvent.click(screen.getByRole("button", { name: "Add upstream" })); await waitFor(() => expect(writes()).toHaveLength(1)); expect(writes()[0]).toEqual({ url: "/api/upstreams", method: "POST", body: { url: "udp://8.8.8.8:53", priority: 150, enabled: true, tls_name: "" }, }); }); test("an upstream write applies live, so the tab says nothing about a restart", async () => { // The server rebuilds the pool on the write and echoes `restart_required: // false`, so `restart_pending` stays down and silence is the whole report. await openResolution(undefined, { responses: { "GET /api/config/status": () => DATABASE } }); await screen.findByRole("heading", { name: "Add upstream" }); fireEvent.change(screen.getByLabelText("URL"), { target: { value: "udp://8.8.8.8:53" } }); fireEvent.click(screen.getByRole("button", { name: "Add upstream" })); await waitFor(() => expect(writes()).toHaveLength(1)); expect(screen.queryByText(/Restart nxdns to apply them/)).toBeNull(); expect(contentArea().textContent).not.toMatch(/restart/i); }); test("toggling enabled resends the whole row", async () => { await openResolution(); await screen.findByLabelText("tls://9.9.9.9:853 enabled"); fireEvent.click(screen.getByLabelText("tls://9.9.9.9:853 enabled")); await waitFor(() => expect(writes()).toHaveLength(1)); expect(writes()[0]).toEqual({ url: "/api/upstreams/2", method: "PUT", body: { url: "tls://9.9.9.9:853", priority: 200, enabled: true, tls_name: "dns.quad9.net" }, }); }); test("upstream delete asks for confirmation and skips the request when cancelled", async () => { await openResolution(); await screen.findByText("udp://1.1.1.1:53"); const dialog = await openDeleteDialog(); expect(dialog.textContent).toContain('Delete upstream "udp://1.1.1.1:53"?'); fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" })); await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull()); expect(writes()).toHaveLength(0); }); test("confirming the upstream delete dialog issues the DELETE", async () => { await openResolution(); await screen.findByText("udp://1.1.1.1:53"); const dialog = await openDeleteDialog(); fireEvent.click(within(dialog).getByRole("button", { name: "Delete" })); await waitFor(() => expect(writes()).toHaveLength(1)); expect(writes()[0]).toMatchObject({ method: "DELETE", url: "/api/upstreams/1" }); }); test("a 409 on create renders the conflict text inline", async () => { await openResolution(undefined, { onWrite: () => new Response(JSON.stringify({ error: "an upstream with that url already exists" }), { status: 409, headers: { "content-type": "application/json" }, }), }); await screen.findByRole("heading", { name: "Add upstream" }); fireEvent.change(screen.getByLabelText("URL"), { target: { value: "udp://1.1.1.1:53" } }); fireEvent.click(screen.getByRole("button", { name: "Add upstream" })); const alert = await screen.findByRole("alert"); expect(alert.textContent).toBe("an upstream with that url already exists"); }); test("a 409 on toggle renders the last-enabled conflict", async () => { await openResolution(undefined, { onWrite: () => new Response(JSON.stringify({ error: "the last enabled upstream cannot be disabled" }), { status: 409, headers: { "content-type": "application/json" }, }), }); await screen.findByLabelText("udp://1.1.1.1:53 enabled"); fireEvent.click(screen.getByLabelText("udp://1.1.1.1:53 enabled")); const alert = await screen.findByRole("alert"); expect(alert.textContent).toBe("the last enabled upstream cannot be disabled"); }); test("a 409 on delete renders the last-enabled conflict", async () => { await openResolution(undefined, { onWrite: () => new Response(JSON.stringify({ error: "the last enabled upstream cannot be removed" }), { status: 409, headers: { "content-type": "application/json" }, }), }); await screen.findByText("udp://1.1.1.1:53"); const dialog = await openDeleteDialog(); fireEvent.click(within(dialog).getByRole("button", { name: "Delete" })); const alert = await screen.findByRole("alert"); expect(alert.textContent).toBe("the last enabled upstream cannot be removed"); }); test("editing a row seeds the form and PUTs the replaced row", async () => { await openResolution(); await screen.findByText("tls://9.9.9.9:853"); fireEvent.click(screen.getAllByRole("button", { name: "Edit" })[1]!); await screen.findByRole("heading", { name: "Edit tls://9.9.9.9:853" }); expect((screen.getByLabelText("URL") as HTMLInputElement).value).toBe("tls://9.9.9.9:853"); expect((screen.getByLabelText("Priority") as HTMLInputElement).value).toBe("200"); expect((screen.getByLabelText("TLS name") as HTMLInputElement).value).toBe("dns.quad9.net"); fireEvent.change(screen.getByLabelText("Priority"), { target: { value: "10" } }); fireEvent.click(screen.getByRole("button", { name: "Save changes" })); await waitFor(() => expect(writes()).toHaveLength(1)); expect(writes()[0]).toEqual({ url: "/api/upstreams/2", method: "PUT", body: { url: "tls://9.9.9.9:853", priority: 10, enabled: false, tls_name: "dns.quad9.net" }, }); await screen.findByRole("heading", { name: "Add upstream" }); }); test("the arrow keys move between tabs, and the panel follows", async () => { await openResolution(); await screen.findByText("udp://1.1.1.1:53"); const tablist = screen.getByRole("tablist", { name: "Resolution" }); expect(screen.getByRole("tab", { name: "Upstreams" }).getAttribute("aria-selected")).toBe("true"); fireEvent.keyDown(tablist, { key: "ArrowRight" }); await waitFor(() => expect(screen.getByRole("tab", { name: "Records" }).getAttribute("aria-selected")).toBe("true"), ); await screen.findByText("nas.lan.home"); fireEvent.keyDown(tablist, { key: "ArrowLeft" }); await waitFor(() => expect(screen.getByRole("tab", { name: "Upstreams" }).getAttribute("aria-selected")).toBe("true"), ); await screen.findByText("udp://1.1.1.1:53"); }); test("creating a local record posts exactly the LocalRecordInput", async () => { await openResolution("records"); 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: 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 waitFor(() => expect(writes()).toHaveLength(1)); expect(writes()[0]).toMatchObject({ url: "/api/local-records", method: "POST", body: { name: "printer.lan.home", rtype: "AAAA", value: "fd00::11" }, }); }); test("the record delete dialog names the record and only deletes on confirm", async () => { await openResolution("records"); await screen.findByText("nas.lan.home"); let dialog = await openDeleteDialog(); expect(dialog.textContent).toContain('Delete record "nas.lan.home"?'); fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" })); await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull()); expect(writes()).toHaveLength(0); dialog = await openDeleteDialog(); fireEvent.click(within(dialog).getByRole("button", { name: "Delete" })); await waitFor(() => expect(writes()).toHaveLength(1)); expect(writes()[0]).toMatchObject({ method: "DELETE", url: "/api/local-records/1" }); }); test("the forward zone delete dialog names the zone and only deletes on confirm", async () => { await openResolution("zones"); await screen.findByText("lan.home"); let dialog = await openDeleteDialog(); expect(dialog.textContent).toContain('Delete forward zone "lan.home"?'); fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" })); await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull()); expect(writes()).toHaveLength(0); dialog = await openDeleteDialog(); fireEvent.click(within(dialog).getByRole("button", { name: "Delete" })); await waitFor(() => expect(writes()).toHaveLength(1)); expect(writes()[0]).toMatchObject({ method: "DELETE", url: "/api/forward-zones/1" }); });