import { render, screen } from '@testing-library/react';
import * as React from 'react';
import { PluginMeta, PluginType, PluginContext } from '@grafana/data';
import { getMockPlugin } from '@grafana/data/test';
import { PluginErrorBoundary } from './PluginErrorBoundary';
const ThrowingComponent = ({ shouldThrow }: { shouldThrow: boolean }) => {
if (shouldThrow) {
throw new Error('Test error message');
}
return
Working component
;
};
const TestFallback = ({ error, errorInfo }: { error: Error | null; errorInfo: React.ErrorInfo | null }) => (
Fallback rendered
Error: {error?.message}
{errorInfo &&
Error info available
}
);
const renderWithPluginContext = ({
children,
pluginMeta,
fallback,
onError,
}: {
children: React.ReactNode;
pluginMeta?: PluginMeta;
fallback?: React.ComponentType<{ error: Error | null; errorInfo: React.ErrorInfo | null }>;
onError?: (error: Error, info: React.ErrorInfo) => void;
}) => {
const mockPluginMeta = pluginMeta || getMockPlugin({ id: 'test-plugin', type: PluginType.panel });
return render(
{children}
);
};
describe('PluginErrorBoundary', () => {
let consoleErrorSpy: jest.SpyInstance;
beforeEach(() => {
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
it('should render children normally when no error occurs', () => {
renderWithPluginContext({ children: });
expect(screen.getByText('Working component')).toBeInTheDocument();
});
it('should render null when an error occurs and no fallback is provided', () => {
const { container } = renderWithPluginContext({ children: });
expect(container.firstChild).toBeNull();
});
it('should render custom fallback component when an error occurs', () => {
renderWithPluginContext({ children: , fallback: TestFallback });
expect(screen.getByText('Fallback rendered')).toBeInTheDocument();
expect(screen.getByText('Error: Test error message')).toBeInTheDocument();
expect(screen.getByText('Error info available')).toBeInTheDocument();
});
it('should call onError callback when an error occurs', () => {
const onErrorMock = jest.fn();
renderWithPluginContext({ children: , onError: onErrorMock });
expect(onErrorMock).toHaveBeenCalledTimes(1);
expect(onErrorMock).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Test error message' }),
expect.objectContaining({
componentStack: expect.any(String),
})
);
});
it('should log error to console with plugin ID when no onError callback is provided', () => {
const mockPluginMeta = getMockPlugin({ id: 'my-test-plugin', type: PluginType.datasource });
renderWithPluginContext({ children: , pluginMeta: mockPluginMeta });
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Plugin "my-test-plugin" failed to load:',
expect.objectContaining({ message: 'Test error message' }),
expect.objectContaining({
componentStack: expect.any(String),
})
);
});
it('should handle error when plugin context is not available', () => {
render(
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Plugin "undefined" failed to load:',
expect.objectContaining({ message: 'Test error message' }),
expect.objectContaining({
componentStack: expect.any(String),
})
);
});
it('should update state correctly when error occurs', () => {
renderWithPluginContext({ children: , fallback: TestFallback });
// Verify that both error and errorInfo are available in the fallback
expect(screen.getByText('Error: Test error message')).toBeInTheDocument();
expect(screen.getByText('Error info available')).toBeInTheDocument();
});
it('should reset error state when children change to non-throwing component', () => {
const { rerender } = renderWithPluginContext({
children: ,
fallback: TestFallback,
});
// Initially should show fallback
expect(screen.getByText('Fallback rendered')).toBeInTheDocument();
// Re-render with non-throwing component
rerender(
);
// Should still show fallback since error boundary doesn't reset automatically
expect(screen.getByText('Fallback rendered')).toBeInTheDocument();
});
it('should handle multiple children correctly', () => {
renderWithPluginContext({
children: (
<>
First child
Third child
>
),
});
expect(screen.getByText('First child')).toBeInTheDocument();
expect(screen.getByText('Working component')).toBeInTheDocument();
expect(screen.getByText('Third child')).toBeInTheDocument();
});
it('should handle error in one of multiple children', () => {
renderWithPluginContext({
children: (
<>
First child
Third child
>
),
fallback: TestFallback,
});
// Should show fallback and not render any of the children
expect(screen.getByText('Fallback rendered')).toBeInTheDocument();
expect(screen.queryByText('First child')).not.toBeInTheDocument();
expect(screen.queryByText('Third child')).not.toBeInTheDocument();
});
});