28

I have been trying (without success) to write a test case for ErrorBoundary component that is handling errors via componentDidCatch lifecycle method. Despite the error produced by child component inside the <ErrorBoundry> component, <ErrorBoundry> does not render info about error in code but the content of faulty component if it would work correct. Component works as expected in production/development but not when it is executed by Jest / Enzyme for testing.

Error from testing:

 PASS  src/ErrorBoundary.test.js
  ● Console

    console.error node_modules/fbjs/lib/warning.js:33
      Warning: `value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.
          in input (at ErrorBoundary.test.js:11)
          in div (at ErrorBoundary.test.js:10)
          in ComponentWithError (at ErrorBoundary.test.js:26)
          in ErrorBoundry (created by WrapperComponent)
          in WrapperComponent
    console.log src/ErrorBoundary.test.js:29
      <ErrorBoundry>
        <ComponentWithError>
          <div>
            <input type="text" value={{...}} />
          </div>
        </ComponentWithError>
      </ErrorBoundry>

ErrorBoundry.js:

import React, { Component } from 'react'
import Raven from 'raven-js'
import { Segment, Button } from 'semantic-ui-react'

export default class ErrorBoundry extends Component {
    state = {
        hasError: false
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true })
        Raven.captureException(error, { extra: info });
    }

    render() {
        if(this.state.hasError) {
            return (
                <div className='error-boundry'>
                    <Segment>
                        <h2> Oh no! Somethin went wrong </h2>
                        <p>Our team has been notified, but click  
                            <Button  onClick={() => Raven.lastEventId() && Raven.showReportDialog()}> 
                            here </Button> to fill out a report.
                        </p>
                    </Segment>
                </div>
            );
        } else {
            return this.props.children;
        }
    }
}

ErrorBoundry.test.js:

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import renderer from 'react-test-renderer'
import { shallow, mount } from 'enzyme'
import ErrorBoundary from './ErrorBoundary'

class ComponentWithError extends Component {
  render() {
    return (
      <div>
        <input type = "text" value = {null}/>  
      </div>
    );
  }
}

describe('<ErrorBoundary> window',()=> {
  it('should match the snapshot', () => {
    const tree = renderer.create(<ErrorBoundary>Test</ErrorBoundary> ).toJSON()
    expect(tree).toMatchSnapshot()
  })

  it('displays error message on error generated by child', () => {
    const wrapper = mount(
      <ErrorBoundary > 
        <ComponentWithError />
      </ErrorBoundary> 
      )
    console.log(wrapper.debug() )
  })
})
skyboyer
  • 22,209
  • 7
  • 57
  • 64
Sylwek
  • 685
  • 1
  • 7
  • 16

3 Answers3

22

Enzyme has simulateError helper now.

So this works very well for me:

const Something = () => null;

describe('ErrorBoundary', () => {
  it('should display an ErrorMessage if wrapped component throws', () => {
    const wrapper = mount(
      <ErrorBoundary>
        <Something />
      </ErrorBoundary>
    );

    const error = new Error('test');

    wrapper.find(Something).simulateError(error);

    /* The rest fo your test */
  }
}
Christopher Moore
  • 3,071
  • 4
  • 30
  • 46
Daniil Ivanov
  • 233
  • 2
  • 7
8

After additional research I found that it is an open issue that has to be solved by Enzyme. https://github.com/airbnb/enzyme/issues/1255

I have implemented it as follows:

function ProblemChild() {
  throw new Error('Error thrown from problem child');
  return <div>Error</div>; // eslint-disable-line
}

describe('<ErrorBoundary> window',()=> {  
  it('displays error message on error generated by child', () => {
    const spy = sinon.spy(ErrorBoundary.prototype, 'componentDidCatch')
    mount(<ErrorBoundary><ProblemChild /></ErrorBoundary>)
    chaiExpect(ErrorBoundary.prototype.componentDidCatch).to.have.property('callCount', 1)
  })
})

Proposed workaround works anyhow

  1. it is still not possible to test error message rendered to the app user by <ErrorBoundary>
  2. test console displays warnings:

    PASS src/ErrorBoundary.test.js ● Console

    console.error node_modules/react-dom/cjs/react-dom.development.js:9627
      The above error occurred in the <ProblemChild> component:
          in ProblemChild (at ErrorBoundary.test.js:37)
          in ErrorBoundry (created by WrapperComponent)
          in WrapperComponent
    
      React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundry.
    
Sylwek
  • 685
  • 1
  • 7
  • 16
0

Adding to @Andreas Köberle's comment since the hasError state changes on ComponentDidCatch lifecycle method, you could also use enzymes setState.

You also don't need to mount the comment, shallow would do.

  it('displays error message on error generated by child', () => {
    const wrapper = shallow(
      <ErrorBoundary > 
        <ComponentWithError />
      </ErrorBoundary> 
    );
    wrapper.setState({ hasError: true });
    wrapper.toMatchSnapshot()
  });
dcodesmith
  • 9,590
  • 4
  • 36
  • 40