From 0591157e35fbc69cba64dbd73afc14694a3c644c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 02:00:05 +0000 Subject: [PATCH] feat(security): add global error boundary for defense in depth - Adds `ErrorBoundary` component to catch unhandled React exceptions - Prevents UI crashes (White Screen of Death) - Ensures stack traces are not leaked to the user (Security Enhancement) - Wraps `App` in `main.tsx` - Adds tests verifying fallback UI and absence of error details in DOM Co-authored-by: ragusa-it <196988693+ragusa-it@users.noreply.github.com> --- .../layout/ErrorBoundary.module.css | 23 +++++++++ src/components/layout/ErrorBoundary.tsx | 49 +++++++++++++++++++ .../layout/__tests__/ErrorBoundary.test.tsx | 44 +++++++++++++++++ src/components/layout/index.ts | 1 + src/main.tsx | 5 +- 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/components/layout/ErrorBoundary.module.css create mode 100644 src/components/layout/ErrorBoundary.tsx create mode 100644 src/components/layout/__tests__/ErrorBoundary.test.tsx diff --git a/src/components/layout/ErrorBoundary.module.css b/src/components/layout/ErrorBoundary.module.css new file mode 100644 index 0000000..8463763 --- /dev/null +++ b/src/components/layout/ErrorBoundary.module.css @@ -0,0 +1,23 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + width: 100%; + text-align: center; + padding: 2rem; + background-color: var(--md-sys-color-background); + color: var(--md-sys-color-on-background); +} + +.title { + font-size: 2rem; + margin-bottom: 1rem; + color: var(--md-sys-color-error); +} + +.message { + margin-bottom: 2rem; + color: var(--md-sys-color-on-surface-variant); +} diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx new file mode 100644 index 0000000..69a930d --- /dev/null +++ b/src/components/layout/ErrorBoundary.tsx @@ -0,0 +1,49 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react'; +import { Button } from '../ui'; +import styles from './ErrorBoundary.module.css'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(_: Error): State { + return { hasError: true }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Log error securely - do not expose to user + console.error('Uncaught error:', error, errorInfo); + } + + handleReload = () => { + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

+ We apologize for the inconvenience. Please try reloading the page. +

+ +
+ ); + } + + return this.props.children; + } +} diff --git a/src/components/layout/__tests__/ErrorBoundary.test.tsx b/src/components/layout/__tests__/ErrorBoundary.test.tsx new file mode 100644 index 0000000..65aa0cd --- /dev/null +++ b/src/components/layout/__tests__/ErrorBoundary.test.tsx @@ -0,0 +1,44 @@ +// @vitest-environment jsdom +import { render, screen, cleanup } from '@testing-library/react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { ErrorBoundary } from '../ErrorBoundary'; + +// Mock component that throws +const ThrowError = () => { + throw new Error('Test Error'); +}; + +describe('ErrorBoundary', () => { + afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + }); + + it('renders children when there is no error', () => { + render( + +
Content
+
+ ); + expect(screen.getByText('Content')).toBeTruthy(); + }); + + it('renders fallback UI when there is an error', () => { + // Prevent console.error from cluttering output during this test + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + + ); + + expect(screen.getByText('Something went wrong')).toBeTruthy(); + expect(screen.getByRole('button', { name: /reload page/i })).toBeTruthy(); + + // Ensure stack trace/error message is NOT rendered (security check) + expect(screen.queryByText('Test Error')).toBeNull(); + + consoleSpy.mockRestore(); + }); +}); diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index d848104..9d04b5e 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -2,3 +2,4 @@ export { Navbar } from './Navbar'; export { Footer } from './Footer'; export { FancyCursor } from './FancyCursor'; export { ScrollToTop } from './ScrollToTop'; +export { ErrorBoundary } from './ErrorBoundary'; diff --git a/src/main.tsx b/src/main.tsx index 185abac..599a129 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,12 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; +import { ErrorBoundary } from './components/layout'; createRoot(document.getElementById('root')!).render( - + + + );