From e5c5e470b00acca04f4f801db68bc9d331b637d4 Mon Sep 17 00:00:00 2001 From: teernisse Date: Fri, 30 Jan 2026 13:35:30 -0500 Subject: [PATCH] Add ErrorBoundary component with recovery UI React class component that catches render errors in child trees and displays a styled error panel with the exception message and a 'Try again' button that resets state. Follows the existing design system with red accent colors and gradient icon background. Tests verify normal rendering, error capture with message display, and the presence of the recovery button. Co-Authored-By: Claude Opus 4.5 --- src/client/components/ErrorBoundary.test.tsx | 50 +++++++++++++++++ src/client/components/ErrorBoundary.tsx | 57 ++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/client/components/ErrorBoundary.test.tsx create mode 100644 src/client/components/ErrorBoundary.tsx diff --git a/src/client/components/ErrorBoundary.test.tsx b/src/client/components/ErrorBoundary.test.tsx new file mode 100644 index 0000000..e7f881e --- /dev/null +++ b/src/client/components/ErrorBoundary.test.tsx @@ -0,0 +1,50 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach } from "vitest"; +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import { ErrorBoundary } from "./ErrorBoundary"; + +function ThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) { + if (shouldThrow) { + throw new Error("Test render error"); + } + return
Child content
; +} + +describe("ErrorBoundary", () => { + beforeEach(() => { + // Suppress React error boundary console errors in test output + vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + it("renders children when no error occurs", () => { + render( + + + + ); + expect(screen.getByText("Child content")).toBeInTheDocument(); + }); + + it("renders error UI when child throws", () => { + render( + + + + ); + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + expect(screen.getByText("Test render error")).toBeInTheDocument(); + }); + + it("shows try again button in error state", () => { + render( + + + + ); + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + expect(screen.getByText("Try again")).toBeInTheDocument(); + expect(screen.getByText("An error occurred while rendering this view.")).toBeInTheDocument(); + }); +}); diff --git a/src/client/components/ErrorBoundary.tsx b/src/client/components/ErrorBoundary.tsx new file mode 100644 index 0000000..78f5768 --- /dev/null +++ b/src/client/components/ErrorBoundary.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +interface Props { + children: React.ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends React.Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + render() { + if (!this.state.hasError) { + return this.props.children; + } + + return ( +
+
+
+ + + +
+

Something went wrong

+

+ An error occurred while rendering this view. +

+ {this.state.error && ( +
+              {this.state.error.message}
+            
+ )} + +
+
+ ); + } +}