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 } });
});