Co-Study4Grid / frontend /src /components /ErrorBoundary.test.tsx
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
5.52 kB
// Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { useState } from 'react';
import ErrorBoundary from './ErrorBoundary';
/** Helper component that throws on render when `shouldThrow` is true. */
function Bomb({ shouldThrow, message = 'boom' }: { shouldThrow: boolean; message?: string }) {
if (shouldThrow) {
throw new Error(message);
}
return <div>child content</div>;
}
/** Wrapper that allows a test to flip the bomb from thrown -> healthy. */
function RecoverableBomb() {
const [broken, setBroken] = useState(true);
return (
<ErrorBoundary>
<button type="button" onClick={() => setBroken(false)}>
fix
</button>
<Bomb shouldThrow={broken} />
</ErrorBoundary>
);
}
describe('ErrorBoundary', () => {
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
// React logs caught errors to console.error; silence them to keep
// test output clean.
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
it('renders children when no error is thrown', () => {
render(
<ErrorBoundary>
<Bomb shouldThrow={false} />
</ErrorBoundary>,
);
expect(screen.getByText('child content')).toBeInTheDocument();
});
it('renders the default fallback UI when a child throws', () => {
render(
<ErrorBoundary>
<Bomb shouldThrow message="kaboom" />
</ErrorBoundary>,
);
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
// Error message appears in both the details block and the stack trace.
expect(screen.getAllByText(/kaboom/).length).toBeGreaterThan(0);
expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Reload page' })).toBeInTheDocument();
});
it('renders a custom fallback when provided', () => {
render(
<ErrorBoundary fallback={<div>custom fallback</div>}>
<Bomb shouldThrow />
</ErrorBoundary>,
);
expect(screen.getByText('custom fallback')).toBeInTheDocument();
expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
});
it('logs the caught error to console.error', () => {
render(
<ErrorBoundary>
<Bomb shouldThrow message="explode" />
</ErrorBoundary>,
);
const loggedFromBoundary = consoleErrorSpy.mock.calls.some(
(call) => typeof call[0] === 'string' && call[0].includes('ErrorBoundary caught an error'),
);
expect(loggedFromBoundary).toBe(true);
});
it('recovers when the user clicks "Try again" and the child no longer throws', () => {
render(<RecoverableBomb />);
// Initially in error state.
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
// The "fix" button is not in the DOM because the fallback replaced the tree.
expect(screen.queryByRole('button', { name: 'fix' })).not.toBeInTheDocument();
// We can't fix state from inside the fallback, so this test just verifies
// that "Try again" resets the boundary. The child will re-throw and the
// fallback will remain.
fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
// After reset + rethrow, still shows fallback.
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
it('calls window.location.reload when "Reload page" is clicked', () => {
const reloadMock = vi.fn();
const originalLocation = window.location;
Object.defineProperty(window, 'location', {
configurable: true,
value: { ...originalLocation, reload: reloadMock },
});
try {
render(
<ErrorBoundary>
<Bomb shouldThrow />
</ErrorBoundary>,
);
fireEvent.click(screen.getByRole('button', { name: 'Reload page' }));
expect(reloadMock).toHaveBeenCalledTimes(1);
} finally {
Object.defineProperty(window, 'location', {
configurable: true,
value: originalLocation,
});
}
});
it('shows error details in a collapsible <details> element', () => {
render(
<ErrorBoundary>
<Bomb shouldThrow message="detail-test" />
</ErrorBoundary>,
);
expect(screen.getByText('Error details')).toBeInTheDocument();
expect(screen.getAllByText(/detail-test/).length).toBeGreaterThan(0);
});
});