javascriptreactjsjestjsreact-testing-libraryreact-error-boundary

How do I test the fallback component for the ErrorBoundary?


I have this component:

import React, { lazy, Suspense } from 'react';
import { ErrorBoundary } from '../ErrorBoundary';

const FALLBACK = <svg aria-label="" data-testid="icon-fallback" viewBox="0 0 21 21" />;

const ERROR = (
    <svg data-testid="icon-notdef" viewBox="0 0 21 21">
        <path d="M0.5,0.5v20h20v-20H0.5z M9.1,10.5l-6.6,6.6V3.9L9.1,10.5z M3.9,2.5h13.2l-6.6,6.6L3.9,2.5z M10.5,11.9l6.6,6.6H3.9 L10.5,11.9z M11.9,10.5l6.6-6.6v13.2L11.9,10.5z" />
    </svg>
);

export const Icon = ({ ariaLabel, ariaHidden, name, size }) => {
    const LazyIcon = lazy(() => import(`../../assets/icons/${size}/${name}.svg`));
    return (
        <i aria-hidden={ ariaHidden }>
            <ErrorBoundary fallback={ ERROR }>
                <Suspense fallback={ FALLBACK }>
                    <LazyIcon aria-label={ ariaLabel } data-testid="icon-module" />
                </Suspense>
            </ErrorBoundary>
        </i>
    );
};

I’m trying to test the condition where an SVG is passed in that doesn’t exist, in turn rendering the <ErrorBoundary /> fallback. The ErrorBoundary works in the browser, but not in my test.

This is the failing test:

test('shows notdef icon', async () => {
    const { getByTestId } = render(<Icon name='doesnt-exist' />);
    const iconModule = await waitFor(() => getByTestId('icon-notdef'));
    expect(iconModule).toBeInTheDocument();
});

I get this error message:

TestingLibraryElementError: Unable to find an element by: [data-testid="icon-notdef"]”.

How do I access ErrorBoundary fallback UI in my test?

Edit

This is the code for the <ErrorBoundary /> component:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export class ErrorBoundary extends Component {
    constructor(props) {
        super(props);
        this.state = {
            error: '',
            errorInfo: '',
            hasError: false,
        };
    }

    static getDerivedStateFromError(error) {
        return { hasError: true, error };
    }

    componentDidCatch(error, errorInfo) {
        console.error({ error, errorInfo });
        this.setState({ error, errorInfo });
    }

    render() {
        const { children, fallback } = this.props;
        const { error, errorInfo, hasError } = this.state;

        // If there is an error AND a fallback UI is passed in…
        if (hasError && fallback) {
            return fallback;
        }

        // Otherwise if there is an error with no fallback UI…
        if (hasError) {
            return (
                <details className="error-details">
                    <summary>There was an error.</summary>
                    <p style={ { margin: '12px 0 0' } }>{error && error.message}</p>
                    <pre>
                        <code>
                            {errorInfo && errorInfo.componentStack.toString()}
                        </code>
                    </pre>
                </details>
            );
        }

        // Finally, render the children.
        return children;
    }
}

ErrorBoundary.propTypes = {
    children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
    fallback: PropTypes.node,
};

… and this is the full error with DOM that I get for the test:

shows notdef icon

    TestingLibraryElementError: Unable to find an element by: [data-testid="icon-notdef"]

    <body>
      <div>
        <i
          aria-hidden="false"
          class="Icon Icon--sm"
        >
          <span
            aria-label=""
            data-testid="icon-module"
          />
        </i>
      </div>
    </body>

    <html>
      <head />
      <body>
        <div>
          <i
            aria-hidden="false"
            class="Icon Icon--sm"
          >
            <span
              aria-label=""
              data-testid="icon-module"
            />
          </i>
        </div>
      </body>
    </html>Error: Unable to find an element by: [data-testid="icon-notdef"]

Lastly, my SVG mock:

import React from 'react';

const SvgrMock = React.forwardRef(
    function mySVG(props, ref) {
        return <span { ...props } ref={ ref } />;
    },
);

export const ReactComponent = SvgrMock;
export default SvgrMock;

Solution

  • As discussed in the comments, it is most likely the mock is avoiding the error. Try re mocking the SVG files with a new mock throwing an error.

    // tests that require unmocking svg files
    describe('non existent svg', () => {
      beforeAll(() => {
        jest.mock('.svg', () => {
          throw new Error('file not found')
        });
      });
      
      test('shows notdef icon', async () => {
        const { getByTestId } = render(<Icon name='doesnt-exist' />);
        const iconModule = await waitFor(() => getByTestId('icon-notdef'));
        expect(iconModule).toBeInTheDocument();
      });
    
      afterAll(() => jest.unmock('.svg'))
    })
    
    

    It is necessary to wrap it to ensure the SVG files are re-mocked only during the test (beforeAll - afterAll) to not interfere with the rest of the tests.