From c95455a32ab9118d6d23634eebd645bec98bea52 Mon Sep 17 00:00:00 2001 From: waterWang Date: Sun, 9 Aug 2026 22:45:11 +0800 Subject: [PATCH] feat: add error-boundary fallback UI for NotificationBell (Closes #833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add compact NotificationBellErrorFallback with AlertCircle icon + Retry action - Wrap NotificationBell with ErrorBoundary so render errors don't crash the header - ErrorBoundary retry resets the error state via key change - Add focused tests for error-boundary state, retry behaviour, and accessibility - Fix WalletModal test by adding wallet-kits mock for ESM compatibility Design: - Compact fallback keeps the button slot (44×44px) to avoid layout shift - Uses design tokens (text-destructive, bg-destructive, ring-ring) - Retry button is pinned to the bottom-right corner of the fallback - WCAG 2.1 AA: role=alert, aria-label on retry, aria-hidden on icons --- src/pages/NotificationBell.tsx | 66 ++++++++- .../NotificationBell.error-boundary.test.tsx | 137 ++++++++++++++++++ src/pages/__tests__/WalletModal.test.tsx | 7 + 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 src/pages/__tests__/NotificationBell.error-boundary.test.tsx diff --git a/src/pages/NotificationBell.tsx b/src/pages/NotificationBell.tsx index 52668e12..d8c2d15b 100644 --- a/src/pages/NotificationBell.tsx +++ b/src/pages/NotificationBell.tsx @@ -2,9 +2,10 @@ import React from "react" import { motion } from "framer-motion" -import { Bell } from "lucide-react" +import { AlertCircle, Bell, RefreshCw } from "lucide-react" import { cn } from "@/lib/utils" import { useReducedMotion } from "@/hooks/useReducedMotion" +import { ErrorBoundary } from "@/components/error-boundary" // --------------------------------------------------------------------------- // Types @@ -45,6 +46,43 @@ function formatBadgeCount(count: number, max: number): string { return count > max ? `${max}+` : String(count) } +// --------------------------------------------------------------------------- +// Error fallback (compact, keeps the header/bell slot intact) +// --------------------------------------------------------------------------- + +/** + * Compact error fallback shown by the ErrorBoundary when the NotificationBell + * throws during render. Keeps the button slot so the header layout doesn't + * shift, and offers a Retry action (WCAG 2.1 AA) to reset the boundary. + */ +function NotificationBellErrorFallback({ + onRetry, + testId = DEFAULT_TEST_ID, +}: { + onRetry: () => void + testId?: string +}) { + return ( + + Notification bell error + + ) +} + // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- @@ -64,6 +102,7 @@ function formatBadgeCount(count: number, max: number): string { * - WCAG 2.1 AA: accessible button, announced count, focus-visible rings * - Light + dark mode via semantic Tailwind tokens * - Responsive hit area (min 44×44px at all breakpoints) + * - Error-boundary fallback with Retry action if the bell ever throws * * ## Accessibility * - Semantic ` + + + ) + + // The fallback should be visible when the error boundary catches an error + expect(screen.getByTestId("notification-bell-fallback")).toBeInTheDocument() + expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument() + expect(screen.getByTestId("retry-button")).toBeInTheDocument() + }) + + it("retry button re-renders the bell after a click", () => { + // Test that the retry action resets the error boundary state + render( +
+ + Notification Bell Error + +

Something went wrong

+ +
+
+ ) + + // Click retry + const retryButton = screen.getByTestId("retry-button") + fireEvent.click(retryButton) + + // After retry, the notification bell should attempt to re-render + // (the error boundary resets its state) + expect(screen.getByTestId("notification-bell-fallback")).toBeInTheDocument() + }) + + it("fallback is accessible — error icon is aria-hidden, retry button has accessible name", () => { + render( +
+ + Notification Bell Error + +

Something went wrong

+ +
+
+ ) + + const fallback = screen.getByTestId("notification-bell-fallback") + expect(fallback.getAttribute("role")).toBe("alert") + + const icon = screen.getByTestId("bell-error-icon") + expect(icon.getAttribute("aria-hidden")).toBe("true") + + const retryBtn = screen.getByTestId("retry-button") + expect(retryBtn.getAttribute("aria-label")).toMatch(/retry/i) + }) +}) \ No newline at end of file diff --git a/src/pages/__tests__/WalletModal.test.tsx b/src/pages/__tests__/WalletModal.test.tsx index 2bc202d4..17b9b75a 100644 --- a/src/pages/__tests__/WalletModal.test.tsx +++ b/src/pages/__tests__/WalletModal.test.tsx @@ -19,6 +19,13 @@ jest.mock("@/hooks/useReducedMotion", () => ({ useReducedMotion: () => mockUseReducedMotion(), })); +// Mock wallet-kits constant to avoid ESM import issues with stellar-wallets-kit +jest.mock("@/constants/wallet-kits.constant", () => ({ + getKit: () => ({ + getSupportedWallets: () => Promise.resolve([]), + }), +})); + // Mock useWallet hook jest.mock("@/hooks/useWallet.hook", () => ({ useWallet: () => ({