import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, expect, it, vi } from "vitest"; import { ENV, GROUP } from "../test/fixtures.ts"; import { serve } from "../test/http.ts"; import { Settings } from "./settings.tsx"; afterEach(() => { vi.unstubAllGlobals(); }); it("says it is reading before the answer arrives", () => { serve({ "/api/env": { body: ENV }, "/api/groups": { body: { groups: [GROUP] } } }); render(); expect(screen.getByText("Reading .env…")).toBeInTheDocument(); }); it("offers group ids to pick rather than a box to retype one into", async () => { // A group id is 44 characters of base64. Picking is the mitigation that // makes editing GROUP_IDS from a browser reasonable at all. serve({ "/api/env": { body: ENV }, "/api/groups": { body: { groups: [GROUP] } } }); render(); const picker = await screen.findByLabelText("GROUP_IDS"); expect(picker.tagName).toBe("SELECT"); expect(screen.getByRole("option", { name: GROUP.name })).toBeInTheDocument(); }); it("shows an empty picker when the listing could not be read", async () => { vi.stubGlobal( "fetch", vi.fn((input: string) => { if (input === "/api/groups") return Promise.reject(new Error("down")); return Promise.resolve({ json: () => Promise.resolve(ENV), ok: true, status: 200, } as Response); }), ); render(); await waitFor(() => { expect(screen.getByLabelText("GROUP_IDS")).toBeInTheDocument(); }); expect(screen.queryByRole("option", { name: GROUP.name })).not.toBeInTheDocument(); }); it("will not let a key the server refuses even be typed into", async () => { serve({ "/api/env": { body: ENV }, "/api/groups": { body: { groups: [GROUP] } } }); render(); await waitFor(() => { expect(screen.getByLabelText("PHONE_NUMBER")).toBeDisabled(); }); expect(screen.getByLabelText("COOLDOWN_SECONDS")).toBeEnabled(); }); it("locks the group fields while no listing has been read", async () => { // parse_group_ids only checks syntax. Writing one blind with the container // down turns a scoping mistake into a unit that will not start. serve({ "/api/env": { body: { ...ENV, groups_known: false } }, "/api/groups": { body: { groups: [] } } }); render(); await waitFor(() => { expect(screen.getByTestId("groups-locked")).toBeInTheDocument(); }); expect(screen.getByLabelText("GROUP_IDS")).toBeDisabled(); }); it("asks for a restart once something has been written", async () => { serve({ "/api/env": { body: ENV }, "/api/groups": { body: { groups: [GROUP] } } }); render(); await userEvent.type(await screen.findByLabelText("COOLDOWN_SECONDS"), "5"); await userEvent.click(screen.getByRole("button", { name: "Save" })); await waitFor(() => { expect(screen.getByTestId("note")).toHaveTextContent("Restart the bot"); }); }); it("shows the server's refusal verbatim", async () => { serve({ "/api/env": { body: ENV }, "/api/groups": { body: { groups: [GROUP] } } }); render(); await userEvent.type(await screen.findByLabelText("COOLDOWN_SECONDS"), "5"); serve({ "/api/env": { body: { detail: "COOLDOWN_SECONDS must be an integer" }, status: 400 }, }); await userEvent.click(screen.getByRole("button", { name: "Save" })); await waitFor(() => { expect(screen.getByTestId("note")).toHaveTextContent("must be an integer"); }); }); it("reports a failure that is not the server's own sentence", async () => { serve({ "/api/env": { body: ENV }, "/api/groups": { body: { groups: [GROUP] } } }); render(); await userEvent.type(await screen.findByLabelText("COOLDOWN_SECONDS"), "5"); vi.stubGlobal( "fetch", vi.fn(() => Promise.reject(new Error("network is down"))), ); await userEvent.click(screen.getByRole("button", { name: "Save" })); await waitFor(() => { expect(screen.getByTestId("note")).toHaveTextContent("could not save"); }); }); it("has nothing to save until something is edited", async () => { serve({ "/api/env": { body: ENV }, "/api/groups": { body: { groups: [GROUP] } } }); render(); await waitFor(() => { expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); }); }); it("writes the group id the picker was set to, not one that was typed", async () => { const fetcher = serve({ "/api/env": { body: ENV }, "/api/groups": { body: { groups: [GROUP] } }, }); render(); await userEvent.selectOptions(await screen.findByLabelText("GROUP_IDS"), GROUP.id); await userEvent.click(screen.getByRole("button", { name: "Save" })); await waitFor(() => { expect(screen.getByTestId("note")).toBeInTheDocument(); }); // A GET is called with one argument, so the init is genuinely absent for // the two reads this view makes on mount. type Call = [string, undefined | { body: string; method: string }]; const calls = fetcher.mock.calls as Call[]; const put = calls.find((call) => call[1]?.method === "PUT"); expect(JSON.parse(put?.[1]?.body ?? "{}")).toEqual({ values: { GROUP_IDS: GROUP.id } }); });