Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
356 changes: 356 additions & 0 deletions dashboard/src/__tests__/ErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
/**
* Comprehensive Unit tests for ErrorBoundary component
*
* Coverage Target:
* - Statements: ≥80% (currently 0/16, target: 13+/16)
* - Branches: ≥70% (currently 0/9, target: 7+/9)
* - Functions: ≥80% (currently 0/6, target: 5+/6)
* - Lines: ≥80% (currently 0/15, target: 12+/15)
*/

import React from 'react'
import { render, screen, fireEvent } from '@utils/test-utils'
import '@testing-library/jest-dom'
import ErrorBoundary from '../ErrorBoundary'

// Mock react-router-dom history
const mockHistory = {
push: jest.fn(),
replace: jest.fn(),
goBack: jest.fn(),
goForward: jest.fn(),
location: { pathname: '/', search: '', hash: '', state: null },
length: 1
}

// Mock CustomButton component
jest.mock('@components/muiComponents', () => ({
CustomButton: ({ onClick, children, ...props }: any) => (
<button onClick={onClick} {...props} data-testid="custom-button">
{children}
</button>
)
}))

// Component that throws an error during render
const ThrowError = () => {
throw new Error('Test error')
}

// Component that throws ChunkLoadError
const ThrowChunkLoadError = () => {
const error = new Error('ChunkLoadError')
error.name = 'ChunkLoadError'
throw error
}

// Component that throws error with custom name
const ThrowNamedError = () => {
const error = new Error('Custom error')
error.name = 'CustomError'
throw error
}

// Component that throws error without name property
const ThrowUnnamedError = () => {
const error = new Error('Error without name')
delete (error as any).name
throw error
}

describe('ErrorBoundary', () => {
const defaultProps = {
history: mockHistory,
children: <div data-testid="child">Child component</div>
}

beforeEach(() => {
jest.clearAllMocks()
// Suppress console.error for error boundary tests
// React logs errors to console even when caught by error boundaries
jest.spyOn(console, 'error').mockImplementation(() => {})
})

afterEach(() => {
;(console.error as jest.Mock).mockRestore()
})

// Helper to render error boundary with error-throwing child
const renderWithError = (ErrorComponent: React.ComponentType) => {
return render(
<ErrorBoundary history={mockHistory}>
<ErrorComponent />
</ErrorBoundary>
)
}

describe('Normal Rendering (No Error)', () => {
it('should render children when there is no error', () => {
render(
<ErrorBoundary {...defaultProps}>
<div data-testid="child">Child content</div>
</ErrorBoundary>
)

expect(screen.getByTestId('child')).toBeInTheDocument()
expect(screen.getByText('Child content')).toBeInTheDocument()
expect(screen.queryByTestId('pageNotFoundPage')).not.toBeInTheDocument()
})

it('should initialize with null error and errorInfo', () => {
const { container } = render(
<ErrorBoundary {...defaultProps}>
<div>Test</div>
</ErrorBoundary>
)

// Should render children, not error UI
expect(container.textContent).toBe('Test')
})
})

describe('Error Catching', () => {
it('should catch errors and display error UI', () => {
renderWithError(ThrowError)

// Error boundary should catch the error and show error UI
expect(screen.getByText('Oops! Something went wrong...')).toBeInTheDocument()
// Component uses data-id, not data-testid
const errorPage = document.querySelector('[data-id="pageNotFoundPage"]')
expect(errorPage).toBeInTheDocument()
})

it('should call componentDidCatch when error occurs', () => {
const componentDidCatchSpy = jest.spyOn(ErrorBoundary.prototype, 'componentDidCatch')

renderWithError(ThrowError)

expect(componentDidCatchSpy).toHaveBeenCalled()
expect(componentDidCatchSpy).toHaveBeenCalledWith(
expect.any(Error),
expect.any(Object)
)
componentDidCatchSpy.mockRestore()
})

it('should set error and errorInfo in state when error occurs', () => {
renderWithError(ThrowError)

// Error UI should be displayed, indicating state was set
const errorPage = document.querySelector('[data-id="pageNotFoundPage"]')
expect(errorPage).toBeInTheDocument()
expect(screen.getByText('Oops! Something went wrong...')).toBeInTheDocument()
})
})

describe('Error UI Display', () => {
it('should display error icon', () => {
renderWithError(ThrowError)

const errorIcon = screen.getByAltText('Error Icon')
expect(errorIcon).toBeInTheDocument()
// The src might be a module path, so just check that it exists
expect(errorIcon).toHaveAttribute('src')
})

it('should render error message with correct data-id', () => {
renderWithError(ThrowError)

// Component uses data-id, not data-testid
const errorMessage = document.querySelector('[data-id="moreInfo"]')
expect(errorMessage).toBeInTheDocument()
expect(errorMessage).toHaveTextContent('Oops! Something went wrong...')
})

it('should have correct CSS classes', () => {
renderWithError(ThrowError)

const errorPage = document.querySelector('[data-id="pageNotFoundPage"]')
expect(errorPage).toBeInTheDocument()
expect(errorPage).toHaveClass('new-error-page')
})
})

describe('Button Rendering Logic', () => {
it('should show "Return to Dashboard" button when error is not ChunkLoadError', () => {
renderWithError(ThrowError)

const returnButton = screen.getByText(/Return to Dashboard/i)
expect(returnButton).toBeInTheDocument()
})

it('should show button when error has name and name is not ChunkLoadError', () => {
renderWithError(ThrowNamedError)

expect(screen.getByText(/Return to Dashboard/i)).toBeInTheDocument()
})

it('should not show "Return to Dashboard" button for ChunkLoadError', () => {
renderWithError(ThrowChunkLoadError)

expect(screen.queryByText(/Return to Dashboard/i)).not.toBeInTheDocument()
})

it('should not show button when error.name is falsy', () => {
// Create an error component that throws an error with name set to empty string
const ThrowErrorWithEmptyName = () => {
const error = new Error('Test error')
error.name = ''
throw error
}
renderWithError(ThrowErrorWithEmptyName)

expect(screen.queryByText(/Return to Dashboard/i)).not.toBeInTheDocument()
})

it('should not show button when error is null', () => {
// Create an error component that throws an error with name set to null
const ThrowErrorWithNullName = () => {
const error = new Error('Test error')
;(error as any).name = null
throw error
}
renderWithError(ThrowErrorWithNullName)

expect(screen.queryByText(/Return to Dashboard/i)).not.toBeInTheDocument()
})

it('should handle error with name property but name is empty string', () => {
// Component that throws error with empty name
const ThrowEmptyNameError = () => {
const error = new Error('Error with empty name')
error.name = ''
throw error
}

renderWithError(ThrowEmptyNameError)

// Should not show button when name is empty string (falsy)
expect(screen.queryByText(/Return to Dashboard/i)).not.toBeInTheDocument()
})

it('should handle error with name property set to 0', () => {
// Component that throws error with name set to 0 (falsy)
const ThrowZeroNameError = () => {
const error = new Error('Error with zero name')
;(error as any).name = 0
throw error
}

renderWithError(ThrowZeroNameError)

// Should not show button when name is 0 (falsy)
expect(screen.queryByText(/Return to Dashboard/i)).not.toBeInTheDocument()
})

it('should handle error with name property set to false', () => {
// Component that throws error with name set to false (falsy)
const ThrowFalseNameError = () => {
const error = new Error('Error with false name')
;(error as any).name = false
throw error
}

renderWithError(ThrowFalseNameError)

// Should not show button when name is false (falsy)
expect(screen.queryByText(/Return to Dashboard/i)).not.toBeInTheDocument()
})
})

describe('Navigation and State Management', () => {
it('should call handleNavigation when "Return to Dashboard" button is clicked', () => {
renderWithError(ThrowError)

const returnButton = screen.getByText(/Return to Dashboard/i)
fireEvent.click(returnButton)

expect(mockHistory.push).toHaveBeenCalledWith('/search')
expect(mockHistory.push).toHaveBeenCalledTimes(1)
})

it('should reset error state when "Return to Dashboard" button is clicked', () => {
renderWithError(ThrowError)

// Verify error is displayed
expect(screen.getByText('Oops! Something went wrong...')).toBeInTheDocument()

// Click return button
const returnButton = screen.getByText(/Return to Dashboard/i)
fireEvent.click(returnButton)

// After clicking, navigation should be called
expect(mockHistory.push).toHaveBeenCalledWith('/search')
})

it('should call setState to reset error and errorInfo', () => {
const { rerender } = renderWithError(ThrowError)

expect(screen.getByText('Oops! Something went wrong...')).toBeInTheDocument()

const returnButton = screen.getByText(/Return to Dashboard/i)
fireEvent.click(returnButton)

// Verify navigation was called (setState happens before navigation)
expect(mockHistory.push).toHaveBeenCalled()
})
})

describe('Refresh Method', () => {
it('should call refresh method that calls window.location.reload', () => {
// Mock window.location.reload
const reloadSpy = jest.fn()
Object.defineProperty(window, 'location', {
writable: true,
value: { reload: reloadSpy },
configurable: true
})

// Create a wrapper component with ref to access instance
class TestWrapper extends React.Component {
private errorBoundaryRef = React.createRef<ErrorBoundary>()

componentDidMount() {
if (this.errorBoundaryRef.current) {
this.errorBoundaryRef.current.refresh()
}
}

render() {
return <ErrorBoundary ref={this.errorBoundaryRef} history={mockHistory}><div>Test</div></ErrorBoundary>
}
}

render(<TestWrapper />)

// Verify reload was called
expect(reloadSpy).toHaveBeenCalled()
})
})

describe('Edge Cases', () => {
it('should handle multiple errors sequentially', () => {
const { rerender } = renderWithError(ThrowError)

expect(screen.getByText('Oops! Something went wrong...')).toBeInTheDocument()

// Rerender with different error
rerender(
<ErrorBoundary history={mockHistory}>
<ThrowNamedError />
</ErrorBoundary>
)

expect(screen.getByText('Oops! Something went wrong...')).toBeInTheDocument()
})

it('should handle error with errorInfo but no error object', () => {
// This tests the branch where errorInfo exists but error might be null
renderWithError(ThrowError)

// Error UI should still display
const errorPage = document.querySelector('[data-id="pageNotFoundPage"]')
expect(errorPage).toBeInTheDocument()
})
})
})
Loading
Loading