Files
web/src/__tests__/ErrorBoundary.test.tsx
T
Flea Flicker d7faa617df
CI / Test (pull_request) Successful in 18s
CI / Lint & Typecheck (pull_request) Successful in 26s
CI / Build & Push Docker Image (pull_request) Successful in 47s
fix(GRO-2094): instrument bootstrap with global error + ErrorBoundary
The bundle at /login was executing but the React tree never painted —
no console errors, no fallback UI, just an empty <div id='root'>.
Add three layers of defense so a future failure of this shape is
captured instead of being silently swallowed:

  1. window 'error' and 'unhandledrejection' listeners in main.tsx,
     printing structured context to console.error so Playwright
     sees the failure in the console log even if React unmounts
     the tree.

  2. A top-level <ErrorBoundary> in main.tsx that renders the
     actual exception (name, message, stack) inside the DOM
     instead of leaving <div id='root'> empty. The boundary also
     logs to console.error via componentDidCatch.

  3. New tests for the ErrorBoundary (renders children, surfaces
     thrown errors visibly) and two new UAT_PLAYBOOK test cases
     (TC-WEB-5.1.6 / 5.1.7) that explicitly assert the
     'never-blank-root' invariant on UAT.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-02 16:05:15 +00:00

55 lines
1.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, cleanup } from "@testing-library/react";
import { ErrorBoundary } from "../ErrorBoundary";
function ThrowingChild(): never {
throw new Error("synthetic render-time failure for GRO-2094");
}
function GoodChild() {
return <div data-testid="good-child">ok</div>;
}
describe("ErrorBoundary (GRO-2094)", () => {
let errorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
// React 18+ logs caught render errors to console.error via React's own
// instrumentation; suppress it so test output is clean but capture it
// for an assertion below.
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
errorSpy.mockRestore();
cleanup();
});
it("renders children when nothing throws", () => {
render(
<ErrorBoundary>
<GoodChild />
</ErrorBoundary>
);
expect(screen.getByTestId("good-child")).toBeInTheDocument();
expect(screen.queryByTestId("error-boundary")).not.toBeInTheDocument();
});
it("renders the error visibly when a child throws during render", () => {
render(
<ErrorBoundary>
<ThrowingChild />
</ErrorBoundary>
);
const fallback = screen.getByTestId("error-boundary");
expect(fallback).toBeInTheDocument();
const message = screen.getByTestId("error-boundary-message");
// The actual exception is shown — no more silent blank root.
expect(message.textContent).toContain("synthetic render-time failure for GRO-2094");
// The boundary also calls console.error so it shows up in the Playwright
// console log even if the DOM-rendered fallback is somehow missed.
expect(errorSpy).toHaveBeenCalled();
});
});