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