/** * 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', () => { 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(); }); }); });