Files
karma/ui/src/ErrorBoundary.test.tsx
2026-03-09 15:41:44 +00:00

126 lines
4.1 KiB
TypeScript

import { act } from "react";
import { render, screen } from "@testing-library/react";
import { ErrorBoundary } from "./ErrorBoundary";
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
jest.useFakeTimers();
consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
jest.restoreAllMocks();
});
const FailingComponent = () => {
throw new Error("Error thrown from problem child");
};
const renderFailingComponent = () => {
return render(
<ErrorBoundary>
<FailingComponent></FailingComponent>
</ErrorBoundary>,
);
};
describe("<ErrorBoundary />", () => {
it("matches snapshot", () => {
const { asFragment } = renderFailingComponent();
expect(asFragment()).toMatchSnapshot();
expect(consoleSpy).toHaveBeenCalled();
});
it("componentDidCatch should catch an error from FailingComponent", () => {
jest.spyOn(ErrorBoundary.prototype, "componentDidCatch");
renderFailingComponent();
expect(ErrorBoundary.prototype.componentDidCatch).toHaveBeenCalled();
});
it("sets up timer to reload after 60s", () => {
// Verifies that a timer is set up for auto-reload after error
renderFailingComponent();
expect(jest.getTimerCount()).toBe(1);
expect(consoleSpy).toHaveBeenCalled();
});
it("renders error message when component fails", () => {
renderFailingComponent();
expect(
screen.getByText("Error: Error thrown from problem child"),
).toBeInTheDocument();
expect(consoleSpy).toHaveBeenCalled();
});
it("decrements reload countdown each second", () => {
// Verifies the reloadApp method decrements the countdown timer
renderFailingComponent();
expect(screen.getByText(/auto refresh in 60s/)).toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(1000);
});
expect(screen.getByText(/auto refresh in 59s/)).toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(1000);
});
});
it("reloadApp decrements countdown when more than one second is left", () => {
// Verifies that reloadApp uses functional setState to decrement reloadSeconds
const boundary = new ErrorBoundary({ children: <span /> });
const setStateSpy = jest.spyOn(boundary, "setState");
boundary.state = { cachedError: null, reloadSeconds: 2 };
boundary.reloadApp();
expect(setStateSpy).toHaveBeenCalledTimes(1);
const updater = setStateSpy.mock.calls[0][0] as unknown as (prev: {
reloadSeconds: number;
}) => { reloadSeconds: number };
expect(updater({ reloadSeconds: 2 })).toEqual({ reloadSeconds: 1 });
});
it("reloadApp does not decrement when countdown reaches 1 or less", () => {
// Verifies that reloadApp calls window.location.reload when reloadSeconds <= 1 (line 65)
const boundary = new ErrorBoundary({ children: <span /> });
const setStateSpy = jest.spyOn(boundary, "setState");
boundary.state = { cachedError: null, reloadSeconds: 1 };
boundary.reloadApp();
expect(setStateSpy).not.toHaveBeenCalled();
});
it("componentDidCatch does not set error if already cached", () => {
// Verifies that componentDidCatch skips setState when error is already cached (line 75)
const boundary = new ErrorBoundary({ children: <span /> });
const setStateSpy = jest.spyOn(boundary, "setState");
const error = new Error("Test error");
boundary.state = { cachedError: error, reloadSeconds: 60 };
boundary.componentDidCatch(error, { componentStack: "" });
expect(setStateSpy).not.toHaveBeenCalled();
});
it("componentDidCatch does not set timer if already set", () => {
// Verifies that componentDidCatch skips setInterval when timer is already set (line 80)
const boundary = new ErrorBoundary({ children: <span /> });
const setIntervalSpy = jest.spyOn(global, "setInterval");
const error = new Error("Test error");
boundary.timer = 123 as unknown as ReturnType<typeof setInterval>;
boundary.componentDidCatch(error, { componentStack: "" });
expect(setIntervalSpy).not.toHaveBeenCalled();
setIntervalSpy.mockRestore();
});
});