feat(ui): E31 S4b — the admin pages behind one rail row, Configuration as a sectioned list, Statistics as tiles and bars
- 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)
This commit is contained in:
@@ -7,8 +7,8 @@ import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { Bell } from "lucide-react";
|
||||
import { NavRailAction, NavRailItem } from "./NavRail";
|
||||
import { Activity, Bell, Shield, Users } from "lucide-react";
|
||||
import { NavRailAction, NavRailGroup, NavRailItem } from "./NavRail";
|
||||
|
||||
vi.mock("../../lib/hints", () => ({ hintsEnabled: () => true, subscribeHints: () => () => {} }));
|
||||
|
||||
@@ -123,3 +123,86 @@ describe("NavRailAction", () => {
|
||||
expect(src).not.toMatch(/<IconButton[\s>]/);
|
||||
});
|
||||
});
|
||||
|
||||
// E31 (the owner's pick): the five admin rows sat below the fold at 133% zoom behind a fade alone, so
|
||||
// they are one "Admin" row opening a panel of them. The panel's pages stay links, as the rail's are.
|
||||
describe("NavRailGroup", () => {
|
||||
const pages = (active: string | null, onSelect = vi.fn()) =>
|
||||
[
|
||||
{ id: "scheduler", icon: Activity, label: "Scheduler" },
|
||||
{ id: "users", icon: Users, label: "Users" },
|
||||
].map((p) => ({ ...p, href: `/?page=${p.id}`, active: p.id === active, onSelect }));
|
||||
|
||||
it("is one row, current while any of its pages is, opening a panel of links", () => {
|
||||
render(
|
||||
<NavRailGroup
|
||||
icon={Shield}
|
||||
label="Administration"
|
||||
shortLabel="Admin"
|
||||
showLabel={false}
|
||||
pages={pages("users")}
|
||||
/>,
|
||||
);
|
||||
const row = screen.getByRole("button", { name: "Administration" });
|
||||
expect(row.getAttribute("aria-current")).toBe("page");
|
||||
expect(screen.queryByRole("link")).toBeNull();
|
||||
fireEvent.click(row);
|
||||
// jsdom never positions the panel, so it stays visibility:hidden and nameless to the role query.
|
||||
const panel = screen.getByRole("dialog", { hidden: true });
|
||||
expect(panel.getAttribute("aria-label")).toBe("Administration");
|
||||
const links = Array.from(panel.querySelectorAll("a"));
|
||||
expect(links.map((a) => [a.textContent, a.getAttribute("href")])).toEqual([
|
||||
["Scheduler", "/?page=scheduler"],
|
||||
["Users", "/?page=users"],
|
||||
]);
|
||||
expect(links.map((a) => a.getAttribute("aria-current"))).toEqual([null, "page"]);
|
||||
});
|
||||
|
||||
it("is not current while none of its pages is", () => {
|
||||
render(
|
||||
<NavRailGroup
|
||||
icon={Shield}
|
||||
label="Administration"
|
||||
shortLabel="Admin"
|
||||
showLabel
|
||||
pages={pages(null)}
|
||||
/>,
|
||||
);
|
||||
const row = screen.getByRole("button", { name: "Admin" });
|
||||
expect(row.getAttribute("aria-current")).toBeNull();
|
||||
expect(row.className).not.toContain("bg-accent/15");
|
||||
});
|
||||
|
||||
it("a plain click goes to the page and closes the panel; a middle or Ctrl click is the browser's", () => {
|
||||
const onSelect = vi.fn();
|
||||
render(
|
||||
<NavRailGroup
|
||||
icon={Shield}
|
||||
label="Administration"
|
||||
shortLabel="Admin"
|
||||
showLabel={false}
|
||||
pages={pages(null, onSelect)}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Administration" }));
|
||||
const link = () =>
|
||||
screen.getByRole("dialog", { hidden: true }).querySelector<HTMLAnchorElement>("a")!;
|
||||
const leftToBrowser: boolean[] = [];
|
||||
const record = (e: Event) => {
|
||||
leftToBrowser.push(!e.defaultPrevented);
|
||||
e.preventDefault();
|
||||
};
|
||||
window.addEventListener("click", record);
|
||||
try {
|
||||
fireEvent.click(link(), { button: 1 });
|
||||
fireEvent.click(link(), { button: 0, ctrlKey: true });
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
fireEvent.click(link(), { button: 0 });
|
||||
} finally {
|
||||
window.removeEventListener("click", record);
|
||||
}
|
||||
expect(leftToBrowser).toEqual([true, true, false]);
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByRole("dialog", { hidden: true })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user