I used Material-UI for a side project and I ran into problems when trying to write a test for form components, this article illustrates the problem and how it was solved.
React Testing Library was used to write the tests which is a cool library for testing react components and also jest-dom which provides custom matchers that can be used to extend that of jest.
This is my App.js
file
import React, { useState } from 'react';
import {
FormControl,
Input,
InputAdornment,
IconButton,
InputLabel
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Send from '@material-ui/icons/Send';
const useStyle = makeStyles((theme) => ({
root: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 200
}
}));
const App = () => {
const classes = useStyle();
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const handleChange = (event) => {
setInput(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
setOutput(`Hello ${input}`);
};
return (
<div data-testid="form">
<h3>Material Form</h3>
<FormControl className={classes.root}>
<InputLabel htmlFor="adornment-send-title" data-testid="label">
Enter Name
</InputLabel>
<Input
id="adornment-send-title"
type="text"
value={input}
onChange={handleChange}
data-testid="nameInput"
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="submit answer"
onClick={handleSubmit}
data-testid="submit"
>
<Send className={classes.iconColor} />
</IconButton>
</InputAdornment>
}
/>
</FormControl>
<p data-testid="output">{output}</p>
</div>
);
};
export default App;
This is a screenshot of the App:
It renders a simple form component that has a text field and a button. When an input is entered and the button is clicked it displays a message.
This is the test for the App component:
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import App from './App';
it('check if form displays', () => {
const { getByTestId } = render(<App />);
const form = getByTestId('form');
const output = getByTestId('output');
const label = getByTestId('label');
const nameInput = getByTestId('nameInput');
const submit = getByTestId('submit');
expect(form).toBeInTheDocument();
expect(output).toBeEmpty('');
expect(label).toHaveTextContent('Enter Name');
expect(nameInput).toHaveValue('');
expect(submit).toBeInTheDocument();
});
it('should check if message is displayed when button is clicked', () => {
const { getByTestId } = render(<App />);
const output = getByTestId('output');
const nameInput = getByTestId('nameInput');
const submit = getByTestId('submit');
expect(output).toBeEmpty('');
expect(nameInput).toHaveValue('');
fireEvent.change(nameInput, { target: { value: 'Sama' } });
fireEvent.click(submit);
expect(nameInput).toHaveValue('Sama');
expect(output).not.toBeEmpty('');
});
There are two tests in the file, the first checks if the page renders correctly and the second checks if the button performs the actions it is expected to perform.
Problem
When you run npm test
you get the following errors:
From the test errors, we can clearly see that
const nameInput = getByTestId('nameInput');
returns undefined which is odd considering the component value is supposed to be an empty string
<Input
required
id="adornment-send-title"
type="text"
value={input}
data-testid="nameInput"
onChange={handleChange}
endAdornment={
<InputAdornment position="end">
<IconButton
type="submit"
aria-label="submit answer"
data-testid="submit"
>
<Send className={classes.iconColor} />
</IconButton>
</InputAdornment>
}
/>
Solution
So after being stuck for a while I decided to inspect the input element in the browser and realized that the Material UI TextField has a div
wrapped around the input
so I further explored by checking the TextField API documentation which can be accessed here. It confirmed my suspicion and further explained that to alter the props to the input element then inputProps
has to be used so I moved the data-testid
attribute to inputProps
and it resulted in this:
<Input
id="adornment-send-title"
type="text"
value={input}
onChange={handleChange}
inputProps={{
'data-testid': 'nameInput'
}}
endAdornment={
<InputAdornment position="end">
<IconButton
type="submit"
aria-label="submit answer"
data-testid="submit"
>
<Send className={classes.iconColor} />
</IconButton>
</InputAdornment>
}
/>
After that, I ran the tests again and viola the results of the tests were successful.
So, shameless plug this is the app created with Material UI, it is a lyric trivia app that tests knowledge of lyrics and this is the repo for it.
Top comments (6)
I would be interested in hearing your experience with submitting the form. I notice that you use 'fireEvent.click' which is the only way I could get it to work. I have seen others claim that fireEvent.submit works, but I have not had that experience. Thanks again for your post. It was most helpful.
Hello Kevin, thanks for reading the article. I don't have any experience with using fireEvent.submit. However, I'll check it out to see how it works.
@sama , have you found any way to have webpack remove the data-attribs from a production build or do you just leave them in?
@goesbysteve I use CRA to bootstrap my React applications and l let it handle webpack issues so I have no idea if the data-attribs are removed from the production build. I'll check it out though. Thanks.
I found this solution :
npmjs.com/package/babel-plugin-jsx...
In my own example, a callback function, handeInput(), in my onChange prop of the Input component "is not recognized" and failing my tests. Any ideas on how to fix this?