I am trying to create a test with a styled Material-UI component using react-testing-library in typescript. I'm finding it difficult to access the internal functions of the component to mock and assert.
Form.tsx
export const styles = ({ palette, spacing }: Theme) => createStyles({
root: {
flexGrow: 1,
},
paper: {
padding: spacing.unit * 2,
margin: spacing.unit * 2,
textAlign: 'center',
color: palette.text.secondary,
},
button: {
margin: spacing.unit * 2,
}
});
interface Props extends WithStyles<typeof styles> { };
export class ExampleForm extends Component<Props, State> {
async handleSubmit(event: React.FormEvent<HTMLFormElement>) {
// Handle form Submit
...
if (errors) {
window.alert('Some Error occurred');
return;
}
}
// render the form
}
export default withStyles(styles)(ExampleForm);
Test.tsx
import FormWithStyles from './Form';
it('alerts on submit click', async () => {
jest.spyOn(window,'alert').mockImplementation(()=>{});
const spy = jest.spyOn(ActivityCreateStyles,'handleSubmit');
const { getByText, getByTestId } = render(<FormWithStyles />)
fireEvent.click(getByText('Submit'));
expect(spy).toHaveBeenCalledTimes(1);
expect(window.alert).toHaveBeenCalledTimes(1);
})
jest.spyOn
throws the following error Argument of type '"handleSubmit"' is not assignable to parameter of type 'never'.ts(2345)
probably because ExampleForm in wrapped in withStyles.
I also tried directly importing the ExampleForm
component and manually assigning the styles, was couldn't do so:
import {ExampleForm, styles} from './Form';
it('alerts on submit click', async () => {
...
const { getByText, getByTestId } = render(<ActivityCreateForm classes={styles({palette,spacing})} />)
...
}
Got the following error: Type '{ palette: any; spacing: any; }' is missing the following properties from type 'Theme': shape, breakpoints, direction, mixins, and 4 more.ts(2345)
I'm finding it difficult to write basic tests in Typescript for Material-UI
components with react-testing-library
& Jest
due to strong typings and wrapped components. Please Guide.
First of all when you use render
method of react-testing-library you don't need to worry about using withStyles
or any wrapper because at the end it renders the component as it could be in the real dom so you can write your tests normally.
Then as far as I can see you are doing the same thing I did when I was starting with tests (it means you are going to become good at it ;). You are trying to mock an internal method and that is not the best approach to follow because what you need to do is to test the real method.
So let's image that we have a Register
users component.
src/Register.tsx
import ... more cool things
import * as api from './api';
const Register = () => {
const [name, setName] = useState('');
const handleNameChange = (event) => {
setName(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
if (name) {
api.registerUser({ name });
}
};
return (
<form onSubmit={handleSubmit}>
<TextField
id='name'
name='name'
label='Name'
fullWidth
value={name}
onChange={handleNameChange}
/>
<Button data-testid='button' fullWidth type='submit' variant='contained'>
Save
</Button>
</form>
);
}
The component is pretty simple, its a form with an input and a button. We are using react hooks
to change the input value and based on that we call or not api.registerUser
when handleSubmit
event is fired.
To test the component the first thing we need to do is to mock api.registerUser
method.
src/__tests__/Register.tsx
import * as api from '../api'
jest.mock('../api')
api.registerUser = jest.fn()
This will allow us to see if that method is called or not.
The next thing to do is ... write the tests, in this scenario we can test two things to see if handleSubmit
is working correctly.
api.registerUser
if name is empty.it('should not call api registerUser method', () => {
const { getByTestId } = render(<Register />)
fireEvent.click(getByTestId('button'))
expect(api.registerUser).toHaveBeenCalledTimes(0)
})
api.registerUser
if name is not empty.it('should call api registerUser method', () => {
const { getByLabelText, getByTestId } = render(<Register />)
fireEvent.change(getByLabelText('Name'), { target: { value: 'Steve Jobs' }})
fireEvent.click(getByTestId('button'))
expect(api.registerUser).toHaveBeenCalledTimes(1)
})
In this last test implicitly we are testing handleNameChange
too because we are changing the name :) so name
is not going to be empty and registerUser
is going to be called.
The example with withStyles and typescript is in this repo.
The demo is here.
Why don't you use enzyme
with Full DOM Rendering?
You can use simulate
method to simulate events on mounted components.
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
const { count } = this.state;
return (
<div>
<div className={`clicks-${count}`}>
{count} clicks
</div>
<a href="url" onClick={() => { this.setState({ count: count + 1 }); }}>
Increment
</a>
</div>
);
}
}
const wrapper = mount(<Foo />);
expect(wrapper.find('.clicks-0').length).to.equal(1);
wrapper.find('a').simulate('click');
expect(wrapper.find('.clicks-1').length).to.equal(1);
You can use unwrap to unwrap the wrapped styled component then test it
import { unwrap } from '@material-ui/core/test-utils';
import {ExampleForm, styles} from './Form';
it('alerts on submit click', async () => {
...
const unwrapped = unwrap(ExampleForm);
...
}
Then you can do he required testing on the unwrapped object
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With