From 5b9cf91cef24bc5088674f74c7eaaf4dc11ce255 Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Thu, 20 Aug 2026 18:02:45 -0500 Subject: [PATCH] feat(frontend): mount error boundaries at the root, the item grid and the modals (#62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three mount points, so a render error costs the smallest part of the page it can. The catalogue boundary is the one that earns its keep. The likeliest throw in this app is a component rendering data from the API, and the item grid renders the most of it per page — contained there, the header, cart badge, filters and footer all survive, so a customer can still navigate instead of being handed one dead page. The modal boundary exists because the modal-route arrangement couples two independent trees. /account, /login and the rest render as modals over the storefront as a backdrop, so without a boundary between them a throw in Account blanks the storefront behind it and a throw in the storefront takes the open modal with it. One boundary separates them in both directions. Every escape action is a hard navigation rather than a Link. This is worth stating because the obvious implementation is wrong: a boundary does not reset when the route changes, so a Link would change the URL and go on rendering the fallback, which reads as the app being permanently broken. ErrorFallback changed too, outside this change's original scope and for a reason worth recording. antd's Result renders its title as a plain div with no heading semantics, so a page whose entire content is an error message offered a screen-reader user navigating by headings nothing at all to find. The title is now wrapped in Typography.Title. The tests assert a heading role and were right to; the component was what needed fixing, not the assertion. DevThrow throws on ?boom= and is mounted only behind import.meta.env.DEV, so Rollup drops it from a production build. Checked in both directions rather than trusted: the dev server serves it, and a production bundle greps to zero occurrences of its marker. A gate that is silently always-off looks identical to one that works. Verified: 87 end-to-end tests pass, 4 of them new — each boundary catches rather than blanking, the header survives a catalogue throw, the storefront survives a modal throw, and the report is observed reaching /api/client-errors on the wire rather than assumed. Build clean, lint 0 errors and 31 warnings. Refs #62 Co-Authored-By: Claude Opus 5 --- frontend/src/App.tsx | 41 +++++++--- frontend/src/components/DevThrow.tsx | 19 +++++ frontend/src/components/ErrorFallback.tsx | 13 ++- frontend/src/main.tsx | 98 ++++++++++++++++++----- frontend/tests/e2e/error-boundary.spec.ts | 56 +++++++++++++ 5 files changed, 196 insertions(+), 31 deletions(-) create mode 100644 frontend/src/components/DevThrow.tsx create mode 100644 frontend/tests/e2e/error-boundary.spec.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8d210d4..5b7c3b3 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,9 @@ import AuthPromptModal from './customer/AuthPromptModal'; import { useThemeMode } from './theme/ThemeContext'; import { useCustomerAuth } from './customer/CustomerAuthContext'; import { useCart } from './cart/CartContext'; +import ErrorBoundary from './components/ErrorBoundary'; +import ErrorFallback from './components/ErrorFallback'; +import DevThrow from './components/DevThrow'; const { Header, Content, Footer } = Layout; const { Title } = Typography; @@ -251,17 +254,33 @@ export default function App() { {loading && !items.length && !failed ? : null} - + ( + window.location.reload()}> + Reload + + } + /> + )} + > + {import.meta.env.DEV && } + +