fix(dashboard): disambiguate quick-add submenu labels (no more double "Hinzufügen")

The dashboard "Add" dropdown rendered two menu items whose visible text
both resolved to bare nouns ("Messung" / "Stimmungseintrag" in DE,
"Measurement" / "Mood entry" in EN). Combined with the trigger label
("Hinzufügen" / "Add"), screen-reader users effectively heard the same
verb three times in a row with no signal which row did what.

Update both translations to self-contained verb-phrases:
- DE: "Messung erfassen" / "Stimmung erfassen"
- EN: "Log measurement" / "Log mood"

Add `src/app/__tests__/quick-add-labels.test.ts` to lock the contract:
both labels must be non-empty, distinct from each other, and distinct
from `common.add` (the trigger). The test loads `messages/{en,de}.json`
directly so any future translator who collapses the labels back to
"Hinzufügen" or "Add" gets a red bar before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 20:16:57 +02:00
1 parent 786b3951f6
commit 3e45a7ba53
5 files changed
+174 -19

No files matched your search

@@ -10,17 +10,20 @@ vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));
// Auth + theme + logout — the user section is rendered inside the
// sidebar and otherwise pulls in TanStack Query + the auth endpoint.
// 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: {
id: "u1",
username: "marc",
email: "marc@example.com",
role: "USER",
gravatarUrl: null,
},
user: mockUserRef.value,
isAuthenticated: true,
isLoading: false,
refetch: vi.fn(),
@@ -36,7 +39,7 @@ vi.mock("@/components/providers", () => ({
}),
}));
// The flag-under-test. Each test mutates `mockSettingsRef.value` before
// 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", () => ({
@@ -45,13 +48,20 @@ vi.mock("@/components/app-settings-provider", () => ({
import { I18nProvider } from "@/lib/i18n/context";
import { SidebarNav } from "../sidebar-nav";
import { ADMIN_SECTIONS } from "@/components/admin/admin-shell";
function render({
pathname = "/",
bugReportEnabled = true,
}: { pathname?: string; bugReportEnabled?: boolean } = {}) {
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 />
@@ -73,3 +83,42 @@ describe("<SidebarNav> bug-report toggle", () => {
expect(html).not.toContain('href="/bugreport"');
});
});
describe("<SidebarNav> admin sub-items context-awareness", () => {
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/* expands the section sub-list", () => {
const html = render({ role: "ADMIN", pathname: "/admin/system-status" });
expect(html).toContain('href="/admin"');
// All section sub-items must render — the global sidebar mirrors
// the in-shell sidebar so users can jump between sections.
for (const section of ADMIN_SECTIONS) {
expect(html).toContain(`href="/admin/${section.slug}"`);
}
});
it("expands sub-items on the /admin overview page (mirrors in-shell sidebar)", () => {
// The in-shell `<AdminShell>` sidebar already lists every section on
// `/admin`. The global sidebar must mirror that so the navigation
// experience is identical between the two viewports.
const html = render({ role: "ADMIN", pathname: "/admin" });
for (const section of ADMIN_SECTIONS) {
expect(html).toContain(`href="/admin/${section.slug}"`);
}
});
});