From 52c65c7365849bacb6b675c37b39ec375c2c60a9 Mon Sep 17 00:00:00 2001 From: Tijesunimi004 Date: Thu, 23 Jul 2026 16:02:57 +0100 Subject: [PATCH] Add error boundary around trade panel buy/sell controls An unhandled render error in the trade buy/sell buttons previously propagated up to the shared Creator Profile section boundary, taking out profile facts and stats along with the panel. Wrap the desktop and mobile trade panel blocks in a dedicated TradePanelErrorBoundary so a panel failure shows a compact inline fallback with a retry button while the rest of the page stays mounted. --- .../common/TradePanelErrorBoundary.tsx | 64 ++++++++ .../TradePanelErrorBoundary.test.tsx | 65 ++++++++ src/pages/LandingPage.tsx | 145 +++++++++--------- 3 files changed, 204 insertions(+), 70 deletions(-) create mode 100644 src/components/common/TradePanelErrorBoundary.tsx create mode 100644 src/components/common/__tests__/TradePanelErrorBoundary.test.tsx diff --git a/src/components/common/TradePanelErrorBoundary.tsx b/src/components/common/TradePanelErrorBoundary.tsx new file mode 100644 index 00000000..27d0f05e --- /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 00000000..26fabee2 --- /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 9c26ce8b..cf98c6b9 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 +
+
+ )} +
+