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=<scope> 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 <noreply@anthropic.com>
44 lines
1.4 KiB
TypeScript
44 lines
1.4 KiB
TypeScript
import React from 'react';
|
|
import Result from 'antd/es/result';
|
|
import Typography from 'antd/es/typography';
|
|
|
|
const { Title, Paragraph, Text } = Typography;
|
|
|
|
type ErrorFallbackProps = Readonly<{
|
|
error: Error;
|
|
title: string;
|
|
actions: React.ReactNode;
|
|
fullPage?: boolean;
|
|
}>;
|
|
|
|
// The single place that decides whether a customer is shown a stack trace.
|
|
// Gated on DEV so a developer sees the throw immediately while a production
|
|
// bundle cannot render it at all — one decision in one file rather than the
|
|
// same judgement repeated at three mount points, where they would drift.
|
|
export default function ErrorFallback({ error, title, actions, fullPage = false }: ErrorFallbackProps) {
|
|
return (
|
|
<Result
|
|
status="error"
|
|
// antd's Result renders `title` as a plain div, with no heading
|
|
// semantics — a screen-reader user navigating by headings would find
|
|
// nothing on a page whose entire content is this error. Wrapped in
|
|
// Title so the fallback has a real heading; do not simplify this back
|
|
// to a bare string.
|
|
title={
|
|
<Title level={3} style={{ marginBottom: 0 }}>
|
|
{title}
|
|
</Title>
|
|
}
|
|
subTitle="This has been reported. Nothing you did caused it."
|
|
style={{ paddingBlock: fullPage ? 64 : 24 }}
|
|
extra={actions}
|
|
>
|
|
{import.meta.env.DEV ? (
|
|
<Paragraph>
|
|
<Text code>{error.message}</Text>
|
|
</Paragraph>
|
|
) : null}
|
|
</Result>
|
|
);
|
|
}
|