// @vitest-environment jsdom // // Locks the stacking that makes an admin config row usable on a phone. The // control column cannot shrink, so beside a wide control it left the label and its hint a sliver of // the ~250 px a phone leaves next to the navigation rail. The sibling fix — a long-labelled choice // becoming a dropdown — is covered in ConfigPanel.choice.dom.test.tsx, but that one does nothing for // a text, number or secret key, whose `w-52` input is the wide control: this is their only fix. // // jsdom does no layout, so what is asserted is the class contract those rules rest on, the same // trade SearchBar.dom.test.tsx already makes for the narrow-header rules. import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ConfigItem } from "../lib/api"; vi.mock("react-i18next", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key, i18n: { language: "en" } }), }; }); const text = (key: string): ConfigItem => ({ key, type: "str", group: "g", secret: false, min: null, max: null, is_set: false, allow_empty: true, choices: [], value: "", default: "", }); vi.mock("../lib/api", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, api: { ...actual.api, adminConfig: () => Promise.resolve({ groups: { g: [text("site_name"), { ...text("signups_open"), type: "bool", value: "false" }], }, sections: [{ id: "s", groups: ["g"] }], secrets_manageable: true, }), }, }; }); vi.mock("../lib/useMe", () => ({ useMe: () => ({ email_configured: true }) })); import ConfigPanel from "./ConfigPanel"; afterEach(() => { cleanup(); vi.clearAllMocks(); }); describe("ConfigPanel field row", () => { it("stacks the label above the control below `sm`, and lets the control fit the screen", async () => { render( , ); const label = await screen.findByText("config.fields.site_name.label"); const row = label.parentElement!.parentElement!; // label → the label/hint column → the row // One column on a phone, the two-column row only from `sm` up: expect(row.className).toContain("flex-col"); expect(row.className).toContain("sm:flex-row"); // The control column may not out-width the screen, and only stops shrinking from `sm` up: const control = row.lastElementChild as HTMLElement; expect(control.className).toContain("max-w-full"); expect(control.className).toContain("sm:shrink-0"); expect(control.className).not.toMatch(/(^|\s)shrink-0(\s|$)/); }); it("toggles a boolean key from its title and its hint, not only from the switch (F12)", async () => { render( , ); const sw = await screen.findByRole("switch", { name: "config.fields.signups_open.label" }); expect(sw.getAttribute("aria-checked")).toBe("false"); fireEvent.click(screen.getByText("config.fields.signups_open.label")); expect(sw.getAttribute("aria-checked")).toBe("true"); fireEvent.click(screen.getByText("config.fields.signups_open.hint")); expect(sw.getAttribute("aria-checked")).toBe("false"); // The switch itself still toggles exactly once, not once more through its label. fireEvent.click(sw); expect(sw.getAttribute("aria-checked")).toBe("true"); }); });