diff --git a/frontend/tests/components/common/ErrorBoundary.test.tsx b/frontend/tests/components/common/ErrorBoundary.test.tsx
new file mode 100644
index 0000000..7530aa0
--- /dev/null
+++ b/frontend/tests/components/common/ErrorBoundary.test.tsx
@@ -0,0 +1,401 @@
+/**
+ * ErrorBoundary Component Tests
+ *
+ * Tests for the error boundary component that catches JavaScript errors
+ * in child components and displays fallback UI.
+ */
+
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { ErrorBoundary } from '@/components/common/ErrorBoundary';
+
+// Component that throws an error for testing
+function ThrowingComponent({ shouldThrow = true }: { shouldThrow?: boolean }) {
+ if (shouldThrow) {
+ throw new Error('Test error message');
+ }
+ return
Child content
;
+}
+
+describe('ErrorBoundary', () => {
+ // Suppress console.error during tests since we're testing error handling
+ const originalError = console.error;
+
+ beforeAll(() => {
+ console.error = jest.fn();
+ });
+
+ afterAll(() => {
+ console.error = originalError;
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when no error occurs', () => {
+ it('renders children normally', () => {
+ render(
+
+ Test content
+
+ );
+
+ expect(screen.getByText('Test content')).toBeInTheDocument();
+ });
+
+ it('does not show error UI', () => {
+ render(
+
+ Test content
+
+ );
+
+ expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
+ });
+
+ it('renders multiple children', () => {
+ render(
+
+ First child
+ Second child
+
+ );
+
+ expect(screen.getByText('First child')).toBeInTheDocument();
+ expect(screen.getByText('Second child')).toBeInTheDocument();
+ });
+ });
+
+ describe('when an error occurs', () => {
+ it('catches the error and shows default fallback UI', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ expect(screen.getByText(/An unexpected error occurred/)).toBeInTheDocument();
+ });
+
+ it('displays the error message', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Test error message')).toBeInTheDocument();
+ });
+
+ it('shows the reset button by default', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();
+ });
+
+ it('logs error to console', () => {
+ render(
+
+
+
+ );
+
+ expect(console.error).toHaveBeenCalledWith(
+ 'ErrorBoundary caught an error:',
+ expect.any(Error),
+ expect.objectContaining({ componentStack: expect.any(String) })
+ );
+ });
+
+ it('hides the throwing component', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.queryByText('Child content')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('custom fallback', () => {
+ it('renders custom fallback when provided', () => {
+ render(
+ Custom error UI}>
+
+
+ );
+
+ expect(screen.getByText('Custom error UI')).toBeInTheDocument();
+ expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
+ });
+
+ it('does not show default fallback when custom fallback is provided', () => {
+ render(
+ Fallback}>
+
+
+ );
+
+ expect(screen.queryByRole('button', { name: /try again/i })).not.toBeInTheDocument();
+ });
+ });
+
+ describe('onError callback', () => {
+ it('calls onError when an error occurs', () => {
+ const onError = jest.fn();
+
+ render(
+
+
+
+ );
+
+ expect(onError).toHaveBeenCalledTimes(1);
+ expect(onError).toHaveBeenCalledWith(
+ expect.objectContaining({ message: 'Test error message' }),
+ expect.objectContaining({ componentStack: expect.any(String) })
+ );
+ });
+
+ it('does not call onError when no error occurs', () => {
+ const onError = jest.fn();
+
+ render(
+
+ Normal content
+
+ );
+
+ expect(onError).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('reset functionality', () => {
+ it('resets error state when reset button is clicked', async () => {
+ const user = userEvent.setup();
+
+ // Track whether we should throw
+ let shouldThrow = true;
+
+ function ResettableThrowingComponent() {
+ if (shouldThrow) {
+ throw new Error('Resettable error');
+ }
+ return Recovered content
;
+ }
+
+ render(
+
+
+
+ );
+
+ // Should show error UI after first render throws
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ expect(screen.getByText('Resettable error')).toBeInTheDocument();
+
+ // Stop throwing before clicking reset
+ shouldThrow = false;
+
+ // Click reset
+ await user.click(screen.getByRole('button', { name: /try again/i }));
+
+ // After reset, the component re-renders without throwing
+ expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
+ expect(screen.getByText('Recovered content')).toBeInTheDocument();
+ });
+ });
+
+ describe('showReset prop', () => {
+ it('hides reset button when showReset is false', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /try again/i })).not.toBeInTheDocument();
+ });
+
+ it('shows reset button when showReset is true', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();
+ });
+
+ it('shows reset button by default (showReset undefined)', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();
+ });
+ });
+
+ describe('error without message', () => {
+ it('handles error with null message gracefully', () => {
+ function ThrowNullError() {
+ const error = new Error();
+ error.message = '';
+ throw error;
+ }
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ // Empty message should still render the error container but be empty
+ });
+ });
+
+ describe('accessibility', () => {
+ it('has descriptive error title', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ });
+
+ it('has descriptive help text', () => {
+ render(
+
+
+
+ );
+
+ expect(
+ screen.getByText(/An unexpected error occurred. Please try again or contact support/)
+ ).toBeInTheDocument();
+ });
+
+ it('icons have aria-hidden', () => {
+ render(
+
+
+
+ );
+
+ const icons = document.querySelectorAll('[aria-hidden="true"]');
+ expect(icons.length).toBeGreaterThan(0);
+ });
+
+ it('reset button is keyboard accessible', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ // First render without error to set up the component
+ expect(screen.getByText('Child content')).toBeInTheDocument();
+ });
+ });
+
+ describe('DefaultFallback rendering', () => {
+ it('shows error in monospace font', () => {
+ render(
+
+
+
+ );
+
+ const errorMessage = screen.getByText('Test error message');
+ expect(errorMessage).toHaveClass('font-mono');
+ });
+
+ it('renders within a Card component', () => {
+ render(
+
+
+
+ );
+
+ // The card should have destructive styling
+ const card = document.querySelector('.border-destructive\\/50');
+ expect(card).toBeInTheDocument();
+ });
+ });
+
+ describe('edge cases', () => {
+ it('handles deeply nested errors', () => {
+ function DeepChild() {
+ throw new Error('Deep error');
+ }
+
+ function MiddleComponent() {
+ return (
+
+
+
+ );
+ }
+
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ expect(screen.getByText('Deep error')).toBeInTheDocument();
+ });
+
+ it('isolates errors to its own boundary', () => {
+ render(
+
+
+
+
+
Sibling content
+
+ );
+
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument();
+ expect(screen.getByText('Sibling content')).toBeInTheDocument();
+ });
+
+ it('allows nested error boundaries', () => {
+ function InnerThrowing() {
+ throw new Error('Inner error');
+ }
+
+ render(
+ Outer fallback}>
+
+ Inner fallback
}>
+
+
+
+
+ );
+
+ // Inner boundary should catch the error
+ expect(screen.getByText('Inner fallback')).toBeInTheDocument();
+ expect(screen.queryByText('Outer fallback')).not.toBeInTheDocument();
+ });
+ });
+});