diff --git a/README.md b/README.md index b6862708..f988c72b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ Built with modern web technologies and Web3 integration, this frontend serves as ### Core Capabilities - **🏠 Property Discovery**: Browse and search tokenized real estate properties with advanced filtering - **💰 Wallet Integration**: Connect MetaMask, WalletConnect, and other Web3 wallets seamlessly -- **🔗 Smart Contract Interaction**: Execute property purchases, transfers, and management through intuitive UI +- **� Route-Level Error Handling**: Full-page route fallback UI with retry and home navigation using `RouteErrorBoundary` +- **�🔗 Smart Contract Interaction**: Execute property purchases, transfers, and management through intuitive UI - **📊 Real-Time Data**: Live property valuations, market trends, and portfolio analytics - **🔐 Web3 Authentication**: Secure wallet-based authentication with multi-network support - **� Responsive Design**: Mobile-first design that works perfectly on all devices diff --git a/docs/adr/ADR-005-error-handling.md b/docs/adr/ADR-005-error-handling.md index 77cb3f68..e6878d6f 100644 --- a/docs/adr/ADR-005-error-handling.md +++ b/docs/adr/ADR-005-error-handling.md @@ -73,6 +73,7 @@ class ValidationError extends Error { - `` wraps the entire app and shows a full-page error screen for catastrophic failures - `` wraps major page sections (property list, wallet panel) and shows inline fallback UI +- `` renders route-level full-screen fallback UI for page-specific failures with retry and home navigation - `` wraps individual widgets and renders a compact error state ### Global Error Handler diff --git a/src/components/error/RouteErrorBoundary.tsx b/src/components/error/RouteErrorBoundary.tsx index 94ba58a3..36ff94bc 100644 --- a/src/components/error/RouteErrorBoundary.tsx +++ b/src/components/error/RouteErrorBoundary.tsx @@ -7,6 +7,7 @@ import { Home, RefreshCw, AlertTriangle, Search } from 'lucide-react'; import type { AppError } from '@/types/errors'; import { ErrorCategory } from '@/types/errors'; import { getWalletErrorMessage } from '@/utils/errorHandling'; +import { getRouteErrorIconConfig, getRouteErrorTitle } from './routeErrorHelpers'; interface RouteErrorBoundaryProps { error: AppError; @@ -27,51 +28,17 @@ const RouteErrorFallback: React.FC = ({ onRetry, onNavigateHome, }) => { - const getErrorIcon = () => { - switch (error.category) { - case ErrorCategory.WEB3: - return ( -
- -
- ); - case ErrorCategory.NETWORK: - return ( -
- -
- ); - default: - return ( -
- -
- ); - } - }; + const { bgClass, iconClass } = getRouteErrorIconConfig(error.category); - const getErrorTitle = () => { - switch (error.category) { - case ErrorCategory.WEB3: - return 'Wallet Connection Error'; - case ErrorCategory.NETWORK: - return 'Network Error'; - case ErrorCategory.VALIDATION: - return 'Validation Error'; - case ErrorCategory.PERMISSION: - return 'Permission Error'; - case ErrorCategory.AUTHENTICATION: - return 'Authentication Error'; - default: - return 'Something went wrong'; - } - }; + const getErrorTitle = () => getRouteErrorTitle(error.category) return (
- {getErrorIcon()} +
+ +

{getErrorTitle()} diff --git a/src/components/error/__tests__/RouteErrorBoundary.test.tsx b/src/components/error/__tests__/RouteErrorBoundary.test.tsx new file mode 100644 index 00000000..a04b72f6 --- /dev/null +++ b/src/components/error/__tests__/RouteErrorBoundary.test.tsx @@ -0,0 +1,67 @@ +const pushMock = jest.fn() + +jest.mock('next/navigation', () => ({ + useRouter: () => ({ + push: pushMock, + replace: jest.fn(), + refresh: jest.fn(), + back: jest.fn(), + forward: jest.fn(), + prefetch: jest.fn(), + }), + useSearchParams: () => new URLSearchParams(), + usePathname: () => '/', +})) + +import React from 'react' +import userEvent from '@testing-library/user-event' +import { render, screen } from '@testing-library/react' +import { RouteErrorBoundary } from '@/components/error/RouteErrorBoundary' +import { ErrorCategory, ErrorSeverity, type AppError } from '@/types/errors' + +describe('RouteErrorBoundary', () => { + const resetError = jest.fn() + const mockError = { + id: 'error-123', + category: ErrorCategory.NETWORK, + severity: ErrorSeverity.HIGH, + message: 'Unable to reach the network', + userMessage: 'Cannot reach network at this time.', + timestamp: new Date(), + context: { endpoint: '/properties' }, + isRecoverable: false, + shouldReport: true, + } as AppError + + beforeEach(() => { + pushMock.mockClear() + resetError.mockClear() + }) + + it('renders the expected error UI for a network error', () => { + render() + + expect(screen.getByRole('heading', { name: /Network Error/i })).toBeInTheDocument() + expect(screen.getByText(/Cannot reach network at this time./i)).toBeInTheDocument() + expect(screen.getByText(/Error in:/i)).toBeInTheDocument() + expect(screen.getByText('Properties')).toBeInTheDocument() + expect(screen.getByText(/Error Details/i)).toBeInTheDocument() + }) + + it('calls resetError when the retry button is clicked', async () => { + render() + + await userEvent.click(screen.getByRole('button', { name: /try again/i })) + + expect(resetError).toHaveBeenCalledTimes(1) + }) + + it('navigates home and resets the error when the home button is clicked', async () => { + render() + + await userEvent.click(screen.getByRole('button', { name: /go home/i })) + + expect(pushMock).toHaveBeenCalledWith('/') + expect(resetError).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/components/error/__tests__/routeErrorHelpers.test.ts b/src/components/error/__tests__/routeErrorHelpers.test.ts new file mode 100644 index 00000000..c1f38821 --- /dev/null +++ b/src/components/error/__tests__/routeErrorHelpers.test.ts @@ -0,0 +1,41 @@ +import { ErrorCategory } from '@/types/errors' +import { getRouteErrorIconConfig, getRouteErrorTitle } from '@/components/error/routeErrorHelpers' + +describe('routeErrorHelpers', () => { + describe('getRouteErrorTitle', () => { + it('returns the expected title for WEB3 errors', () => { + expect(getRouteErrorTitle(ErrorCategory.WEB3)).toBe('Wallet Connection Error') + }) + + it('returns the expected title for NETWORK errors', () => { + expect(getRouteErrorTitle(ErrorCategory.NETWORK)).toBe('Network Error') + }) + + it('returns a fallback title for unknown error categories', () => { + expect(getRouteErrorTitle(ErrorCategory.UNKNOWN)).toBe('Something went wrong') + }) + }) + + describe('getRouteErrorIconConfig', () => { + it('returns orange styling for WEB3 errors', () => { + expect(getRouteErrorIconConfig(ErrorCategory.WEB3)).toEqual({ + bgClass: 'bg-orange-100 dark:bg-orange-900/20', + iconClass: 'text-orange-600 dark:text-orange-400', + }) + }) + + it('returns red styling for NETWORK errors', () => { + expect(getRouteErrorIconConfig(ErrorCategory.NETWORK)).toEqual({ + bgClass: 'bg-red-100 dark:bg-red-900/20', + iconClass: 'text-red-600 dark:text-red-400', + }) + }) + + it('returns neutral styling for unknown categories', () => { + expect(getRouteErrorIconConfig(ErrorCategory.UNKNOWN)).toEqual({ + bgClass: 'bg-gray-100 dark:bg-gray-800', + iconClass: 'text-gray-600 dark:text-gray-400', + }) + }) + }) +}) diff --git a/src/components/error/routeErrorHelpers.ts b/src/components/error/routeErrorHelpers.ts new file mode 100644 index 00000000..7239ff9c --- /dev/null +++ b/src/components/error/routeErrorHelpers.ts @@ -0,0 +1,43 @@ +import { ErrorCategory } from '@/types/errors' + +export interface RouteErrorIconConfig { + bgClass: string + iconClass: string +} + +export const getRouteErrorIconConfig = (category: ErrorCategory): RouteErrorIconConfig => { + switch (category) { + case ErrorCategory.WEB3: + return { + bgClass: 'bg-orange-100 dark:bg-orange-900/20', + iconClass: 'text-orange-600 dark:text-orange-400', + } + case ErrorCategory.NETWORK: + return { + bgClass: 'bg-red-100 dark:bg-red-900/20', + iconClass: 'text-red-600 dark:text-red-400', + } + default: + return { + bgClass: 'bg-gray-100 dark:bg-gray-800', + iconClass: 'text-gray-600 dark:text-gray-400', + } + } +} + +export const getRouteErrorTitle = (category: ErrorCategory): string => { + switch (category) { + case ErrorCategory.WEB3: + return 'Wallet Connection Error' + case ErrorCategory.NETWORK: + return 'Network Error' + case ErrorCategory.VALIDATION: + return 'Validation Error' + case ErrorCategory.PERMISSION: + return 'Permission Error' + case ErrorCategory.AUTHENTICATION: + return 'Authentication Error' + default: + return 'Something went wrong' + } +}