loading...

Testing Form Reset with Enzyme

jeffpereira profile image Jeff Pereira ・5 min read

Introduction

Being a relatively new developer to Javascript, I see everything I work on as an opportunity to learn. On the current app that I work on, test coverage needs some significant improvement, and there is a push to not only have as close to 100% test coverage on new features, but to add tests for any code you touch as a bigger initiate to increase the test coverage of the app. This initiative has really made me interested in writing tests, so my next two posts are going to be on this subject.

The Problem

Recently I was tasked with creating a new form. This is a simple Redux form with one field. All of the other data needing to be sent is masked and just sent as part of the API request. So the form only has a "Name" field, a "Cancel" button, and finally a "Submit" button.

For this entry let's just focus in on testing the "Cancel" button. The main things that I want to make sure of is that the form, which is in a modal closes, and the fact that the form resets. For example, let's say in the "Name" field you entered in "FOO" and decided to cancel and come back to that later, we should not save that input. It is a best practice in our case to clear the form for the user.

While writing tests for this form I came across an interesting issue. I was using enzyme to test the render, and it seemed like I could not find a way to test the aforementioned "Cancel" button effectively. I was able to test that the modal closed, but verifying that the field in the form was no longer filled in became a little difficult.

The Button

The button in our app creates a new thing. Let's call it "Create Foo". Once clicked it will make a modal visible that has a form inside of it. here is some sample code for the button.

class CreateFooButton extends Component {
    constructor(props) {
        super(props);
        this.state = { dialogOpen: false };
    }

    static propTypes = {
        someName: string
    }

    static defaultProps = {
        someName: ''
    }

    setDialogOpen(dialogOpen) {
        return this.setState({ dialogOpen });
    }

    render() {
        const { dialogOpen } = this.state;
        const { someName } = this.props;

        if (!someName) return null;
        return (
            <div>
                <Button
                    onClick={ () => this.setDialogOpen(true) }
                    name='button'
                    label={ I18n.t('create_foo_button') }
                />

                <Modal
                    visible={ dialogOpen }
                >
                    <CreateFooModalForm
                        setDialogOpen={ this.setDialogOpen.bind(this) }
                        someName={ someName }
                    />
                </Modal>
            </div>
        );
    }
}

The Form

In the form below we just have a simple name field. The user can then hit "Submit" which will then call the function handleSubmit which is the part that send out the magical API request with all of the data. If the user clicks the "Cancel" button, we reset the form, and close the modal.

            <StandaloneForm>
                <div styleName='container'>
                    <Confirm
                        disabled={ disabled }
                        onTouchTapPrimary={ handleSubmit(this.handleFormSubmit) }
                        onTouchTapSecondary={ () => {
                            reset();
                            setDialogOpen(false);
                        } }
                        label='save'
                    >
                        <Field
                            name={ NAME }
                        />
                    </Confirm>
                </div>
            </StandaloneForm>

The Tests

Making Sure the Modal Closes

First we want to test that the modal closes. this is something relatively trivial using some of the tools that Enzyme gives us. Below we will set up some data to render the component, and mock out the setDialogOpen function. Here we are not testing the implementation of that function as it is not necessary, we just want to make sure that the function is called. We want to make sure that filling in the form has no effect on the "Cancel" button. So we will it in, find the "Cancel" button, and then click on it.

        describe('when Cancel button is clicked', () => {
            test('should close the modal form', () => {
                const mockedSetDialogOpen = jest.fn();
                const wrapper = mount(
                    <Root>
                        <CreateFooModalForm
                            setDialogOpen={ mockedSetDialogOpen }
                            someName={ 'foo' }
                        />
                    </Root>
                );
                const input = wrapper.findWhere(
                    n => n.name() === 'Field' && n.props().name === NAME
                ).find('input');
                input.simulate('change', { target: { value: 'bar' }});
                const cancel = wrapper.findWhere(
                    n => n.name() === 'button'
                ).first();

                cancel.simulate('click');

                expect(mockedSetDialogOpen).toHaveBeenCalledWith(false);
            });

        });

Testing the Modal's Content

When testing the content inside of the modal it would be simple enough to find it with Enzyme's find, and then write a simple assertion that the content is there. This however does not work because modals are rendered in a different portal element than everything else on the page. Some modal libraries like react-modal provide some tools for testing this, but unfortunately we are not using one of those libraries. So in order to test any of the content inside of the form modal we use this bit of stubbing logic to replace our modal with a div that ports in all of the content. A bit hacky, but we could not think of a better way with the tools we have.

jest.mock('components/Modal', () => ({ children, ...rest }) => <div id='modal' data={{ ...rest }}>{ children }</div>);

Making Sure the Form Resets

Initially I thought to myself, find the button, click it and then just make sure everything looks right. One of the things I wanted to make sure of though, is that I need to fill this field in, and then make sure that the change is present.

describe('when Cancel button is clicked', () => {
            test('should clear out text from fields', () => {
                const mockedSetDialogOpen = jest.fn();
                const sidebarParams = {
                    ...a bunch of unimportant data
                };
                const wrapper = mount(
                    <Root>
                        <CreateFooModalForm
                            setDialogOpen={ mockedSetDialogOpen }
                            someName={ 'foo' }
                        />
                    </Root>
                );
                // find field and update input with value
                const field = wrapper.findWhere(
                    n => n.name() === 'Field' && n.props().name === NAME
                ).find('input').simulate('change', { target: { value: 'bar' }});

                expect(field).toHaveProp('value', 'bar');
                // find cancel button
                const cancel = wrapper.findWhere(
                    n => n.name() === 'button'
                ).first();

                cancel.simulate('click');

                expect(inputAfterReset).toHaveProp('value', '');
                expect(mockedSetDialogOpen).toHaveBeenCalledWith(false);
            });
        });

The first problem we run into here is that when we expect(field).toHaveProp('value', 'bar'); it fails. When logging the wrapper after the simulated change, the data is not present. It took me more than a few seconds to realize I need to find the field again.

describe('when Cancel button is clicked', () => {
            test('should clear out text from fields', () => {
                const mockedSetDialogOpen = jest.fn();
                const sidebarParams = {
                    ...a bunch of unimportant data
                };
                const wrapper = mount(
                    <Root>
                        <CreateFooModalForm
                            setDialogOpen={ mockedSetDialogOpen }
                            someName={ 'foo' }
                        />
                    </Root>
                );
                // find field and update input with value
                wrapper.findWhere(
                    n => n.name() === 'Field' && n.props().name === NAME
                ).find('input').simulate('change', { target: { value: 'bar' }});
                // we need to do find the field again to get the updated value
                const field = wrapper.findWhere(
                    n => n.name() === 'Field' && n.props().name === NAME
                ).find('input');
                expect(field).toHaveProp('value', 'bar');

                // find cancel button
                const cancel = wrapper.findWhere(
                    n => n.name() === 'button'
                ).first();

                cancel.simulate('click');


                expect(field).toHaveProp('value', '');
                expect(mockedSetDialogOpen).toHaveBeenCalledWith(false);
            });
        });

From here, we get a similar failure. Even though we simulate the clicking of the cancel button, the data does not change. I linked this one together a little quicker. We need to find the field yet again to get the updated value from the cancel button.

describe('when Cancel button is clicked', () => {
            test('should clear out text from fields', () => {
                const mockedSetDialogOpen = jest.fn();
                const sidebarParams = {
                    ...a bunch of unimportant data
                };
                const wrapper = mount(
                    <Root>
                        <CreateFooModalForm
                            setDialogOpen={ mockedSetDialogOpen }
                            someName={ 'foo' }
                        />
                    </Root>
                );
                // find field and update input with value
                wrapper.findWhere(
                    n => n.name() === 'Field' && n.props().name === NAME
                ).find('input').simulate('change', { target: { value: 'bar' }});
                // we need to do find the field again to get the updated value
                const inputBeforeReset = wrapper.findWhere(
                    n => n.name() === 'Field' && n.props().name === NAME
                ).find('input');
                expect(inputBeforeReset).toHaveProp('value', 'bar');
                // find cancel button
                const cancel = wrapper.findWhere(
                    n => n.name() === 'button'
                ).first();

                cancel.simulate('click');

                // find field again to that the values are updated after the reset
                const inputAfterReset = wrapper.findWhere(
                    n => n.name() === 'Field' && n.props().name === NAME
                ).find('input');

                expect(inputAfterReset).toHaveProp('value', '');
                expect(mockedSetDialogOpen).toHaveBeenCalledWith(false);
            });
        });

Posted on by:

jeffpereira profile

Jeff Pereira

@jeffpereira

I am passionate about programming, foosball, my old collector cars, and my fish tank.

Discussion

pic
Editor guide