Files
siftlode/frontend/src/modules/config/ConfigPanel.field.dom.test.tsx
T
peter 63a0fecc8b refactor(frontend): the flat components/ folder moves into shell/, modules/<page>/ and features/<name>/
A pure move (git mv, history kept): 333 files leave components/, which now holds only the platform
layer (ui/) and the legal pages. The shell is the frame (App, header, nav rail, palette, boot); each
page lives in modules/<page>/ with its rail, store and tests; what several pages share sits in
features/<name>/; the shared-link page is watch/. Domain-free helpers went straight to ui/
(ViewSwitcher, FilterRail, the confirm dialog, the language switcher, the copy rows).

Every relative specifier was rewritten (imports, dynamic imports, vi.mock, new URL), and the
contract tests that scan sources now scan the whole app source, not only components/ — each glob
was counted before and after so none went green over fewer files. The locale-format rule matched
`/src/components/` only, so the move would have taken ~300 files out from under its i18n-singleton
check; it now covers every folder that holds components, with a test per folder.
2026-10-22 23:12:45 +02:00

100 lines
3.8 KiB
TypeScript

// @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<typeof import("react-i18next")>();
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<typeof import("../../lib/api")>();
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(
<QueryClientProvider client={new QueryClient()}>
<ConfigPanel />
</QueryClientProvider>,
);
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(
<QueryClientProvider client={new QueryClient()}>
<ConfigPanel />
</QueryClientProvider>,
);
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");
});
});