diff --git a/src/components/common/TradePanelErrorBoundary.tsx b/src/components/common/TradePanelErrorBoundary.tsx new file mode 100644 index 0000000..27d0f05 --- /dev/null +++ b/src/components/common/TradePanelErrorBoundary.tsx @@ -0,0 +1,64 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; +import { RotateCcw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import TransactionStatusIcon from '@/components/common/TransactionStatusIcon'; +import { cn } from '@/lib/utils'; + +interface Props { + children: ReactNode; + className?: string; +} + +interface State { + hasError: boolean; +} + +class TradePanelErrorBoundary extends Component { + public state: State = { hasError: false }; + + public static getDerivedStateFromError(): State { + return { hasError: true }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error in trade panel:', error, errorInfo); + } + + private handleRetry = () => { + this.setState({ hasError: false }); + }; + + public render() { + if (this.state.hasError) { + return ( +
+ +

+ Trading is unavailable right now. +

+ +
+ ); + } + + return this.props.children; + } +} + +export default TradePanelErrorBoundary; diff --git a/src/components/common/__tests__/TradePanelErrorBoundary.test.tsx b/src/components/common/__tests__/TradePanelErrorBoundary.test.tsx new file mode 100644 index 0000000..26fabee --- /dev/null +++ b/src/components/common/__tests__/TradePanelErrorBoundary.test.tsx @@ -0,0 +1,65 @@ +import { describe, expect, it, vi, afterEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import TradePanelErrorBoundary from '@/components/common/TradePanelErrorBoundary'; + +const BuggyTradePanel = ({ shouldThrow = false }: { shouldThrow?: boolean }) => { + if (shouldThrow) { + throw new Error('Test trade panel error'); + } + return
Buy Sell Buttons
; +}; + +describe('TradePanelErrorBoundary', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renders children when no error occurs', () => { + render( + + + + ); + expect(screen.getByText('Buy Sell Buttons')).toBeInTheDocument(); + }); + + it('renders fallback UI when a render error occurs', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + + ); + + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText(/trading is unavailable right now/i)).toBeInTheDocument(); + expect(screen.queryByText('Buy Sell Buttons')).not.toBeInTheDocument(); + expect(consoleSpy).toHaveBeenCalled(); + }); + + it('resets error state and remounts the panel when Retry is clicked', () => { + vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { rerender } = render( + + + + ); + + expect(screen.getByText(/trading is unavailable right now/i)).toBeInTheDocument(); + + rerender( + + + + ); + + fireEvent.click(screen.getByRole('button', { name: /retry/i })); + + expect( + screen.queryByText(/trading is unavailable right now/i) + ).not.toBeInTheDocument(); + expect(screen.getByText('Buy Sell Buttons')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 9c26ce8..cf98c6b 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -42,6 +42,7 @@ import PrecisionModeToggle, { } from '@/components/common/PrecisionModeToggle'; import ScrollToTop from '@/components/common/ScrollToTop'; import SectionErrorBoundary from '@/components/common/SectionErrorBoundary'; +import TradePanelErrorBoundary from '@/components/common/TradePanelErrorBoundary'; import StaleDataWarning from '@/components/common/StaleDataWarning'; import { useScrollPreservation } from '@/hooks/useScrollPreservation'; import { useStaleData } from '@/hooks/useStaleData'; @@ -1278,44 +1279,46 @@ function LandingPage() { } /> {isNetworkMismatch && } -
-
- - -
- {tradeSubmitting && ( -
-
-
- Submitting trade -
+ +
- )} -
+ {tradeSubmitting && ( +
+
+
+ Submitting trade +
+
+ )} +
+
)} @@ -1357,42 +1360,44 @@ function LandingPage() {
-
-
- - -
- {tradeSubmitting && ( -
-
-
- Submitting trade -
+ +
- )} -
+ {tradeSubmitting && ( +
+
+
+ Submitting trade +
+
+ )} +
+