码桶

发现社区成员的开源项目

main
cloudflare-dns-manager/tests/components/ErrorBoundary.test.jsx
ErrorBoundary.test.jsx5.6 KB
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ErrorBoundary from '../../src/components/ErrorBoundary.jsx';

const t = (key) => key;

// Suppress console.error from ErrorBoundary's componentDidCatch during tests
beforeEach(() => {
    vi.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
    vi.restoreAllMocks();
});

/**
 * Component that throws an error during render.
 */
function ThrowingChild({ shouldThrow = true }) {
    if (shouldThrow) {
        throw new Error('Test render error');
    }
    return <div>Child rendered successfully</div>;
}

/**
 * Component that throws on demand (via state change).
 */
function ConditionalThrow() {
    const [shouldThrow, setShouldThrow] = React.useState(false);
    if (shouldThrow) {
        throw new Error('Conditional error');
    }
    return (
        <div>
            <span>No error yet</span>
            <button onClick={() => setShouldThrow(true)}>Trigger error</button>
        </div>
    );
}

describe('ErrorBoundary component', () => {
    it('renders children normally when no error occurs', () => {
        render(
            <ErrorBoundary t={t}>
                <div>Hello World</div>
            </ErrorBoundary>
        );

        expect(screen.getByText('Hello World')).toBeInTheDocument();
    });

    it('renders multiple children normally', () => {
        render(
            <ErrorBoundary t={t}>
                <div>Child A</div>
                <div>Child B</div>
            </ErrorBoundary>
        );

        expect(screen.getByText('Child A')).toBeInTheDocument();
        expect(screen.getByText('Child B')).toBeInTheDocument();
    });

    it('catches errors and shows fallback error UI', () => {
        render(
            <ErrorBoundary t={t}>
                <ThrowingChild shouldThrow={true} />
            </ErrorBoundary>
        );

        // Children should not be rendered
        expect(screen.queryByText('Child rendered successfully')).not.toBeInTheDocument();

        // Default error boundary UI should show translated keys
        expect(screen.getByText('errorBoundaryTitle')).toBeInTheDocument();
        expect(screen.getByText('errorBoundaryMessage')).toBeInTheDocument();
        expect(screen.getByText('errorBoundaryRetry')).toBeInTheDocument();
    });

    it('retry button resets error state and re-renders children', () => {
        // Use an external flag that the test controls to decide whether to throw.
        const control = { shouldThrow: true };

        function MaybeThrow() {
            if (control.shouldThrow) {
                throw new Error('First render error');
            }
            return <div>Recovered successfully</div>;
        }

        render(
            <ErrorBoundary t={t}>
                <MaybeThrow />
            </ErrorBoundary>
        );

        // First render should show error UI
        expect(screen.getByText('errorBoundaryTitle')).toBeInTheDocument();

        // Set the flag so the child will succeed on the next render
        control.shouldThrow = false;

        // Click the retry button
        const retryButton = screen.getByText('errorBoundaryRetry');
        fireEvent.click(retryButton);

        // After retry, the component should re-render successfully
        expect(screen.getByText('Recovered successfully')).toBeInTheDocument();
        expect(screen.queryByText('errorBoundaryTitle')).not.toBeInTheDocument();
    });

    it('uses custom fallback when provided', () => {
        const customFallback = ({ error, resetError }) => (
            <div>
                <span>Custom error: {error.message}</span>
                <button onClick={resetError}>Custom retry</button>
            </div>
        );

        render(
            <ErrorBoundary t={t} fallback={customFallback}>
                <ThrowingChild shouldThrow={true} />
            </ErrorBoundary>
        );

        expect(screen.getByText('Custom error: Test render error')).toBeInTheDocument();
        expect(screen.getByText('Custom retry')).toBeInTheDocument();
    });

    it('custom fallback retry resets error state', () => {
        const control = { shouldThrow: true };

        function MaybeThrowCustom() {
            if (control.shouldThrow) {
                throw new Error('Custom throw');
            }
            return <div>Custom recovered</div>;
        }

        const customFallback = ({ resetError }) => (
            <div>
                <button onClick={resetError}>Custom reset</button>
            </div>
        );

        render(
            <ErrorBoundary t={t} fallback={customFallback}>
                <MaybeThrowCustom />
            </ErrorBoundary>
        );

        expect(screen.getByText('Custom reset')).toBeInTheDocument();

        // Set the flag so the child will succeed on re-render
        control.shouldThrow = false;

        fireEvent.click(screen.getByText('Custom reset'));

        expect(screen.getByText('Custom recovered')).toBeInTheDocument();
    });

    it('calls console.error when an error is caught', () => {
        render(
            <ErrorBoundary t={t}>
                <ThrowingChild shouldThrow={true} />
            </ErrorBoundary>
        );

        expect(console.error).toHaveBeenCalled();
    });

    it('works with t as a non-function (falls back to key identity)', () => {
        render(
            <ErrorBoundary t={null}>
                <ThrowingChild shouldThrow={true} />
            </ErrorBoundary>
        );

        // When t is null, the component uses (key) => key as fallback
        expect(screen.getByText('errorBoundaryTitle')).toBeInTheDocument();
    });
});