From 29415da421a38f581de26f65a02e8cb7df8a6874 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sat, 21 Mar 2026 17:26:40 +0100 Subject: [PATCH] Add ErrorBoundary component to catch render-time errors - Create ErrorBoundary component to handle React render errors - Wrap App component with ErrorBoundary for global error handling - Add comprehensive tests for ErrorBoundary functionality - Show fallback UI with error message when errors occur --- frontend/src/App.tsx | 9 ++- frontend/src/components/ErrorBoundary.tsx | 62 +++++++++++++++++++ .../__tests__/ErrorBoundary.test.tsx | 33 ++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary.tsx create mode 100644 frontend/src/components/__tests__/ErrorBoundary.test.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ae418b7..4ff80d5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -26,6 +26,7 @@ import { AuthProvider } from "./providers/AuthProvider"; import { TimezoneProvider } from "./providers/TimezoneProvider"; import { RequireAuth } from "./components/RequireAuth"; import { SetupGuard } from "./components/SetupGuard"; +import { ErrorBoundary } from "./components/ErrorBoundary"; import { MainLayout } from "./layouts/MainLayout"; import { SetupPage } from "./pages/SetupPage"; import { LoginPage } from "./pages/LoginPage"; @@ -43,9 +44,10 @@ import { BlocklistsPage } from "./pages/BlocklistsPage"; function App(): React.JSX.Element { return ( - - - + + + + {/* Setup wizard — always accessible; redirects to /login if already done */} } /> @@ -85,6 +87,7 @@ function App(): React.JSX.Element { + ); } diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..98adc42 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,62 @@ +/** + * React error boundary component. + * + * Catches render-time exceptions in child components and shows a fallback UI. + */ +import React from "react"; + +interface ErrorBoundaryState { + hasError: boolean; + errorMessage: string | null; +} + +interface ErrorBoundaryProps { + children: React.ReactNode; +} + +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, errorMessage: null }; + this.handleReload = this.handleReload.bind(this); + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, errorMessage: error.message || "Unknown error" }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + console.error("ErrorBoundary caught an error", { error, errorInfo }); + } + + handleReload(): void { + window.location.reload(); + } + + render(): React.ReactNode { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

{this.state.errorMessage ?? "Please try reloading the page."}

+ +
+ ); + } + + return this.props.children; + } +} diff --git a/frontend/src/components/__tests__/ErrorBoundary.test.tsx b/frontend/src/components/__tests__/ErrorBoundary.test.tsx new file mode 100644 index 0000000..d90e4d3 --- /dev/null +++ b/frontend/src/components/__tests__/ErrorBoundary.test.tsx @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { ErrorBoundary } from "../ErrorBoundary"; + +function ExplodingChild(): React.ReactElement { + throw new Error("boom"); +} + +describe("ErrorBoundary", () => { + it("renders the fallback UI when a child throws", () => { + render( + + + , + ); + + expect(screen.getByRole("alert")).toBeInTheDocument(); + expect(screen.getByText("Something went wrong")).toBeInTheDocument(); + expect(screen.getByText(/boom/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /reload/i })).toBeInTheDocument(); + }); + + it("renders children normally when no error occurs", () => { + render( + +
safe
+
, + ); + + expect(screen.getByTestId("safe-child")).toBeInTheDocument(); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); +});