fix(insights): settle the stat strip when its chart fails to load

The strip holds min, max, median and mean until the chart reports its
visible window. A chart chunk that failed was caught by the boundary
and never reported, so the cells stayed skeletons although the summary
was already there. The boundary now settles the strip with no window.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 22:57:47 +02:00
1 parent 78b0cc06e2
commit efbaf02335
3 files changed
+71 -4

No files matched your search

@@ -0,0 +1,48 @@
/**
* A chart whose chunk fails never reports its visible window, and the stat
* strip above it holds its cells for that report. The boundary's catch
* settles it with "no window" so the strip paints the full-range summary
* instead of staying a skeleton for good.
*/
import type { ComponentProps, ReactElement } from "react";
import { describe, expect, it, vi } from "vitest";
import { ChartBoundary } from "@/components/charts/chart-error-state";
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
type Props = ComponentProps<typeof HealthChartDynamic>;
describe("chart boundary settles the stat strip", () => {
it("reports an empty window when the chart fails to mount", () => {
const onVisibleStats = vi.fn();
const element = HealthChartDynamic({
types: ["WEIGHT"],
onVisibleStats,
} as unknown as Props) as ReactElement<{
onError?: (error: Error) => void;
}>;
expect(element.props.onError).toBeTypeOf("function");
element.props.onError?.(new Error("chunk"));
expect(onVisibleStats).toHaveBeenCalledWith(null);
});
it("passes no handler for a chart nobody reads stats from", () => {
const element = HealthChartDynamic({
types: ["WEIGHT"],
} as unknown as Props) as ReactElement<{ onError?: unknown }>;
expect(element.props.onError).toBeUndefined();
});
it("the boundary calls its handler when it catches", () => {
const onError = vi.fn();
const boundary = new ChartBoundary({
fallback: null,
children: null,
onError,
});
const error = new Error("chunk");
boundary.componentDidCatch(error);
expect(onError).toHaveBeenCalledWith(error);
});
});
+15 -3
View File
@@ -95,13 +95,16 @@ export function ChartErrorState({
interface BoundaryProps {
fallback: ReactNode;
children: ReactNode;
/** Called once the boundary has caught, after the fallback commits. */
onError?: (error: Error) => void;
}
interface BoundaryState {
failed: boolean;
}
class Boundary extends Component<BoundaryProps, BoundaryState> {
/** Exported for its unit test; mount `<ChartErrorBoundary>` instead. */
export class ChartBoundary extends Component<BoundaryProps, BoundaryState> {
state: BoundaryState = { failed: false };
static getDerivedStateFromError(): BoundaryState {
@@ -118,6 +121,7 @@ class Boundary extends Component<BoundaryProps, BoundaryState> {
};
g.__healthlog_onError?.(error);
}
this.props.onError?.(error);
}
render(): ReactNode {
@@ -134,12 +138,20 @@ class Boundary extends Component<BoundaryProps, BoundaryState> {
*/
export function ChartErrorBoundary({
children,
onError,
}: {
children: ReactNode;
/**
* Lets the page settle anything that waited on the chart: a stat strip
* that holds its cells for the chart's window would otherwise wait for a
* report the failed chart can never send.
*/
onError?: (error: Error) => void;
}): ReactElement {
const { t } = useTranslations();
return (
<Boundary
<ChartBoundary
onError={onError}
fallback={
// Wrapped in the chart-card shell so a chunk failure renders
// as one solid card among the other cards, not a bare dashed
@@ -159,6 +171,6 @@ export function ChartErrorBoundary({
}
>
{children}
</Boundary>
</ChartBoundary>
);
}
@@ -66,8 +66,15 @@ export function HealthChartDynamic(
: props.showDataTable
? HealthChartLazyTable
: HealthChartLazy;
// A chart that never mounts never reports its window, and a stat strip
// holding its cells for that report would stay a skeleton for good. The
// failure settles it with "no window", so the strip falls back to the
// full-range summary it already has.
const { onVisibleStats } = props;
return (
<ChartErrorBoundary>
<ChartErrorBoundary
onError={onVisibleStats ? () => onVisibleStats(null) : undefined}
>
<Lazy {...props} />
</ChartErrorBoundary>
);