diff --git a/web-app/src/containers/GlobalError.tsx b/web-app/src/containers/GlobalError.tsx new file mode 100644 index 000000000..d741b462d --- /dev/null +++ b/web-app/src/containers/GlobalError.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react' + +interface GlobalErrorProps { + error: Error | unknown +} + +export default function GlobalError({ error }: GlobalErrorProps) { + console.error('Error in root route:', error) + const [showFull, setShowFull] = useState(false) + + return ( +
+
+
+
+ + + + +
+
+

+ Oops! Unexpected error occurred. +

+

+ Something went wrong. Try to{' '} + {' '} + or
feel free to{' '} + + contact us + {' '} + if the problem persists. +

+
+ Error: + + {error instanceof Error ? error.message : String(error)} + +
+
+              
+                {error instanceof Error
+                  ? showFull
+                    ? error.stack
+                    : error.stack?.slice(0, 200)
+                  : String(error)}
+              
+            
+ +
+
+
+
+ ) +} diff --git a/web-app/src/containers/__tests__/GlobalError.test.tsx b/web-app/src/containers/__tests__/GlobalError.test.tsx new file mode 100644 index 000000000..1ac29eff0 --- /dev/null +++ b/web-app/src/containers/__tests__/GlobalError.test.tsx @@ -0,0 +1,106 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import GlobalError from '../GlobalError' +import '@testing-library/jest-dom' + +describe('GlobalError Component', () => { + it('should render error message for Error instance', () => { + const error = new Error('Test error message') + render() + + expect(screen.getByText('Oops! Unexpected error occurred.')).toBeDefined() + expect(screen.getByText('Test error message')).toBeDefined() + }) + + it('should render error message for non-Error instance', () => { + const error = 'String error message' + render() + + expect(screen.getByText('Oops! Unexpected error occurred.')).toBeDefined() + expect(screen.getAllByText('String error message')).toHaveLength(2) + }) + + it('should show truncated stack trace initially', () => { + const error = new Error('Test error') + error.stack = 'a'.repeat(300) + render() + + const stackTrace = screen.getByText('a'.repeat(200)) + expect(stackTrace).toBeDefined() + }) + + it('should toggle between truncated and full stack trace', () => { + const error = new Error('Test error') + error.stack = 'a'.repeat(300) + render() + + const showMoreButton = screen.getByText('Show more') + fireEvent.click(showMoreButton) + + expect(screen.getByText('a'.repeat(300))).toBeDefined() + expect(screen.getByText('Show less')).toBeDefined() + + const showLessButton = screen.getByText('Show less') + fireEvent.click(showLessButton) + + expect(screen.getByText('a'.repeat(200))).toBeDefined() + expect(screen.getByText('Show more')).toBeDefined() + }) + + it('should handle refresh page button click', () => { + const originalLocation = window.location + const reloadSpy = vi.fn() + + delete (window as any).location + ;(window as any).location = { ...originalLocation, reload: reloadSpy } + + const error = new Error('Test error') + render() + + const refreshButton = screen.getByText('refresh this page') + fireEvent.click(refreshButton) + + expect(reloadSpy).toHaveBeenCalledTimes(1) + ;(window as any).location = originalLocation + }) + + it('should render contact us link with correct href', () => { + const error = new Error('Test error') + render() + + const contactLink = screen.getByText('contact us') + expect(contactLink).toHaveAttribute('href', 'https://discord.gg/FTk2MvZwJH') + expect(contactLink).toHaveAttribute('target', '_blank') + expect(contactLink).toHaveAttribute('rel', 'noopener noreferrer') + }) + + it('should log error to console', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const error = new Error('Test error') + + render() + + expect(consoleSpy).toHaveBeenCalledWith('Error in root route:', error) + consoleSpy.mockRestore() + }) + + it('should render proper error structure with styling', () => { + const error = new Error('Test error') + render() + + const errorContainer = screen.getByRole('alert') + expect(errorContainer).toHaveClass( + 'mt-5', + 'w-full', + 'md:w-4/5', + 'mx-auto', + 'rounded', + 'border', + 'border-red-400', + 'bg-red-100', + 'px-4', + 'py-3', + 'text-red-700' + ) + }) +}) diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index bc432a3b7..4ca2f4623 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -28,9 +28,11 @@ import { ResizableHandle, } from '@/components/ui/resizable' import { useCallback } from 'react' +import GlobalError from '@/containers/GlobalError' export const Route = createRootRoute({ component: RootLayout, + errorComponent: ({ error }) => , }) const AppLayout = () => {