fix(a11y): Name der Glocke enthält die sichtbare Zahl (Lighthouse label-content-name-mismatch)
Backend Image / build-and-push (push) Successful in 5m4s
Frontend Image / build-and-push (push) Successful in 15m12s

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-19 14:02:31 +02:00
1 parent 427969d8b9
commit d8efbc34fa
2 files changed
+18 -13

No files matched your search

@@ -60,7 +60,7 @@ describe("NotificationBell", () => {
notification({ id: "n-2", message: "Ohne Ziel", relatedType: "Colony", relatedId: "c-9" }), notification({ id: "n-2", message: "Ohne Ziel", relatedType: "Colony", relatedId: "c-9" }),
]); ]);
renderBell(); renderBell();
await userEvent.click(await screen.findByRole("button", { name: "Benachrichtigungen" })); await userEvent.click(await screen.findByRole("button", { name: /^Benachrichtigungen/ }));
const linked = await screen.findByText("Wartezeit-Konflikt bei B3"); const linked = await screen.findByText("Wartezeit-Konflikt bei B3");
expect(linked.closest("a")).toHaveAttribute("href", "/harvests/h-1"); expect(linked.closest("a")).toHaveAttribute("href", "/harvests/h-1");
@@ -76,7 +76,7 @@ describe("NotificationBell", () => {
notification({ id: "n-2", message: "Schema", link: "https://evil.example" }), notification({ id: "n-2", message: "Schema", link: "https://evil.example" }),
]); ]);
renderBell(); renderBell();
await userEvent.click(await screen.findByRole("button", { name: "Benachrichtigungen" })); await userEvent.click(await screen.findByRole("button", { name: /^Benachrichtigungen/ }));
expect((await screen.findByText("Wartezeit-Konflikt bei B3")).closest("a")).toBeNull(); expect((await screen.findByText("Wartezeit-Konflikt bei B3")).closest("a")).toBeNull();
expect(screen.getByText("Schema").closest("a")).toBeNull(); expect(screen.getByText("Schema").closest("a")).toBeNull();
}); });
@@ -84,7 +84,7 @@ describe("NotificationBell", () => {
it("hides „Alle gelesen“ when nothing is unread and names the category in words", async () => { it("hides „Alle gelesen“ when nothing is unread and names the category in words", async () => {
vi.mocked(fetchNotifications).mockResolvedValue([notification({ read: true })]); vi.mocked(fetchNotifications).mockResolvedValue([notification({ read: true })]);
renderBell(); renderBell();
await userEvent.click(await screen.findByRole("button", { name: "Benachrichtigungen" })); await userEvent.click(await screen.findByRole("button", { name: /^Benachrichtigungen/ }));
expect(await screen.findByText("Wartezeit")).toBeInTheDocument(); expect(await screen.findByText("Wartezeit")).toBeInTheDocument();
expect(screen.queryByRole("menuitem", { name: /Alle gelesen/ })).not.toBeInTheDocument(); expect(screen.queryByRole("menuitem", { name: /Alle gelesen/ })).not.toBeInTheDocument();
}); });
@@ -120,10 +120,12 @@ describe("NotificationBell", () => {
expect(vi.mocked(toast.error).mock.calls[0][1]).toEqual({ id: "notification-read-error" }); expect(vi.mocked(toast.error).mock.calls[0][1]).toEqual({ id: "notification-read-error" });
}); });
it("the badge is announced with the unread count, the digit is hidden from readers", async () => { it("the button name carries the visible badge text, the digit itself is hidden from readers", async () => {
vi.mocked(fetchNotifications).mockResolvedValue([notification({}), notification({ id: "n-2" })]); vi.mocked(fetchNotifications).mockResolvedValue([notification({}), notification({ id: "n-2" })]);
renderBell(); renderBell();
expect(await screen.findByText("2 ungelesene Benachrichtigungen")).toHaveClass("sr-only"); expect(
await screen.findByRole("button", { name: "Benachrichtigungen, 2 ungelesen" }),
).toBeInTheDocument();
expect(screen.getByText("2", { selector: "[aria-hidden='true']" })).toBeInTheDocument(); expect(screen.getByText("2", { selector: "[aria-hidden='true']" })).toBeInTheDocument();
}); });
@@ -109,19 +109,22 @@ export function NotificationBell() {
variant="ghost" variant="ghost"
size="icon" size="icon"
className="relative" className="relative"
aria-label="Benachrichtigungen" // The name contains the visible badge text (WCAG 2.5.3).
aria-label={
unreadCount > 0
? `Benachrichtigungen, ${unreadCount > 9 ? "9+" : unreadCount} ungelesen`
: "Benachrichtigungen"
}
/> />
} }
> >
<Bell className="size-4.5" /> <Bell className="size-4.5" />
{unreadCount > 0 ? ( {unreadCount > 0 ? (
<span className="absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-destructive px-1 text-xs leading-none font-medium text-destructive-foreground"> <span
<span aria-hidden="true"> aria-hidden="true"
{unreadCount > 9 ? "9+" : unreadCount} className="absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-destructive px-1 text-xs leading-none font-medium text-destructive-foreground"
</span> >
<span className="sr-only"> {unreadCount > 9 ? "9+" : unreadCount}
{unreadCount} ungelesene Benachrichtigungen
</span>
</span> </span>
) : null} ) : null}
</DropdownMenuTrigger> </DropdownMenuTrigger>