Source:  Twitter logo

I have a fairly basic react component in a React app. I want to test that the "submitted" portion of the state changes from false to true when the form is submitted. Not particularly hard. But the enzyme test seems unable to find the button. Not sure if it has to do with the if/else statement.

Here is the component:

import React from 'react';
import { connect } from 'react-redux';
import { questionSubmit } from '../actions/users';
import { getCurrentUser, clearMessage } from '../actions/auth';

export class AnswerForm extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            submitted: false
        }
    }

    handleFormSubmit(event) {
        event.preventDefault();
        this.setState({ submitted: true });
        this.props.dispatch(questionSubmit(this.answerInput.value, this.props.currentUsername));
        this.answerInput.value = '';
    }

    handleNextButton() {
        this.setState({ submitted: false });
        this.props.dispatch(getCurrentUser(this.props.currentUsername))
    }

    render() {
        let nextButton;
        let form;
        let message = <p>{this.props.message}</p>
        if (this.state.submitted) {
            nextButton = <button className="button-next" onClick={() => this.handleNextButton()}>Next</button>;
        }
        else {
            form = 
            <form onSubmit={e => this.handleFormSubmit(e)}>
                <input className="input-answer" ref={input => this.answerInput = input}
                    placeholder="Your answer" />
                <button id="button-answer" type="submit">Submit</button>
            </form>;
        }

        return (
            <div>
                <p>{this.props.message}</p>
                {form}
                {nextButton}
            </div>
        )
    }
}

export const mapStateToProps = (state, props) => { 
    return {
        message: state.auth.message ? state.auth.message : null, 
        currentUsername: state.auth.currentUser ? state.auth.currentUser.username : null,
        question: state.auth.currentUser ? state.auth.currentUser.question : null
    }
}

export default connect(mapStateToProps)(AnswerForm); 

Here is the test:

import React from 'react'; 
import {AnswerForm} from '../components/answer-form'; 
import {shallow, mount} from 'enzyme'; 

describe('<AnswerForm />', () => {
    it('changes submitted state', () => {
        const spy = jest.fn(); 
        const wrapper = mount(<AnswerForm dispatch={spy}/> );
        wrapper.instance(); 
        expect(wrapper.state('submitted')).toEqual(false);
        const button = wrapper.find('#button-answer');
        button.simulate('click') 
        expect(wrapper.state('submitted')).toEqual(true); 
    }); 
}); 

I get this error when I try running this test:

   expect(received).toEqual(expected)

    Expected value to equal:
      true
    Received:
      false

      at Object.it (src/tests/answer-form.test.js:24:44)
          at <anonymous>
      at process._tickCallback (internal/process/next_tick.js:188:7)

Any ideas? It's a pretty straight shot other than the if statement. Not sure what is going on here.

The issue here is that the intrinsic DOM event propagation that is expected to occur between a submit button and a form element is not being done by enzyme or React during simulation.

The event system in React is all synthetic in order to normalise browser quirks, they actually all get added to document (not the node you add the handler to) and fake events are bubbled through the components by React (I highly recommend watching this webinar from the React core team explaining in event system in depth)

This makes testing them a little unintuitive and sometimes problematic, because simulation does not trigger real DOM event propagation

In enzyme, events triggered on shallow renders are not real events at all and will not have associated DOM target. Even when using mount which does have a DOM fragment backing it, it still uses React's synthetic event system, so simulate still only tests synthetic events bubbling though your components, they do not propagate via real DOM, so simulating a click on a submit button does not in turn intrinsically trigger a submit DOM event on the form itself, as its the browser not React that is responsible for that. https://github.com/airbnb/enzyme/issues/308

So two ways to get around that in a test are...

1) Not ideal from a UI test perspective as bypasses button, but clean for a unit test, especially as it should work with shallow rendering to isolate the component.

describe('<AnswerForm />', () => {

    const spy = jest.fn(); 
    const wrapper = shallow(<AnswerForm dispatch={spy}/> ); 

    it('should show form initially', () => {
        expect(wrapper.find('form').length).toEqual(0);
    })

    describe('when the form is submitted', () => {

        before(() => wrapper.find('form').simulate('submit')))

        it('should have dispatched the answer', () => {
            expect(spy).toHaveBeenCalled(); 
        }); 

        it('should not show the form', () => {
            expect(wrapper.find('form').length).toEqual(0); 
        }); 

        it('should show the "next" button', () => {
            expect(wrapper.find('#button-next').length).toEqual(1); 
        }); 

    });

});

2) Trigger a real click event on DOM button element itself, rather than simulating it on your component as if it were a Selenium functional test (so feels a little dirty here), which the browser will propagate into a form submit before React catches the submit event and takes over with synthetic events. Therefore this only works with mount

describe('<AnswerForm />', () => {

    const spy = jest.fn(); 
    const wrapper = mount(<AnswerForm dispatch={spy}/> ); 

    it('should show form initially', () => {
        expect(wrapper.find('form').length).toEqual(0);
    })

    describe('when form is submitted by clicking submit button', () => {

        before(() => wrapper.find('#button-answer').getDOMNode().click())

        it('should have dispatched the answer', () => {
            expect(spy).toHaveBeenCalled(); 
        }); 

        it('should not show the form', () => {
            expect(wrapper.find('form').length).toEqual(0); 
        }); 

        it('should show the "next" button', () => {
            expect(wrapper.find('#button-next').length).toEqual(1); 
        }); 

    });

});

You'll also note I'm not testing state itself. It's generally bad practice to test state directly as its pure implementation detail (state change should eventually cause something more tangible to happen to the component that can instead be tested).

Here I have instead tested that your event causes the dispatch spy to have been called with correct args, and that the Next button is now shown instead of the form. That way it is more focused on outcomes and less brittle should you ever refactor the internals.

6 users liked answer #0dislike answer #06
alechill profile pic
alechill

Be mindful that the component you are testing is not the AnswerForm class component, but rather the wrapped component created by passing AnswerForm to react-redux's connect higher order component.

If you use shallow rendering rather than full mounting, you can use the dive() function of the Enzyme API to get to your actual class component. Try this:

import React from 'react'; 
import {AnswerForm} from '../components/answer-form'; 
import {shallow, mount} from 'enzyme'; 

describe('<AnswerForm />', () => {
    it('changes submitted state', () => {
        const spy = jest.fn(); 
        const wrapper = shallow(<AnswerForm dispatch={spy}/> ); 
        expect(wrapper.dive().state('submitted')).toEqual(false);
        const button = wrapper.dive().find('#button-answer');
        button.simulate('click') 
        expect(wrapper.dive().state('submitted')).toEqual(true); 
    }); 
});

Another option is to test the non-wrapped component instance directly. To do this, you just need to change your export and import. In answer-form.js:

export class AnswerForm extends React.Component

...your code

export default connect(mapStateToProps)(AnswerForm);

This exports the non-wrapped component in addition to the wrapped component. Then your imports in answer-form.test.js:

import WrappedAnswerForm, { AnswerForm } from 'path/to/answer-form.js`;

This way, you can test AnswerForm functionality independently, assuming you don't need to test any Redux received props. Check out this GitHub issue for more guidance.

0 users liked answer #1dislike answer #10
Parker Ziegler profile pic
Parker Ziegler

Copyright © 2022 QueryThreads

All content on Query Threads is licensed under the Creative Commons Attribution-ShareAlike 3.0 license (CC BY-SA 3.0).