diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..ffa1f37 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,113 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; +} + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; +} + + + +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { + hasError: true, + error, + }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Error caught by ErrorBoundary:', error, errorInfo); + + this.setState({ + error, + errorInfo, + }); + } + + handleReset = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+
+

+ Something went wrong +

+

+ An unexpected error occurred. The error has been reported. +

+
+ +
+
+ + Error details + +
+
+ Message: +

+ {this.state.error?.message} +

+
+ {this.state.error?.stack && ( +
+ Stack trace: +
+                        {this.state.error.stack}
+                      
+
+ )} +
+
+
+ +
+ + +
+
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index f322051..e263904 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,10 +3,15 @@ import { createRoot } from 'react-dom/client'; // Import polyfills first import './lib/polyfills.ts'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; import App from './App.tsx'; import './index.css'; // FIXME: a custom font should be used. Eg: // import '@fontsource-variable/'; -createRoot(document.getElementById("root")!).render(); +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/src/test/ErrorBoundary.test.tsx b/src/test/ErrorBoundary.test.tsx new file mode 100644 index 0000000..3f49427 --- /dev/null +++ b/src/test/ErrorBoundary.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; + + + +// Test component that throws an error +const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error('Test error'); + } + return
No error
; +}; + +describe('ErrorBoundary', () => { + it('renders children when no error occurs', () => { + render( + +
Test content
+
+ ); + + expect(screen.getByText('Test content')).toBeInTheDocument(); + }); + + it('catches and displays error when child throws', () => { + // Suppress console.error for this test + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + + ); + + expect(screen.getByText('Something went wrong')).toBeInTheDocument(); + expect(screen.getByText('An unexpected error occurred. The error has been reported.')).toBeInTheDocument(); + + consoleSpy.mockRestore(); + }); + + + + it('uses custom fallback when provided', () => { + const customFallback =
Custom error message
; + + // Suppress console.error for this test + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + + ); + + expect(screen.getByText('Custom error message')).toBeInTheDocument(); + + consoleSpy.mockRestore(); + }); +}); \ No newline at end of file