fix(frontend): contain viewer/widget crashes with error boundaries
A render error in one viewer or gate widget unmounted the whole React root (white screen). Add a reusable ErrorBoundary (class component, no new dep) with resetKeys + an on-brand fallback, and wire it at two surfaces: - WidgetHost: isolates the gate widget (reset on descriptor id) so a malformed gate payload no longer blanks the conversation - SessionDocumentsPanel: wraps each document (reset on doc key/content) so one crashing viewer degrades only its section; siblings and the panel survive The observed crash: a schema-linking doc that parses but lacks `candidates` makes SchemaLinkingViewer throw. Verified live via Playwright against the mock backend. TDD throughout; tsc clean, 123/123 vitest (+8 new tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { WidgetHost } from "./WidgetHost";
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
import type { WidgetDescriptor } from "../api/types";
|
||||
|
||||
// Replace the widget registry with a renderer that always throws, simulating a
|
||||
// viewer/widget that crashes on malformed gate data (the real white-screen path).
|
||||
vi.mock("../widgets", () => ({
|
||||
resolve: () => function Exploding() {
|
||||
throw new Error("bad gate payload");
|
||||
},
|
||||
}));
|
||||
|
||||
let errorSpy: ReturnType<typeof vi.spyOn>;
|
||||
beforeEach(() => {
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
useSessionStore.getState().resetSession();
|
||||
});
|
||||
afterEach(() => {
|
||||
errorSpy.mockRestore();
|
||||
useSessionStore.getState().resetSession();
|
||||
});
|
||||
|
||||
const gate: WidgetDescriptor = { id: "g1", widget: "select", title: "Choose" };
|
||||
|
||||
test("a crashing widget shows a fallback instead of unmounting the app", () => {
|
||||
useSessionStore.setState({ pendingWidget: gate });
|
||||
render(<WidgetHost sessionId="s1" />);
|
||||
// No throw escaped: the fallback rendered.
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(screen.getByText(/couldn't be displayed/i)).toBeInTheDocument();
|
||||
// The store (and thus the surrounding app) is still alive and interactive.
|
||||
expect(useSessionStore.getState().pendingWidget).toEqual(gate);
|
||||
});
|
||||
Reference in New Issue
Block a user