Previous Issue: https://medium.com/front-end-weekly/tested-react-build-and-test-modal-using-react-features-and-dom-events-39b7246a3a6f

Photo by rawpixel on Unsplash

“Tested React” series of guides to get people accustomed to testing components in React ecosystem. This series IS NOT about setting up testing environments for React — The goal is to help you build intuition on what to test in your React apps.

In this issue, we are going to build a Form that stores all input data in context; so that we can get the data during submission. The goal of this post is to test a component that works with Context.

Specification: Form

To keep this post simple, our Form consists of the following items:

Form Context

Form Component that holds Context Provider

TextInput Component that writes to context and reads from context

Submit Button component (gets disabled during submission)

Before we move to implementation, some things need to be clarified:

Why are we using Context and other components?

The structure of a Form can different from page to page. For example, some form items can be separated by sections (e.g Basic Information, Credentials) or inputs can be aligned horizontally in a Row. As a result, form inputs can be nested inside other tags or even components. On the other hand, it is a good practice to access the Form state in React (not DOM) during submission. That’s why Context works perfectly for this scenario because we can “connect” inputs to the Form context by wrapping each input element in a component and accessing the context for getting input values, errors etc.

Why the Submit Button?

Even though it is not necessary for Submit Button to be connected to the context, due to the fact that the context has all the information about the entire process of the Form (including submission), connecting the button to the form allows for displaying state of the Form during submission (e.g disable the button and show a loading indicator when form is being submitted)

Test: Form with Text Input

Because Context is being used for the Form, there is no point to test the input without Form component. So, let’s start with a simple test and bootstrap our code:

// src/Form/Form.test.js import React from 'react';

import { mount } from 'enzyme'; import Form from './Form'; it('calls onSubmit prop function when form is submitted', () => {

const onSubmitFn = jest.fn();

const wrapper = mount(<Form onSubmit={onSubmitFn}/>); const form = wrapper.find('form');

form.simulate('submit');

expect(onSubmitFn).toHaveBeenCalledTimes(1);

});

We can create our component and context and test to see if the form is submitted:

// src/Form/FormContext.js import { createContext } from 'react'; export default createContext({

getInputValue: (name, defaultValue = '') => null,

inputChange: name => e => {}

}); // We are going to talk about those values in a bit

By default the context is empty with a function that does nothing when input change event is being triggered. Now, onto creating the Form component with state that does nothing at the moment:

// src/Form/Form.js import React from 'react'; import FormContext from './FormContext'; export default class Form extends React.Component {

state = { data: {} }; onSubmit = e => {

e.preventDefault();

this.props.onSubmit();

} render() {

return (

<FormContext.Provider value={null}>

<form method="post" onSubmit={this.onSubmit}>

{this.props.children}

</form>

</FormContext.Provider>

);

}

}

A barebone Form component is ready.

Let’s create TextInput and SubmitButton components that are connected to Context. As usual, let’s start with tests:

// src/Form/TextInput.test.js import React from 'react';

import { mount } from 'enzyme'; import TextInput from './TextInput'; it('renders text input with label (default type)', () => {

const wrapper = mount(<TextInput name="first_name" label="First Name" />);

const label = wrapper.find('label');

expect(label).toHaveLength(1);

expect(label.prop('htmlFor')).toEqual('first_name');

expect(label.text()).toEqual('First Name'); const input = wrapper.find('input');

expect(input).toHaveLength(1);

expect(input.prop('type')).toEqual('text');

expect(input.prop('name')).toEqual('first_name');

expect(input.prop('id')).toEqual('first_name');

}); it('renders email input with label given the type', () => {

const wrapper = mount(<TextInput type="email" name="email" label="Email" />);

const label = wrapper.find('label');

expect(label).toHaveLength(1);

expect(label.prop('htmlFor')).toEqual('email');

expect(label.text()).toEqual('Email'); const input = wrapper.find('input');

expect(input).toHaveLength(1);

expect(input.prop('type')).toEqual('email');

expect(input.prop('name')).toEqual('email');

expect(input.prop('id')).toEqual('email');

});

Implementation:

// src/Form/TextInput.js import React from 'react'; import FormContext from './FormContext'; export default class TextInput extends React.Component {

static contextType = FormContext; render() {

const { name, label, type } = this.props; return (

<div className="input-row">

<label htmlFor={name}>{label}</label>

<input

type={type}

name={name}

id={name}

/>

</div>

);

}

} TextInput.defaultProps = {

type: 'text'

};

Notice that default prop for type is being tested even though it is a React feature. This is because, testing for default prop makes it clear for the tester what the default prop is without needing to dive into code to see it. This is a detail that is completely dependent on the developer to include or not. I personally, like including it as it clarifies the code using tests better.

Now, let’s test and implement Submit Button:

// src/Form/SubmitButton.test.js import React from 'react';

import { mount } from 'enzyme'; import SubmitButton from './SubmitButton'; it('renders submit button with custom text', () => {

const wrapper = mount(<SubmitButton>Click here</SubmitButton>);

const button = wrapper.find('button');

expect(button).toHaveLength(1);

expect(button.prop('type')).toEqual('submit');

expect(button.text()).toEqual('Click here');

});

Implementation:

// src/Form/SubmitButton.js

import React from 'react'; import FormContext from './FormContext'; export default class SubmitButton extends React.Component {

static contextType = FormContext; render() {

return <button type="submit">{this.props.children}</button>;

}

}

Features that can be specified in isolation have been tested and implemented individually. However, Form and Input components must be integrated to each other in order to function properly. Easiest way to test the integration is to test them together. Let’s start with text input first. As mentioned in previous issue, working with events in enzyme that does anything complex (for simply checking a function call is fine) is very convoluted. As a result, we are going to use real DOM and React DOM TestUtils to simulate the change event:

// src/Form/TextInput.test.js import React from 'react';

import ReactDOM from 'react-dom';

import TestUtils from 'react-dom/test-utils';

import { mount } from 'enzyme'; import TextInput from './TextInput';

import Form from './Form.js'; // Previous tests... it('reads and sets input value when using context to store the data', () => {

const wrapper = document.createElement('div'); ReactDOM.render(

<Form>

<TextInput name="first_name" label="First Name" />;

</Form>,

wrapper

); const input = wrapper.querySelector('input'); TestUtils.Simulate.change(input, { target: { value: 'Peter Parker' } }); expect(input.value).toEqual('Peter Parker');

});

We are testing a change event with the event object parameters similar to what a browser typically sends. Then, we test whether the changed value becomes the new value. Now, onto the implementation:

// src/Form/Form.js // ...imports here export default class Form extends React.Component {

state = { data: {} }; onSubmit = e => {

e.preventDefault();

this.props.onSubmit();

} getInputValue = (name, defaultValue = '') => {

return this.state.data[name] || defaultValue;

} inputChange = name => e => {

const targetValue = e.target.value; this.setState(prevState => ({

data: {

...prevState.data,

[name]: targetValue

}

});

} render() {

return (

<FormContext.Provider value={{ getInputValue: this.getInputValue, inputChange: this.inputChange }}>

<form method="post" onSubmit={this.onSubmit}>

{this.props.children}

</form>

</FormContext.Provider>

);

}

}

getInputValue function returns the value in data if it exists, otherwise it returns the provided default value

function returns the value in data if it exists, otherwise it returns the provided default value inputChange function sets the state based on the name provided in outer function and the event target (e.g input element) value provided in the inner function. The use of two functions makes it easier to use the function right inside onChange event, alleviating unnecessary onChange handlers that call this function in separation.

The two functions are then used inside TextInput (or any other input that is connected to the Context):

// src/Form/TextInput.js // ...imports export default class TextInput extends React.Component {

static contextType = FormContext; render() {

const { name, label, type } = this.props; return (

<div className="input-row">

<label htmlFor={name}>{label}</label>

<input

type={type}

name={name}

id={name}

onChange={this.context.inputChange(name)}

value={this.context.getInputValue(name)}

/>

</div>

);

}

} // ...other code

As you can see, inputChange returns a callback/listener for onChange event and that function performs the needed state update. The reason this is possible is because Javascript functions can access the scope of the “parent” function (this is an answer to a popular interview question for Frontend/Javascript 😃).

Now the TextInput is fully connected to the Form Context, getting and setting values for their respective forms based on name. There are couple more things that need to be implemented. Firstly, the submit function must receive the entire form state; so that something can be performed using the state (e.g an API call):

// src/Form/Form.test.js import React from 'react';

import ReactDOM from 'react-dom';

import TestUtils from 'react-dom/test-utils'; import { mount } from 'enzyme'; // REMOVE THIS LINE import Form from './Form';

import TextInput from './TextInput'; // REMOVE THIS TEST AS THIS IS UNNECESSARY

it('calls onSubmit prop function when form is submitted', () => {

const onSubmitFn = jest.fn();

const wrapper = mount(<Form onSubmit={onSubmitFn}/>); const form = wrapper.find('form');

form.simulate('submit');

expect(onSubmitFn).toHaveBeenCalledTimes(1);

});

// REMOVE THIS TEST AS THIS IS UNNECESSARY it('gets the form state from onSubmit function', () => {

const wrapper = document.createElement('div'); // just return the data

const onSubmitFn = jest.fn(data => data); ReactDOM.render(

<Form onSubmit={onSubmitFn}>

<TextInput name="first_name" label="First Name" />;

</Form>,

wrapper

); const input = wrapper.querySelector('input');

const form = wrapper.querySelector('form'); TestUtils.Simulate.change(input, { target: { value: 'Peter Parker' } }); TestUtils.Simulate.submit(form); expect(onSubmitFn).toHaveBeenCalledTimes(1);

expect(onSubmitFn.mock.results[0].value).toEqual({ 'first_name': 'Peter Parker' });

});

Jest Mock functions allow function implementations; so that, we can access the results of a function call and compare the values. Because of this new test, there is no point in having the previous test. So, you can remove the test. Tests can change. They can be replaced with a test that is more meaningful or it can be completely deleted and that is fine. It is part of of the process.

One more thing is left to implement. When form is in submission state, submit button must be disabled and once the submission is done, the button will be enabled again. This requires using async inside tests, which may sound a bit illogical but because of async/await, it is possible to wait for an operation to finish before calling the test.

Firstly, because simulating events and testing for context does not work in Enzyme, we are going to get back to using ReactDOM and TestUtils. Secondly, the submit function and the test case must be async functions:

// src/Form/SubmitButton.test.js import React from 'react';

import { mount } from 'enzyme'; import TestUtils from 'react-dom/test-utils';

import ReactDOM from 'react-dom'; import SubmitButton from './SubmitButton';

import Form from './Form'; it('renders submit button with custom text', () => {

const wrapper = mount(<SubmitButton>Click here</SubmitButton>);

const button = wrapper.find('button');

expect(button).toHaveLength(1);

expect(button.prop('type')).toEqual('submit');

expect(button.text()).toEqual('Click here');

}); // New test case

it('disables the submit button during submission and enables it when done', async () => {

const onSubmitFn = jest.fn(async () => { await new Promise(resolve => resolve()); }); const wrapper = document.createElement('div');

ReactDOM.render(

<Form onSubmit={onSubmitFn}>

<SubmitButton>Click here</SubmitButton>

</Form>,

wrapper

); const button = wrapper.querySelector('button');

const form = wrapper.querySelector('form');

TestUtils.Simulate.submit(form); expect(button.disabled).toBeTruthy(); // This line makes sure the submit function is finished before executing next instructions

await onSubmitFn.mock.results[0].value; expect(button.disabled).toBeFalsy();

});

Now we can implement submitting state in Form and SubmitButton. First, the submitting state should be available in Context:

// src/Form/FormContext.js import { createContext } from 'react'; export default createContext({

getInputValue: (name, defaultValue = '') => null,

inputChange: name => e => {},

isSubmitting: false

});

Submit Button must be disable if isSubmitting is true:

// src/Form/SubmitButton.js

import React from 'react'; import FormContext from './FormContext'; export default class SubmitButton extends React.Component {

static contextType = FormContext; render() {

return <button type="submit" disabled={this.context.isSubmitting}>{this.props.children}</button>;

}

}

Form context will store submitting as state and update it before and after onSubmit property is being called:

// src/Form/Form.js export default class Form extends React.Component {

state = { data: {}, isSubmitting: false }; onSubmit = async e => {

e.preventDefault();

this.setState({ isSubmitting: true });

await this.props.onSubmit();

this.setState({ isSubmitting: false });

} // ...rest of the code render() {

return (

<FormContext.Provider value={{ getInputValue: this.getInputValue, inputChange: this.inputChange, isSubmitting: this.state.isSubmitting }}>

<form method="post" onSubmit={this.onSubmit}>

{this.props.children}

</form>

</FormContext.Provider>

);

}

As you can see, onSubmit handler must also be async because it has to wait for onSubmit to finish changing the state.

Voila! We have a tested Form component with text input and submit button. Following the same principles with context, it is going to be easy to add other input elements.