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
71 changes: 71 additions & 0 deletions src/components/common/ErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import React from 'react'
import { ErrorBoundary } from './ErrorBoundary'

function ProblemChild({ shouldThrow }: { shouldThrow?: boolean }) {
if (shouldThrow) {
throw new Error('Test render crash')
}
return <div>Healthy content</div>
}

describe('ErrorBoundary', () => {
let consoleErrorSpy: any

beforeEach(() => {
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
})

afterEach(() => {
consoleErrorSpy.mockRestore()
})

it('renders children when there is no error', () => {
render(
<ErrorBoundary>
<ProblemChild shouldThrow={false} />
</ErrorBoundary>
)
expect(screen.getByText('Healthy content')).toBeInTheDocument()
})

it('catches render error, logs via console.error, and renders default fallback UI', () => {
render(
<ErrorBoundary>
<ProblemChild shouldThrow={true} />
</ErrorBoundary>
)

expect(screen.getByText('Something went wrong')).toBeInTheDocument()
expect(consoleErrorSpy).toHaveBeenCalledWith(
'[ErrorBoundary] Caught render error:',
expect.any(Error),
expect.any(String)
)
})

it('calls optional onError callback when an error is caught', () => {
const onError = vi.fn()
render(
<ErrorBoundary onError={onError}>
<ProblemChild shouldThrow={true} />
</ErrorBoundary>
)

expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Test render crash' }),
expect.objectContaining({ componentStack: expect.any(String) })
)
})

it('renders custom fallback when provided', () => {
render(
<ErrorBoundary fallback={<div>Custom Error Screen</div>}>
<ProblemChild shouldThrow={true} />
</ErrorBoundary>
)
expect(screen.getByText('Custom Error Screen')).toBeInTheDocument()
})
})
51 changes: 43 additions & 8 deletions src/components/common/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,46 @@
import React,{Component,type ReactNode}from 'react'
interface P{children:ReactNode;fallback?:ReactNode}
interface St{hasError:boolean;error?:Error}
export class ErrorBoundary extends Component<P,St>{
state:St={hasError:false}
static getDerivedStateFromError(e:Error):St{return{hasError:true,error:e}}
render(){
if(this.state.hasError)return this.props.fallback??(<div className='p-6 text-center'><p className='text-red-500'>Something went wrong</p><button onClick={()=>this.setState({hasError:false})} className='btn-secondary mt-4'>Try again</button></div>)
import React, { Component, type ReactNode, type ErrorInfo } from 'react'

interface ErrorBoundaryProps {
children: ReactNode
fallback?: ReactNode
onError?: (error: Error, info: ErrorInfo) => void
}

interface ErrorBoundaryState {
hasError: boolean
error?: Error
}

export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false }

static getDerivedStateFromError(e: Error): ErrorBoundaryState {
return { hasError: true, error: e }
}

componentDidCatch(error: Error, info: ErrorInfo) {
console.error('[ErrorBoundary] Caught render error:', error, info.componentStack)
if (this.props.onError) {
this.props.onError(error, info)
}
}

render() {
if (this.state.hasError) {
return (
this.props.fallback ?? (
<div className="p-6 text-center">
<p className="text-red-500">Something went wrong</p>
<button
onClick={() => this.setState({ hasError: false, error: undefined })}
className="btn-secondary mt-4"
>
Try again
</button>
</div>
)
)
}
return this.props.children
}
}