Testing state mutations with Enzyme.js and wrapper.update()

When testing React components, I often reach for Enzyme.js, a library of testing utilities for React components developed by the team at Airbnb. It’s become a standard tool among React developers. Recently, I was testing a component that used state to determine the className given to a child component. Here’s the general idea in code:

import React from 'react';

export default class FormWithCollapsableRows extends Component {

    constructor(props: Props) {
        this.state = {
            hidden: false,
        };
    }

    toggleHidden() {

        /**
         * Toggles the state for form rows.
         */

        this.setState(prevState => ({
            hidden: !prevState.hidden,
        }));
    }

    setFormRowClass(rowType: string) {

        /**
         * Sets the class names for each form row, based on the
         * row type and whether `state.hidden` is true or false.
         */

        const {
            hidden,
        } = this.state;
        const partyRowClassNames = hidden ? ['hidden'] : [];

        return partyRowClassNames.concat([rowType]).join(' ');
    }

    render() {

        return <tbody className='expandable-table'>

            <tr className={this.setFormRowClass('row-type-0')}>
                <td>
                    Row 0 content goes here
                </td>
            </tr>

            <tr className={this.setFormRowClass('row-type-1')}>
                <td>
                    Row 1 content goes here
                </td>
            </tr>

        </tbody>;
    }
}

In the LESS style sheet, I have something like this going on:

.expandable-table {
    tr {
        .hidden {
            visibility: collapse;
        }
    }
}

When the component’s has state.expanded === false, the hidden class is applied to the rows, hiding them with the style visibility: collapse. I wanted to test whether the rendered component was correctly applying my class naming logic, so I wrote up a test like this:

import { shallow } from 'enzyme';
import test from 'tape';

import FormWithCollapsableRows from './FormWithCollapsableRows';

test('When `state.hidden === false`:', t => {

    const wrapper = shallow(<FormWithCollapsableRows />);

    // The component mounts with `state.hidden ==== false`,
    // so the class names should not include "hidden" to
    // begin with.

    t.notOk(
        wrapper.containsMatchingElement(
            <tr className='hidden row-type-0' />),
        'the rows should not be hidden');

    t.end();
});

test('When `state.hidden === true`:', t => {

    const wrapper = shallow(<FormWithCollapsableRows />);

    // Once we toggle `state.hidden` to `true`, the class
    // names should include "hidden"

    wrapper.instance().toggleHidden();

    t.ok(
        wrapper.containsMatchingElement(
            <tr className='hidden row-type-0' />),
        'the rows should be hidden');

    t.end();
});

However, I found that my second test case was failing. After I made the call to wrapper.instance().toggleHidden(), I expected the class name to update according to my render function, which called this.setFormRowClass(). Instead, it seemed that the wrapper I was testing had not received the state update.

It took some perusal of the Enzyme docs to find update(), a method on shallow wrappers that allows you to force a re-render on the component. This turned out to be exactly what I needed. I modified the second test case like this:

test('When `state.hidden === true`:', t => {

    const wrapper = shallow(<FormWithCollapsableRows />);

    // Once we toggle `state.hidden` to `true`, the class
    // names should include "hidden"

    wrapper.instance().toggleHidden();

    // Triggers a re-render, so we know that `this.setFormRowClass` 
    // will be called:

    wrapper.update();

    t.ok(
        wrapper.containsMatchingElement(
            <tr className='hidden row-type-0' />),
        'the rows should be hidden');

    t.end();
});

Now the test passes, as expected.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *