Files
HealthLog/src/components/layout/__tests__/sidebar-nav.test.tsx
T
Marc-André BombeckandClaude Opus 4.7 363e73cbf2 chore: voice + filename hygiene + wave-4 state wrap
Maintainer-voice sweep across `src/`. ~190 references to the
maintainer by first name in code comments and JSDoc neutralised to
"the maintainer", "the live tenant", "users", or "the user" depending
on context. Three sed passes covering ~50 distinct patterns
(`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.).
Test fixtures (`displayName: "Marc B."`, `username: "marc"`,
`<HeroStrip userName="Marc">`) kept as opaque test data per the brief.

CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a
German bullet with its English translation by historical design — the
in-app copy is English-default and the user docs are English, so the
mixed-language style was a holdover from the early-marathon notes.
A one-off node script extracted the `_..._` English paragraph from
each bilingual bullet and rewrote the entry as that text. ~44 bullets
reduced; one Docs bullet that lost a leading word in the regex
fixed by hand.

CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer
AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md`
stays in place for multi-agent compatibility (the conventions doc is
shared with Codex / Cursor / etc.).

Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete;
v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20
items the marathon landed; `.planning/phase-W4-report.md` documents
the per-commit decisions, test deltas, and the 23 items deferred to
v1.4.23 with reasoning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:46:32 +02:00

145 lines
5.5 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
// `<SidebarNav>` reads the active route from `usePathname()`. Stub
// next/navigation so the SSR test render works without an App-Router
// runtime.
const mockPathnameRef = { value: "/" };
vi.mock("next/navigation", () => ({
usePathname: () => mockPathnameRef.value,
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));
// Each test mutates `mockUserRef.value.role` before rendering so we can
// exercise both regular-user and admin layouts from the same suite.
const mockUserRef = {
value: {
id: "u1",
username: "marc",
email: "marc@example.com",
role: "USER" as "USER" | "ADMIN",
gravatarUrl: null,
},
};
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({
user: mockUserRef.value,
isAuthenticated: true,
isLoading: false,
refetch: vi.fn(),
}),
useLogout: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/components/providers", () => ({
useTheme: () => ({
theme: "system" as const,
resolvedTheme: "dark" as const,
setTheme: vi.fn(),
}),
}));
// The bug-report flag — each test mutates `mockSettingsRef.value` before
// rendering so the sidebar reads the desired state.
const mockSettingsRef = { value: { bugReportEnabled: true } };
vi.mock("@/components/app-settings-provider", () => ({
useAppSettings: () => mockSettingsRef.value,
}));
import { I18nProvider } from "@/lib/i18n/context";
import { SidebarNav } from "../sidebar-nav";
import { ADMIN_SECTIONS } from "@/components/admin/admin-shell";
function render({
pathname = "/",
bugReportEnabled = true,
role = "USER" as "USER" | "ADMIN",
}: {
pathname?: string;
bugReportEnabled?: boolean;
role?: "USER" | "ADMIN";
} = {}) {
mockPathnameRef.value = pathname;
mockSettingsRef.value = { bugReportEnabled };
mockUserRef.value = { ...mockUserRef.value, role };
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<SidebarNav />
</I18nProvider>,
);
}
describe("<SidebarNav> bug-report toggle", () => {
it("renders the Bug Report entry when the admin flag is enabled", () => {
const html = render({ bugReportEnabled: true });
expect(html).toContain('href="/bugreport"');
// i18n string for the entry — sanity check that the link is the actual
// bug-report nav entry, not e.g. an error-page report-bug button.
expect(html).toContain("Bug Report");
});
it("hides the Bug Report entry when the admin flag is disabled", () => {
const html = render({ bugReportEnabled: false });
expect(html).not.toContain('href="/bugreport"');
});
});
describe("<SidebarNav> admin entry mirrors Settings (no sub-item expansion)", () => {
// v1.4.16 A1: the maintainer reported the global sidebar expanding admin
// sub-items on `/admin/*` was unwanted UX — the in-shell `<AdminShell>`
// already provides per-section nav inside the page. The Admin entry
// must behave EXACTLY like the Settings entry: a single link with no
// sub-list at any route, regardless of where the gravatar dropdown is.
it("does not render admin entries at all for a regular user", () => {
const html = render({ role: "USER", pathname: "/admin/system-status" });
expect(html).not.toContain('href="/admin"');
for (const section of ADMIN_SECTIONS) {
expect(html).not.toContain(`href="/admin/${section.slug}"`);
}
});
it("for an admin off /admin/* shows a single Admin link without sub-items", () => {
const html = render({ role: "ADMIN", pathname: "/" });
expect(html).toContain('href="/admin"');
// No sub-route links — sidebar collapses into a single Admin entry.
for (const section of ADMIN_SECTIONS) {
expect(html).not.toContain(`href="/admin/${section.slug}"`);
}
});
it("for an admin on /admin/* still shows ONLY the single Admin link (no sub-list)", () => {
const html = render({ role: "ADMIN", pathname: "/admin/system-status" });
expect(html).toContain('href="/admin"');
// Sub-section links must NOT appear in the global sidebar — they
// belong to `<AdminShell>`'s in-page nav. the maintainer reported the
// expansion as broken UX in v1.4.16; this guard keeps it gone.
for (const section of ADMIN_SECTIONS) {
expect(html).not.toContain(`href="/admin/${section.slug}"`);
}
});
it("on the /admin overview page also shows ONLY the single Admin link", () => {
const html = render({ role: "ADMIN", pathname: "/admin" });
expect(html).toContain('href="/admin"');
for (const section of ADMIN_SECTIONS) {
expect(html).not.toContain(`href="/admin/${section.slug}"`);
}
});
it("Admin entry markup mirrors Settings entry markup (same shape, no expansion)", () => {
// Both Settings and Admin should render as a single <a> with no
// adjacent <ul> sub-list. Counting <ul aria-label="Admin sections"
// (or any descendant ul under the Admin link) confirms there's no
// disclosure widget. We assert by checking the structural pattern:
// the Admin link is followed by the Settings link (or the user
// section), never by a <ul>.
const html = render({ role: "ADMIN", pathname: "/admin/system-status" });
// No sub-list <ul> on /admin/*: previously this was the
// `aria-label={t("admin.shell.sectionsNav")}` ul; assert the only
// Settings-bearing nav surface is intact (sectionsNav label belongs
// to `<AdminShell>`, which the sidebar must NOT echo).
expect(html).not.toMatch(/aria-label="Admin sections"/);
});
});