- NavRailGroup: one "Admin" rail row opening a panel of the admin pages (links, so a middle-click still opens a tab); the rail fits 133% zoom without a hidden scroll - sysconfig.SECTIONS orders the config groups under four headings (every group in exactly one, test-held); the page shows them as Settings' side list (Tabs `group` headings); the lone registration group reads "Registration" under "Access" - StatTiles / StatTile / StatBars: the Statistics overview's figures as tiles, the per-operation quota use as horizontal bars (a zero draws none)
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// E31 (the owner's pick): Configuration's sixteen group tabs wrapped to three rows at 133% zoom, so they
|
|
// are a side list under four headings, as Settings has. The registry owns the order: the groups follow
|
|
// its sections, not the order the groups object happens to arrive in.
|
|
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, group: string): ConfigItem => ({
|
|
key,
|
|
type: "str",
|
|
group,
|
|
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: { b: [text("kb", "b")], c: [text("kc", "c")], a: [text("ka", "a")] },
|
|
sections: [
|
|
{ id: "one", groups: ["a", "b"] },
|
|
{ id: "two", groups: ["c"] },
|
|
],
|
|
secrets_manageable: true,
|
|
}),
|
|
},
|
|
};
|
|
});
|
|
vi.mock("../lib/useMe", () => ({ useMe: () => ({ email_configured: true }) }));
|
|
|
|
import ConfigPanel from "./ConfigPanel";
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
localStorage.clear();
|
|
});
|
|
|
|
const renderPanel = () =>
|
|
render(
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
<ConfigPanel />
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
describe("Configuration sections (E31)", () => {
|
|
it("lists the groups in the registry's sections, each heading once above its run", async () => {
|
|
renderPanel();
|
|
const list = await screen.findByRole("tablist");
|
|
expect(list.getAttribute("aria-orientation")).toBe("vertical");
|
|
// The list's own lines, top to bottom: headings and tabs.
|
|
const lines = Array.from(list.children).map((el) =>
|
|
el.getAttribute("role") === "tab" ? `tab:${el.textContent}` : `head:${el.textContent}`,
|
|
);
|
|
expect(lines).toEqual([
|
|
"head:config.sections.one",
|
|
"tab:config.groups.a",
|
|
"tab:config.groups.b",
|
|
"head:config.sections.two",
|
|
"tab:config.groups.c",
|
|
]);
|
|
});
|
|
|
|
it("opens on the first group and switches to the one picked", async () => {
|
|
renderPanel();
|
|
expect(await screen.findByText("config.fields.ka.label")).toBeTruthy();
|
|
fireEvent.click(screen.getByRole("tab", { name: "config.groups.c" }));
|
|
expect(screen.getByText("config.fields.kc.label")).toBeTruthy();
|
|
expect(screen.queryByText("config.fields.ka.label")).toBeNull();
|
|
});
|
|
});
|