From 2d4938519fe8793c3464b8b715fa5397a1f30d9e Mon Sep 17 00:00:00 2001 From: ghzhost Date: Wed, 2 Sep 2026 08:43:42 +0000 Subject: [PATCH] fix(common): add componentDidCatch logging and onError callback to ErrorBoundary (#11) --- src/components/common/ErrorBoundary.test.tsx | 71 ++++++++++++++++++++ src/components/common/ErrorBoundary.tsx | 51 +++++++++++--- 2 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 src/components/common/ErrorBoundary.test.tsx diff --git a/src/components/common/ErrorBoundary.test.tsx b/src/components/common/ErrorBoundary.test.tsx new file mode 100644 index 0000000..6d409de --- /dev/null +++ b/src/components/common/ErrorBoundary.test.tsx @@ -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
Healthy content
+} + +describe('ErrorBoundary', () => { + let consoleErrorSpy: any + + beforeEach(() => { + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + afterEach(() => { + consoleErrorSpy.mockRestore() + }) + + it('renders children when there is no error', () => { + render( + + + + ) + expect(screen.getByText('Healthy content')).toBeInTheDocument() + }) + + it('catches render error, logs via console.error, and renders default fallback UI', () => { + render( + + + + ) + + 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( + + + + ) + + 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( + Custom Error Screen}> + + + ) + expect(screen.getByText('Custom Error Screen')).toBeInTheDocument() + }) +}) diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index d32f2b7..0d5dfb0 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -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{ - state:St={hasError:false} - static getDerivedStateFromError(e:Error):St{return{hasError:true,error:e}} - render(){ - if(this.state.hasError)return this.props.fallback??(

Something went wrong

) +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 { + 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 ?? ( +
+

Something went wrong

+ +
+ ) + ) + } return this.props.children } }