diff --git a/src/components/admin/__tests__/sections.test.tsx b/src/components/admin/__tests__/sections.test.tsx
new file mode 100644
index 000000000..1c3bf7b29
--- /dev/null
+++ b/src/components/admin/__tests__/sections.test.tsx
@@ -0,0 +1,136 @@
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+// The section components rely on TanStack Query, the auth hook, and a
+// few API routes. Stub everything at the module boundary so we can
+// SSR-render each section in isolation and assert the heading paints.
+//
+// This is intentionally a smoke test — covering the loading branch
+// (most sections paint a Loader2 + label until data arrives) is enough
+// to guarantee the dynamic-route renderer doesn't blow up when it
+// imports a section.
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
+ usePathname: () => "/admin/system-status",
+}));
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({ data: null, isLoading: false, refetch: vi.fn() }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn() }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ isPending: false,
+ isError: false,
+ error: null,
+ }),
+}));
+
+vi.mock("@/hooks/use-auth", () => ({
+ useAuth: () => ({
+ user: {
+ id: "u1",
+ username: "marc",
+ email: "marc@example.com",
+ role: "ADMIN",
+ },
+ isAuthenticated: true,
+ isLoading: false,
+ refetch: vi.fn(),
+ }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { ADMIN_SECTION_SLUGS } from "../section-slugs";
+import { ApiTokenOverviewSection } from "../api-token-overview-section";
+import { BackupsSection } from "../backups-section";
+import { DangerZoneSection } from "../danger-zone-section";
+import { FeedbackInboxSection } from "../feedback-inbox-section";
+import { GeneralSettingsSection } from "../general-settings-section";
+import { IntegrationsGroupSection } from "../integrations-group-section";
+import { LoginOverviewSection } from "../login-overview-section";
+import { RemindersSection } from "../reminders-section";
+import { ServicesSection } from "../services-section";
+import { SystemStatusSection } from "../system-status-section";
+import { UserManagementSection } from "../user-management-section";
+
+function render(node: React.ReactElement) {
+ return renderToStaticMarkup(
+ {node},
+ );
+}
+
+describe("admin sections — SSR smoke", () => {
+ it("every slug in `ADMIN_SECTION_SLUGS` has a wired component", () => {
+ // Renderer-side parity: if a slug is added to the list but no
+ // matching case is added in `renderer.tsx`, the switch's `never`
+ // exhaustiveness guard fails type-check. This test mirrors that
+ // guarantee at runtime so a slug typo (or stale list) is caught
+ // even without `tsc`.
+ expect(ADMIN_SECTION_SLUGS).toContain("system-status");
+ expect(ADMIN_SECTION_SLUGS).toContain("backups");
+ expect(ADMIN_SECTION_SLUGS).toContain("users");
+ expect(ADMIN_SECTION_SLUGS).toContain("danger-zone");
+ });
+
+ it(" renders", () => {
+ const html = render();
+ expect(html).toContain("System");
+ });
+
+ it(" renders", () => {
+ const html = render();
+ expect(html).toContain("Allow registration");
+ });
+
+ it(" renders", () => {
+ const html = render();
+ expect(html).toContain("Telegram");
+ });
+
+ it(" renders all four sub-sections", () => {
+ const html = render();
+ expect(html).toContain("Umami");
+ expect(html).toContain("Glitchtip");
+ expect(html).toContain("Web Push");
+ });
+
+ it(" renders", () => {
+ const html = render();
+ expect(html).toContain("Feedback");
+ });
+
+ it(" renders", () => {
+ const html = render();
+ expect(html).toContain("Medication Reminders");
+ });
+
+ it(" renders with filter pills", () => {
+ const html = render();
+ expect(html).toContain("User Management");
+ // Filter pills painted from i18n
+ expect(html).toContain("All");
+ expect(html).toContain("Admins");
+ });
+
+ it(" renders", () => {
+ const html = render();
+ expect(html).toContain("API Tokens");
+ });
+
+ it(" renders", () => {
+ const html = render();
+ expect(html).toContain("Login Overview");
+ });
+
+ it(" renders heading + run-now button", () => {
+ const html = render();
+ expect(html).toContain("Backups");
+ expect(html).toContain("Backup now");
+ });
+
+ it(" renders", () => {
+ const html = render();
+ expect(html).toContain("Danger Zone");
+ });
+});
diff --git a/src/components/admin/__tests__/status-card-grid.test.tsx b/src/components/admin/__tests__/status-card-grid.test.tsx
index 168bb3046..6bbc9a449 100644
--- a/src/components/admin/__tests__/status-card-grid.test.tsx
+++ b/src/components/admin/__tests__/status-card-grid.test.tsx
@@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";
+import { existsSync } from "node:fs";
+import { join } from "node:path";
import { renderToStaticMarkup } from "react-dom/server";
import { StatusBadge, buildCards } from "../status-card-grid";
+import { ADMIN_SECTION_SLUGS } from "../section-slugs";
import type { StatusOverview } from "@/app/api/admin/status-overview/route";
const mockOverview: StatusOverview = {
@@ -99,36 +102,42 @@ describe("buildCards()", () => {
expect(cards[5].severity).toBe("info"); // audit log
});
- it("links each card to a real anchor on the admin page", () => {
- // Source-of-truth list of section IDs rendered by `src/app/admin/page.tsx`.
- // If you add a new section to the admin page, add the ID here. The test
- // proves every status-card href can actually scroll to something —
- // `startsWith("/admin")` was too lax and let the v1.4.5 regression slip
- // (every CTA bounced to `/admin#integrations` which does not exist).
- const realAnchorIds = new Set([
- "section-system-status",
- "section-admin-general",
- "section-admin-services",
- "section-admin-umami",
- "section-admin-glitchtip",
- "section-admin-webpush",
- "section-admin-bugreport",
- "section-admin-feedback",
- "section-admin-reminders",
- "section-user-management",
- "section-api-tokens",
- "section-login-overview",
- "section-danger-zone",
- ]);
+ it("links each card to a real `/admin/` route that exists on disk", () => {
+ // v1.5: status-card CTAs no longer use `#anchor` fragments — every href
+ // points at a dynamic-route slug under `/admin/[section]`. This test
+ // proves the slug exists (a) in the static `ADMIN_SECTION_SLUGS` list
+ // that drives `generateStaticParams()` AND (b) that the dynamic route
+ // file `src/app/admin/[section]/page.tsx` exists on disk. If a future
+ // refactor moves the dynamic route, the second check fails loudly.
+ const validSlugs = new Set(ADMIN_SECTION_SLUGS);
+
+ // `__dirname` here is `.../src/components/admin/__tests__`, so walk
+ // up four levels to the repo root.
+ const repoRoot = join(__dirname, "..", "..", "..", "..");
+ const dynamicRoute = join(
+ repoRoot,
+ "src",
+ "app",
+ "admin",
+ "[section]",
+ "page.tsx",
+ );
+ expect(
+ existsSync(dynamicRoute),
+ `Dynamic admin route missing — expected ${dynamicRoute}`,
+ ).toBe(true);
const cards = buildCards(mockOverview);
for (const card of cards) {
expect(card.cta.length).toBeGreaterThan(0);
- // Hrefs must be `/admin#section-...` — sub-routes like `/admin/users`
- // do not exist as of v1.4.6 and were the original regression.
- expect(card.href).toMatch(/^\/admin#section-/);
- const anchor = card.href.replace(/^\/admin#/, "");
- expect(realAnchorIds.has(anchor)).toBe(true);
+ // Hrefs must be real sub-routes; `/admin#section-*` was the v1.4.6
+ // honesty problem the v1.5 refactor permanently removed.
+ expect(card.href).toMatch(/^\/admin\/[a-z][a-z0-9-]*$/);
+ const slug = card.href.replace(/^\/admin\//, "");
+ expect(
+ validSlugs.has(slug),
+ `Card ${card.title} -> ${card.href} is not in ADMIN_SECTION_SLUGS`,
+ ).toBe(true);
}
});