How do I test the fallback component for the ErrorBoundary?
Asked Answered
T

1

10

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;
Transitive answered 16/2, 2021 at 23:45 Comment(8)
Could you add the code for your ErrorBoundary component to the question?Dosia
Also, what does the DOM look like during the test? You can use debug() from RTL to check.Dosia
Added further detail, @juliomalves!Transitive
So if the an element with data-testid="icon-module" gets rendered that means it's not erroring at all? Could it be the SVG mock interfering?Dosia
@Dosia — I’m honestly not sure. I’ve tried to replicate on Codesandbox and haven’t been able to.Transitive
How are you using your SVG mock? If you don't mock the SVGs, do you still get the issue?Dosia
If it helps, I had to do some very similar tests for my own library, feel free to reference them for inspiration. I opted to use react-test-renderer directly though rather than react-testing-library.Escalate
What mechanism are you using for mocking svg files? you need to know a way to revert it and do it in that single testAirtight
A
1

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.

Airtight answered 25/2, 2021 at 4:50 Comment(7)
We’re on to something here! If I remove the mock from config and run just this test, it passes! But if I reenable the mock and add jest.unmock('../../../test/__mocks__/svg.js'); at the beginning of the test it fails as it did before.Transitive
@BrandonDurham did you try with the path in the answer? ../../assets/icons/sm/doesnt-exist.svgAirtight
I sure did. Fails in the same way.Transitive
@BrandonDurham, I updated my answer assuming you're using the moduleNameMapper option in jest config. I tried it, and it workedAirtight
That particular regex didn’t work, but this one did: '^(?!.*(doesnt-exist)).*svg$': '<rootDir>/test/__mocks__/svg.js', Not a great idea to put this in config, though, is it?Transitive
@BrandonDurham definitely not. I'll try another approach later todayAirtight
@BrandonDurham I updated the answer with a new approach, it's a little verbose, but you could make a utility function if you're testing unexisting SVG files somewhere elseAirtight

© 2022 - 2024 — McMap. All rights reserved.