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(); }); });