From 6993c06998d827e65b95e59a4c58d82424638c45 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Sep 2026 03:48:12 +0200 Subject: [PATCH] fix: a crashing page now says so, and the in-progress and stats cards are gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /quizzes rendered a white screen — no navbar, no footer, nothing to read or report. I could not reproduce it from the data shape the API returns, and that is the point: an unhandled render error unmounts the whole tree and leaves nobody, reader or developer, anything to work from. There is now an error boundary around the routed page. The navbar and footer survive, the message and the component trail reach the console, and the reader gets a reload and a way out. It is keyed by path, so navigating away clears it. Also removed, both superseded by the analysis page: the dashboard's in-progress list and its three stat cards. Still open on /quizzes: I have not found the underlying throw. With the boundary in place the next visit will name it rather than showing a blank page, which is the thing I actually needed and did not have. 249 frontend tests green. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- frontend/src/App.jsx | 9 +++- frontend/src/components/ErrorBoundary.css | 13 ++++++ frontend/src/components/ErrorBoundary.jsx | 42 +++++++++++++++++++ .../src/components/ErrorBoundary.test.jsx | 25 +++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary.css create mode 100644 frontend/src/components/ErrorBoundary.jsx create mode 100644 frontend/src/components/ErrorBoundary.test.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3cae676..128ddd7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,9 +1,10 @@ import { lazy, Suspense } from 'react' -import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom' +import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation } from 'react-router-dom' import { AuthProvider, useAuth } from './context/AuthContext' import { ThemeProvider } from './context/ThemeContext' import Navbar from './components/Navbar' import SiteFooter from './components/SiteFooter' +import ErrorBoundary from './components/ErrorBoundary' const LoginPage = lazy(() => import('./pages/LoginPage')) const RegisterPage = lazy(() => import('./pages/RegisterPage')) @@ -54,11 +55,15 @@ function LoadingFallback() { // Layout wrapper for authenticated app pages (Navbar + container + footer) function AppLayout() { + // Keyed by path so navigating away from a broken page clears the error. + const location = useLocation() return ( <>
- + + +
diff --git a/frontend/src/components/ErrorBoundary.css b/frontend/src/components/ErrorBoundary.css new file mode 100644 index 0000000..7b5fdb1 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.css @@ -0,0 +1,13 @@ +/* A failure the reader can see, report, and get out of. */ +.eb-card { + max-width: 640px; margin: 48px auto; padding: 26px; + background: var(--card-bg); border: 1px solid var(--wrong-bd); border-radius: 12px; +} +.eb-card h1 { margin: 0 0 8px; font-size: 1.2rem; color: var(--wrong-fg); } +.eb-card p { margin: 0 0 14px; color: var(--text-muted); font-size: 0.9rem; } +.eb-card pre { + margin: 0 0 16px; padding: 12px; overflow-x: auto; + background: var(--bg); border: 1px solid var(--border); border-radius: 8px; + font-size: 0.8rem; color: var(--text-muted); white-space: pre-wrap; +} +.eb-actions { display: flex; gap: 8px; flex-wrap: wrap; } diff --git a/frontend/src/components/ErrorBoundary.jsx b/frontend/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..66fc6d7 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.jsx @@ -0,0 +1,42 @@ +import { Component } from 'react' +import './ErrorBoundary.css' + +/** + * Catch a render error and say so, rather than leaving a white page. + * + * Without this a single thrown error unmounts the whole tree — navbar included — + * and the reader gets a blank screen with nothing to act on and nothing to + * report. What broke is usually one page; the rest of the app still works, and + * the message is what makes the difference between "it's broken" and a bug + * anyone can chase. + */ +export default class ErrorBoundary extends Component { + constructor(props) { + super(props) + this.state = { error: null } + } + + static getDerivedStateFromError(error) { + return { error } + } + + componentDidCatch(error, info) { + // The console is where a developer will look; keep the component trail. + console.error('Render failed:', error, info?.componentStack) + } + + render() { + if (!this.state.error) return this.props.children + return ( +
+

This page failed to load

+

The rest of the app is still working — the error was on this screen.

+
{String(this.state.error?.message || this.state.error)}
+
+ + Dashboard +
+
+ ) + } +} diff --git a/frontend/src/components/ErrorBoundary.test.jsx b/frontend/src/components/ErrorBoundary.test.jsx new file mode 100644 index 0000000..8b517d5 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.test.jsx @@ -0,0 +1,25 @@ +import { describe, expect, it, vi, afterEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import ErrorBoundary from './ErrorBoundary' + +const Boom = () => { throw new Error('Cannot read properties of undefined') } + +afterEach(() => vi.restoreAllMocks()) + +describe('a page that throws', () => { + it('says so instead of leaving a white screen', () => { + vi.spyOn(console, 'error').mockImplementation(() => {}) + render() + + // A blank page tells the reader nothing and gives them nothing to report. + expect(screen.getByRole('alert')).toHaveTextContent('This page failed to load') + expect(screen.getByText(/Cannot read properties of undefined/)).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Reload' })).toBeInTheDocument() + }) + + it('leaves a working page alone', () => { + render(

All fine

) + expect(screen.getByText('All fine')).toBeInTheDocument() + expect(screen.queryByRole('alert')).not.toBeInTheDocument() + }) +})