How would i be able to unit-test onChange method on this component.
Comment.js
import React from "react";
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
const Comment = (props) => (
<div>
<form onSubmit={props.onSubmit}>
<TextField
type="text"
id="outlined-multiline-static"
label="Write A Comment"
multiline
name="comment_body"
value={props.commentBody}
rows="10"
fullWidth
margin="normal"
variant="outlined"
onChange={props.commentChange}
/>
{/* <Button type="submit" variant="outlined" component="span" color="primary">
Post A Comment
</Button> */}
<button type="submit" variant="outlined" component="span" color="primary">
Write a Comment
</button>
</form>
</div>
)
export default Comment;
This is my attempt to unit test the onChange component, getting a
Method “simulate” is meant to be run on 1 node. 0 found instead
around this line
const component = shallow(<Comment commentChange={onChangeMock} commentBody={'test'} />)
component.find('input').simulate('change');
Comment.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import { shallow } from 'enzyme';
import Comment from './Comment';
describe('Should render <Comment/> component', () => {
it('Should render form', () => {
const wrapper = shallow(<Comment/>)
// wrapper.find('Form').at(0)
expect(wrapper.find("form")).toHaveLength(1); // checks if there is a form.
})
it('Should render button', () => {
const wrapper = shallow(<Comment/>)
expect(wrapper.find('button')).toHaveLength(1);
})
it('should check for onChange method', () => {
// const wrapper = shallow(<Comment onChange={}/>)
const onChangeMock = jest.fn();
// const event = {
// preventDefualt(){},
// target: {
// value: 'testing'
// }
// }
const component = shallow(<Comment commentChange={onChangeMock} commentBody={'test'} />)
component.find('input').simulate('change');
expect(onChangeMock).toBeCalledWith('test')
})
})
The Comment component is being passed in another component like this:
ImageContainer.js
state = {
isComment: false,
comment_body: ""
}
handleCommentChange = (e) => {
this.setState({
comment_body: e.target.value
})
}
commentSubmit = (event, id) => {
event.preventDefault();
console.log(this.state.comment_body); // doesn't get console.log
// note that commentBody is being used for the req.body as well so its called by req.body.commentBody
const commentBody = this.state.comment_body
const data = {
commentBody,
id
}
this.props.postComment(data);
this.setState({
comment_body: ''
})
}
<Comment onSubmit={(e) => this.commentSubmit(e, img.id)}
commentBody={this.state.comment_body }
commentChange={this.handleCommentChange}/>
The reason you are having the error is because when you call component.find('input') it returns an array of matched components, so what you want to do is
component.find('input').at(0).simulate('change')
However, there is another way you can test this, which is my preferred method.
component.find('input').at(0).props().onChange()
Below is the correct way to do the test with both methods
import React from "react";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Comment from "./Comment";
import TextField from "#material-ui/core/TextField";
Enzyme.configure({ adapter: new Adapter() });
describe("Should render <Comment/> component", () => {
it("should check for onChange method (1)", () => {
// const wrapper = shallow(<Comment onChange={}/>)
const onChangeMock = jest.fn();
const component = shallow(
<Comment commentChange={onChangeMock} commentBody={"test"} />
);
component
.find(TextField)
.at(0)
.simulate("change", "test");
expect(onChangeMock).toBeCalledWith("test");
});
it("should check for onChange method (2)", () => {
// const wrapper = shallow(<Comment onChange={}/>)
const onChangeMock = jest.fn();
const component = shallow(
<Comment commentChange={onChangeMock} commentBody={"test"} />
);
component
.find(TextField)
.at(0)
.props()
.onChange();
expect(onChangeMock).toBeCalled();
});
});
For this particular test it will be better if you just use toBeCalled rather than toBeCalledWith. There is no need to test the value it is called with.
Related
I'm trying to test a material ui text field using react-testing-library.
The issue im facing is that in order to test the material ui textField i would have to use this property method
screen.getByLabelText()
which works, however i do not want to display the label on the UI, i want the label to remain hidden, as im already using Material UI <FormLabel>.
I tried using inputProps and passing data-testId on the element, using the getByTestId() method. but i get this error
TestingLibraryElementError: Found multiple elements by:
[data-testid="bio"]
(If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)).
editForm.test.tsx
import "#testing-library/jest-dom";
import React from "react";
import { createMount } from "#material-ui/core/test-utils";
import Button from "#material-ui/core/Button";
import Typography from "#material-ui/core/Typography";
import EditProfileForm from "./editForm";
import { render as testRender, fireEvent, screen, getByText } from "#testing-library/react";
const props = {
handleBio: jest.fn(),
};
describe("<EditProfileForm/>", () => {
let wrapper;
let mount;
beforeEach(() => {
mount = createMount();
wrapper = mount(<EditProfileForm {...props} />);
});
it("should render <EditProfileForm/>", () => {
expect(wrapper).toHaveLength(1);
});
it("calls handleBio on bio TextField change", () => {
const input = screen.getByLabelText("bio");
fireEvent.change(input, { target: { value: "new value" } });
expect(props.handleBio).toHaveBeenCalledTimes(1);
});
});
editForm.tsx
import Button from "#material-ui/core/Button";
import FormGroup from "#material-ui/core/FormGroup";
import FormLabel from "#material-ui/core/FormLabel";
import TextField from "#material-ui/core/TextField";
import Typography from "#material-ui/core/Typography";
import React from "react";
const EditProfileForm = (props: any) => (
<form onSubmit={props.onSubmit}>
<Typography variant="h5">Edit Profile</Typography>
<FormGroup style={{ padding: "30px 0px" }}>
<FormLabel style={{ display: "block" }}>Bio</FormLabel>
<TextField
id="outlined-name"
style={{
width: "100%",
}}
name="bio"
label="bio"
multiline={true}
rows="3"
defaultValue={props.bio}
onChange={props.handleBio}
margin="normal"
variant="outlined"
/>
</FormGroup>
<Button className="subBtn" variant="outlined" color="primary" type="submit">
Submit
</Button>
</form>
);
export default EditProfileForm;
I was able to resolve this issue by first moving the test function after beforeEach been called.
so it will now be
import "#testing-library/jest-dom";
import React from "react";
import { createMount } from "#material-ui/core/test-utils";
import Button from "#material-ui/core/Button";
import Typography from "#material-ui/core/Typography";
import EditProfileForm from "./editForm";
import { render as testRender, fireEvent, screen, getByText } from "#testing-library/react";
const props = {
handleChange: jest.fn(),
onSubmit: jest.fn(),
bio: "test",
gravatar: "https://i.pravatar.cc/150?img=3",
handleBio: jest.fn(),
handleGravatar: jest.fn(),
};
describe("<EditProfileForm/>", () => {
let wrapper;
let mount;
beforeEach(() => {
mount = createMount();
wrapper = mount(<EditProfileForm {...props} />);
});
// must be called first
it("calls handleBio on bio TextField change", () => {
const input = screen.getByTestId("bio");
fireEvent.change(input, { target: { value: "new value" } });
expect(props.handleBio).toHaveBeenCalledTimes(1);
});
it("should render <EditProfileForm/>", () => {
expect(wrapper).toHaveLength(1);
});
it("should check header title ", () => {
expect(wrapper.find(Typography).at(0)).toHaveLength(1);
expect(
wrapper
.find(Typography)
.at(0)
.text(),
).toContain("Edit Profile");
});
it("should test bio prop", () => {
expect(wrapper.props().bio).toContain("test");
});
it("should test gravtar prop", () => {
const link = "https://i.pravatar.cc/150?img=3";
expect(wrapper.props().gravatar).toContain(link);
});
it("should test handleChange props", () => {
const title = "Test";
expect(
wrapper.props().handleChange({
target: {
value: title,
},
}),
);
expect(props.handleChange).toHaveBeenCalled();
});
it("should test onSubmit prop", () => {
// console.log(wrapper.find(TextField).debug());
const submit = jest.fn();
wrapper.simulate("submit", { submit });
expect(props.onSubmit).toBeCalled();
});
it("should test button click", () => {
const button = wrapper.find(Button);
button.simulate("click");
expect(props.onSubmit).toBeCalled();
});
});
And then passing data-testid as an input prop on text field like this
<TextField
id="outlined-name"
className="bio-test"
style={{
width: "100%",
}}
name="bio"
inputProps={{
"data-testid": "bio",
}}
multiline={true}
rows="3"
defaultValue={props.bio}
onChange={props.handleBio}
margin="normal"
variant="outlined"
/>
When I try to simulate a change event with Enzyme in my Material-UI component, value is not changing.
My component looks like this:
export class EditableList extends Component {
...
onChangeNewEntry = event => {
this.setState({newEntry: event.target.value});
};
render() {
const {classes} = this.props;
return (
<div className={classes.div_main}>
<List>
<Paper className={classes.paper_title} elevation={2} key="NewEntry">
<ListItem>
<InputBase
data-testid='input-base'
className={classes.inputBase}
placeholder="New Entry"
value={this.state.newEntry}
onKeyPress={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.onCreateItem();
}
}}
onChange={this.onChangeNewEntry}
multiline
/>
</ListItem>
</Paper>
</List>
</div>
);
}
}
My test looks like this:
import React from 'react';
import {Provider} from "react-redux";
import EditableList from "./EditableList";
import store from "../../../../store";
import {createMount, createShallow} from "#material-ui/core/test-utils";
import {InputBase} from "#material-ui/core";
let mount;
let wrapper;
beforeEach(() => {
mount = createMount();
wrapper = mount(
<Provider store={store}>
<EditableList/>
</Provider>
);
});
afterEach(() => {
mount.cleanUp();
});
const findComponent = ( value) => {
return wrapper.find(`[data-testid='${value}']`).first();
};
it('allow user to write input for new item', () => {
const inputBase = findComponent('input-base');
const value = "New item";
inputBase.simulate('change', {target: {value: value}});
wrapper.update();
console.log(inputBase.prop('value'));
});
In the console, the valueis always an empty string.
I tried also the following without success:
wrapper.find(InputBase).at(0).props().onChange({target: {value: value}});
wrapper.find(InputBase).at(0).props().onChange({target: {name: 'InputBase', value: value}});
I have a <Comment/> component that takes an on onSubmit prop that takes in a parameter like this.
<Comment onSubmit={(e) => this.commentSubmit(e, img.id)}
commentBody={this.state.comment_body }
commentChange={this.handleCommentChange}/>
How would i be able to unit test the onSubmit prop given the parmaters ?
This is my attempt to test the <Comment/> component.
Comment.test.js
describe('Should render <Comment/> component', () => {
let wrapper;
beforeEach( () => {
wrapper = shallow(<Comment/>)
})
it('should test onSubmit', () => {
const onSubmit = jest.fn();
const mockEvent = {
onSubmit,
target:{
commentBody:"test",
id:23
}
};
const component = shallow(
<Comment onSubmit={mockEvent}/>
)
wrapper.find('form').simulate('submit');
expect(component).toBeCalled(1)
})
}
However i get this error
Matcher error: this matcher must not have an expected argument
The Comment component is within another component.
ImageContainer.js
class ImageContainer extends React.Component{
state = {
isComment: false,
comment_body: ""
}
commentSubmit = (event, id) => {
event.preventDefault();
console.log(this.state.comment_body); // doesn't get console.log
// note that commentBody is being used for the req.body as well so its called by req.body.commentBody
const commentBody = this.state.comment_body
const data = {
commentBody,
id
}
this.props.postComment(data);
this.setState({
comment_body: ''
})
}
render(){
const { img, deleteImg } = this.props
return(
<Comment onSubmit={(e) => this.commentSubmit(e, img.id)}
commentBody={this.state.comment_body }
commentChange={this.handleCommentChange}/>
)
}
const mapDispatchToProps = (dispatch) => ({
postComment: (data) => dispatch(postComment(data))
})
export default connect(mapStateToProps, mapDispatchToProps)(ImageContainer)
Comment component
import React from "react";
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
const Comment = (props) => (
<div>
<form onSubmit={props.onSubmit}>
<TextField
type="text"
id="outlined-multiline-static"
label="Write A Comment"
multiline
name="comment_body"
value={props.commentBody}
rows="10"
fullWidth
margin="normal"
variant="outlined"
onChange={props.commentChange}
/>
{/* <Button type="submit" variant="outlined" component="span" color="primary">
Post A Comment
</Button> */}
<button type="submit" variant="outlined" component="span" color="primary">
Write a Comment
</button>
</form>
</div>
)
export default Comment;
I made a fix, and the test passes. If this could be improved please make the changes. I'm still learning enzyme/jest unit testing.
it('should test onSubmit', () => {
const mockSubmit = jest.fn();
const component = shallow(
<Comment commentBody={'owl'} onSubmit={mockSubmit}/>
);
const props = {
id:2,
comment_body:'test'
}
console.log(component.debug())
component.find('form').simulate('submit', props);
expect(mockSubmit).toBeCalledWith({'comment_body': "test", "id": 2});
})
As I can remember you only need to pass your mock on onSubmit and your custom event on simulate, try something like this:
describe('Should render <Comment/> component', () => {
let wrapper;
beforeEach( () => {
wrapper = shallow(<Comment/>)
})
it('should test onSubmit', () => {
const onSubmit = jest.fn();
const mockEvent = {
// onSubmit, <-- Maybe not required
target:{
commentBody:"test",
id:23
}
};
const component = shallow(
<Comment onSubmit={onSubmit}/> // <-- Setup mock here
)
wrapper.find('form').simulate('submit', mockEvent); // <--- Your event here
expect(component).toBeCalled(1)
// expect(component).toHaveBeenCalledWith(...) <-- Assert right data is being called.
})
}
I am looking to fire a submit handler for a LoginForm. However, for some reason, instead of my mock function being called, the actual handler for the component gets fired (calling an external api). How can I ensure that my mock handler gets called instead?
The three components of interest are below (The presentational, container and the test suite)
LoginForm.js
import { Formik, Form, Field } from 'formik';
import { CustomInput } from '..';
const LoginForm = ({ initialValues, handleSubmit, validate }) => {
return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={handleSubmit}
>
{({ isSubmitting, handleSubmit }) => {
return (
<Form onSubmit={handleSubmit}>
<div className="d-flex flex-column justify-content-center align-items-center">
<Field
data-testid="usernameOrEmail"
type="text"
name="identifier"
placeholder="Username/Email"
component={CustomInput}
inputClass="mb-4 mt-2 text-monospace"
/>
<Field
data-testid="login-password"
type="password"
name="password"
placeholder="Password"
component={CustomInput}
inputClass="mb-4 mt-4 text-monospace"
/>
<button
data-testid="login-button"
className="btn btn-primary btn-lg mt-3 text-monospace"
type="submit"
disabled={isSubmitting}
style={{ textTransform: 'uppercase', minWidth: '12rem' }}
>
Submit
</button>
</div>
</Form>
)}}
</Formik>
);
};
export default LoginForm;
LoginPage.js
import React, { useContext } from 'react';
import { loginUser } from '../../services';
import { userContext } from '../../contexts';
import { loginValidator } from '../../helpers';
import { setAuthorizationToken, renderAlert } from '../../utils';
import LoginForm from './login-form';
const INITIAL_VALUES = { identifier: '', password: '' };
const LoginPage = props => {
const { handleUserData, handleAuthStatus } = useContext(userContext);
const handleSubmit = async (values, { setSubmitting }) => {
try {
const result = await loginUser(values);
handleAuthStatus(true);
handleUserData(result.data);
setAuthorizationToken(result.data.token);
props.history.push('/habits');
renderAlert('success', 'Login Successful');
} catch (err) {
renderAlert('error', err.message);
}
setSubmitting(false);
};
return (
<LoginForm
initialValues={INITIAL_VALUES}
validate={values => loginValidator(values)}
handleSubmit={handleSubmit}
/>
);
};
export default LoginPage;
LoginPage.spec.js
import React from 'react';
import { cleanup, getByTestId, fireEvent, wait } from 'react-testing-library';
import { renderWithRouter } from '../../../helpers';
import LoginPage from '../login-page';
afterEach(cleanup);
const handleSubmit = jest.fn();
test('<LoginPage /> renders with blank fields', () => {
const { container } = renderWithRouter(<LoginPage />);
const usernameOrEmailNode = getByTestId(container, 'usernameOrEmail');
const passwordNode = getByTestId(container, 'login-password');
const submitButtonNode = getByTestId(container, 'login-button');
expect(usernameOrEmailNode.tagName).toBe('INPUT');
expect(passwordNode.tagName).toBe('INPUT');
expect(submitButtonNode.tagName).toBe('BUTTON');
expect(usernameOrEmailNode.getAttribute('value')).toBe('');
expect(passwordNode.getAttribute('value')).toBe('');
});
test('Clicking the submit button after entering values', async () => {
const { container } = renderWithRouter(<LoginPage handleSubmit={handleSubmit} />);
const usernameOrEmailNode = getByTestId(container, 'usernameOrEmail');
const passwordNode = getByTestId(container, 'login-password');
const submitButtonNode = getByTestId(container, 'login-button');
fireEvent.change(usernameOrEmailNode, { target: { value: fakeUser.username }});
fireEvent.change(passwordNode, { target: { value: fakeUser.password }});
fireEvent.click(submitButtonNode);
await wait(() => {
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
expect(usernameOrEmailNode.tagName).toBe('INPUT');
expect(passwordNode.tagName).toBe('INPUT');
expect(submitButtonNode.tagName).toBe('BUTTON');
expect(usernameOrEmailNode.getAttribute('value')).toBe('');
expect(passwordNode.getAttribute('value')).toBe('');
});```
To answer your question, you will need to first make the handleSubmit constant accessible outside LoginPage.js so that it may be mocked and then tested. For example,
LoginPage.js
export const handleSubmit = async (values, { setSubmitting }) => {
... code to handle submission
})
And in your tests - LoginPage.spec.js
jest.unmock('./login-page');
import LoginPage, otherFunctions from '../login-page'
otherFunctions.handleSubmit = jest.fn();
...
test('Clicking the submit button after entering values', () => {
...
fireEvent.click(submitButtonNode);
expect(handleSubmit).toHaveBeenCalledTimes(1);
})
I hope the above fixes your problem.
But, going by the philosophy of unit testing, the above components
must not be tested the way you are doing it. Instead your test setup
should be like this -
Add a new test file called LoginForm.spec.js that tests your LoginForm component. You would test the following in this -
Check if all input fields have been rendered.
Check if the correct handler is called on submit and with the correct parameters.
The existing test file called LoginPage.spec.js would then only test if the particular form was rendered and then you could also test
what the handleSubmit method does individually.
I believe the above would make your tests more clearer and readable
too, because of the separation of concerns and would also allow you to
test more edge cases.
Consider the following component:
import React, { Component } from "react";
import ImageUpload from "./ImageUpload/ImageUpload";
import axios from "axios";
class Dashboard extends Component {
state = {
fileName: ""
};
onFormSubmit = e => {
e.preventDefault();
const { group, country } = e.target;
axios.post("/api/dashboard", {
name: group.value,
country: country.value,
image: this.state.fileName
});
//TODO redirect the user
};
onFileNameChange = name => {
this.setState({ fileName: name });
};
render() {
return (
<div>
<form onSubmit={this.onFormSubmit}>
<input type="text" name="group" placeholder="Group Name" />
<input type="text" name="country" placeholder="Country" />
<ImageUpload
fileName={this.state.fileName}
onFileNameChange={this.onFileNameChange}
/>
<button type="submit">Add Group</button>
</form>
</div>
);
}
}
export default Dashboard;
I am trying to fake the submission of a form and test that when onFormSubmit is being called. I make onFormSubmit a spy function. But, it is not being called at all.
import React from "react";
import Dashboard from "components/admin/Dashboard/Dashboard";
import { shallow, render } from "enzyme";
describe("The Dashboard component", () => {
it("should not regress", () => {
const wrapper = render(<Dashboard />);
expect(wrapper).toMatchSnapshot();
});
it("should submit the form and send the group to server", () => {
const wrapper = shallow(<Dashboard />);
const preventDefault = jest.fn();
const event = {
preventDefault,
target: {
group: "Samrat",
country: "Nepal"
}
};
wrapper.instance().onFormSubmit = jest.fn();
wrapper.update();
wrapper.find("form").simulate("submit", event);
expect(wrapper.instance().onFormSubmit).toHaveBeenCalledWith(event);
});
});