Files
ThothII/frontend/src/components/ErrorBoundary.test.tsx
T
Codex d8a29bfbdd Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
2026-09-13 14:26:39 +02:00

112 lines
3.8 KiB
TypeScript

import { act, render, screen } from "@testing-library/react";
import { setLocale } from "../i18n";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { ErrorBoundary } from "./ErrorBoundary";
/** A child that throws on demand. Toggle `boom` to control whether it crashes. */
function Boom({ boom, label = "kaboom" }: { boom: boolean; label?: string }) {
if (boom) throw new Error(label);
return <div>child ok</div>;
}
// React logs caught render errors to console.error; silence it so the suite output
// stays readable while still exercising the real error path.
let errorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
act(() => setLocale("en"));
errorSpy.mockRestore();
});
test("updates an already displayed error boundary when the locale changes", () => {
render(<ErrorBoundary label="document"><Boom boom label="SELECT * malformed" /></ErrorBoundary>);
act(() => setLocale("it"));
expect(screen.getByRole("alert")).toHaveTextContent("Impossibile visualizzare questo elemento (documento).");
expect(screen.getByRole("button", { name: "Riprova" })).toBeVisible();
expect(screen.getByText("SELECT * malformed")).toBeInTheDocument();
});
test("renders children when nothing throws", () => {
render(
<ErrorBoundary>
<Boom boom={false} />
</ErrorBoundary>,
);
expect(screen.getByText("child ok")).toBeInTheDocument();
});
test("renders the default fallback when a child throws", () => {
render(
<ErrorBoundary label="step">
<Boom boom />
</ErrorBoundary>,
);
expect(screen.queryByText("child ok")).not.toBeInTheDocument();
// Default fallback names the labelled surface.
expect(screen.getByText(/couldn't be displayed/i)).toBeInTheDocument();
expect(screen.getByText(/step/i)).toBeInTheDocument();
});
test("uses a custom fallback render prop with the error", () => {
render(
<ErrorBoundary fallback={(err) => <p>custom: {err.message}</p>}>
<Boom boom label="explode" />
</ErrorBoundary>,
);
expect(screen.getByText("custom: explode")).toBeInTheDocument();
});
test("calls onError when a child throws", () => {
const onError = vi.fn();
render(
<ErrorBoundary onError={onError}>
<Boom boom label="reported" />
</ErrorBoundary>,
);
expect(onError).toHaveBeenCalledTimes(1);
expect(onError.mock.calls[0][0]).toBeInstanceOf(Error);
expect((onError.mock.calls[0][0] as Error).message).toBe("reported");
});
test("recovers when resetKeys change to non-crashing content", async () => {
function Harness() {
const [boom, setBoom] = useState(true);
return (
<>
<button onClick={() => setBoom(false)}>fix</button>
<ErrorBoundary resetKeys={[boom]}>
<Boom boom={boom} />
</ErrorBoundary>
</>
);
}
render(<Harness />);
// Starts crashed → fallback visible.
expect(screen.getByText(/couldn't be displayed/i)).toBeInTheDocument();
// Flip the data (and thus resetKeys); the boundary clears and re-renders children.
await userEvent.click(screen.getByRole("button", { name: "fix" }));
expect(screen.getByText("child ok")).toBeInTheDocument();
expect(screen.queryByText(/couldn't be displayed/i)).not.toBeInTheDocument();
});
test("Try again resets the boundary", async () => {
let boom = true;
function Flaky() {
if (boom) throw new Error("transient");
return <div>recovered</div>;
}
render(
<ErrorBoundary>
<Flaky />
</ErrorBoundary>,
);
expect(screen.getByText(/couldn't be displayed/i)).toBeInTheDocument();
// Simulate the underlying condition clearing, then retry.
boom = false;
await userEvent.click(screen.getByRole("button", { name: /try again/i }));
expect(screen.getByText("recovered")).toBeInTheDocument();
});