mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
test(admin): tighten status-card-grid + add per-section render smoke
* `status-card-grid.test.tsx` — replaces the v1.4.6 anchor-id allowlist with a route-existence check: every card's href must match `/admin/ <slug>` AND `slug` must be in the `ADMIN_SECTION_SLUGS` source-of- truth, AND `src/app/admin/[section]/page.tsx` must exist on disk. This guarantees the dynamic-route file can never be silently moved out from under the cards. * `sections.test.tsx` — SSR smoke test for every section component reachable from the dynamic-route renderer. Every section paints a recognizable heading, and the `<UserManagementSection>` filter pills + `<BackupsSection>` "Backup now" button are asserted so the v1.5 fold-ins for T2.6 and T2.7 don't regress silently. Locale parity for the new admin.section.* keys is enforced by the existing `i18n-locale-integrity` test suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7a47532231
commit
1a7e3d6c2c
2 files changed
+171
-26
No files matched your search
@@ -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(
|
||||
<I18nProvider initialLocale="en">{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
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("<SystemStatusSection> renders", () => {
|
||||
const html = render(<SystemStatusSection />);
|
||||
expect(html).toContain("System");
|
||||
});
|
||||
|
||||
it("<GeneralSettingsSection> renders", () => {
|
||||
const html = render(<GeneralSettingsSection />);
|
||||
expect(html).toContain("Allow registration");
|
||||
});
|
||||
|
||||
it("<ServicesSection> renders", () => {
|
||||
const html = render(<ServicesSection />);
|
||||
expect(html).toContain("Telegram");
|
||||
});
|
||||
|
||||
it("<IntegrationsGroupSection> renders all four sub-sections", () => {
|
||||
const html = render(<IntegrationsGroupSection />);
|
||||
expect(html).toContain("Umami");
|
||||
expect(html).toContain("Glitchtip");
|
||||
expect(html).toContain("Web Push");
|
||||
});
|
||||
|
||||
it("<FeedbackInboxSection> renders", () => {
|
||||
const html = render(<FeedbackInboxSection />);
|
||||
expect(html).toContain("Feedback");
|
||||
});
|
||||
|
||||
it("<RemindersSection> renders", () => {
|
||||
const html = render(<RemindersSection />);
|
||||
expect(html).toContain("Medication Reminders");
|
||||
});
|
||||
|
||||
it("<UserManagementSection> renders with filter pills", () => {
|
||||
const html = render(<UserManagementSection currentUserId="u1" />);
|
||||
expect(html).toContain("User Management");
|
||||
// Filter pills painted from i18n
|
||||
expect(html).toContain("All");
|
||||
expect(html).toContain("Admins");
|
||||
});
|
||||
|
||||
it("<ApiTokenOverviewSection> renders", () => {
|
||||
const html = render(<ApiTokenOverviewSection />);
|
||||
expect(html).toContain("API Tokens");
|
||||
});
|
||||
|
||||
it("<LoginOverviewSection> renders", () => {
|
||||
const html = render(<LoginOverviewSection />);
|
||||
expect(html).toContain("Login Overview");
|
||||
});
|
||||
|
||||
it("<BackupsSection> renders heading + run-now button", () => {
|
||||
const html = render(<BackupsSection />);
|
||||
expect(html).toContain("Backups");
|
||||
expect(html).toContain("Backup now");
|
||||
});
|
||||
|
||||
it("<DangerZoneSection> renders", () => {
|
||||
const html = render(<DangerZoneSection />);
|
||||
expect(html).toContain("Danger Zone");
|
||||
});
|
||||
});
|
||||
@@ -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/<slug>` 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<string>(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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user