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.
})
}
Related
I have this simplified structure:
<Page>
<Modal>
<Form />
</Modal>
</Page>
All of these are functional components.
And in <Modal /> I have a close function that looks like this:
const close = () => {
// apply a CSS class - so the modal disappears animatedly ..
// then use setTimeout() to completely remove the modal after the animation ends ..
}
Do you have an idea how the <Page /> component can call the <Modal /> close method? And the page has to do it because this is where I'm doing the call to API with the data from the form, and so if all is OK with API request - close the modal.
(The <Form /> handles only the form validation but then passes the data to <Page /> where all the business logic happens.)
PS: The project uses Typescript ... so I have to deal with types as well :(
I look into your problem. I think my example should clarify your problem. Let me know if you have any questions.
import { ReactNode, useCallback, useEffect, useState } from 'react'
import { render } from 'react-dom'
function App() {
return (
<div>
<Page />
</div>
)
}
function Page() {
const [isModalOpen, setModalOpen] = useState(false)
const handleFormSubmit = useCallback((formValues: FormValues) => {
console.log({ formValues })
setModalOpen(false)
}, [])
return (
<div>
<button onClick={() => setModalOpen(!isModalOpen)}>Toggle modal</button>
<Modal isOpen={isModalOpen}>
<Form onSubmit={handleFormSubmit} />
</Modal>
</div>
)
}
interface ModalProps {
isOpen: boolean
children: ReactNode
}
function Modal(props: ModalProps) {
const [isOpen, setIsOpen] = useState(false)
const close = () => {
setIsOpen(false)
}
const open = () => {
setIsOpen(true)
}
useEffect(() => {
if (!props.isOpen && isOpen) close()
if (props.isOpen && !isOpen) open()
}, [props.isOpen])
if (!isOpen) return null
return <div className="modal">{props.children}</div>
}
interface FormProps {
onSubmit: (formValues: FormValues) => void
}
interface FormValues {
username: string
password: string
}
function Form(props: FormProps) {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
return (
<form
onSubmit={e => {
e.preventDefault()
props.onSubmit({
username,
password
})
}}
>
<input
type="text"
placeholder="username"
onChange={e => {
setUsername(e.target.value)
}}
/>
<input
type="text"
placeholder="password"
onChange={e => {
setPassword(e.target.value)
}}
/>
<button type="submit">Submit</button>
</form>
)
}
render(<App />, document.getElementById('root'))
I assumed you are fresh in FE or React world. Propably you do not need that much-nested structure.
There is a special hook in React called useImperativeHandle. You can use it to call child's functions from parent.
You can find out more in the oficcial React documentation.
example of usage
Child Component
Needs to be wrapped into forwardRef like that:
export const ChildComponent = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
async functionName() {
await someLogic();
},
}));
Parent Component
In parent component you need to pass ref to the child.
Then you can use child's function this way:
const childRef = useRef(null)
childRef.current.functionName()
I need to fix a memory leak in my app but Im not sure how to. I have a component that uses a modal and I get the error when I am adding an item. The modal is reusable and I use it in other components as well. This is the main component:
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Card, Select, Form, Button } from 'antd';
import Table from 'components/Table';
import Modal from '../Modal';
import styles from '../index.module.scss';
const { Item } = Form;
const { Option } = Select;
const PersonForm = ({ details, form }) => {
const [modalVisible, setModalVisible] = useState(false);
const [name, setName] = useState(
details?.name ? [...details?.name] : []
);
useEffect(() => {
form.setFieldsValue({
name: name || [],
});
}, [form, details, name]);
const addName = values => {
setName([...name, values]);
setModalVisible(false);
};
const removeName = obj => {
setName([...name.filter(i => i !== obj)]);
};
const cancelModal = () => {
setModalVisible(false);
};
return (
<div>
<Card
title="Names
extra={
<Button type="solid" onClick={() => setModalVisible(true)}>
Add Name
</Button>
}
>
<Table
tableData={name}
dataIndex="name"
removeName={removeName}
/>
</Card>
<Item name="name">
<Modal
title="Add Name"
fieldName="name"
onSubmit={addName}
visible={modalVisible}
closeModal={cancelModal}
/>
</Item>
</div>
);
};
PersonForm.propTypes = {
details: PropTypes.instanceOf(Object),
form: PropTypes.instanceOf(Object),
};
PersonForm.defaultProps = {
form: null,
details: {},
};
export default PersonForm;
And this is the modal component:
import React from 'react';
import PropTypes from 'prop-types';
import { Input, Form } from 'antd';
import Modal from 'components/Modal';
import LocaleItem from 'components/LocaleItem';
const { Item } = Form;
const FormModal = ({ visible, closeModal, onSubmit, fieldName, title }) => {
const [form] = Form.useForm();
const layout = {
labelCol: { span: 8 },
wrapperCol: { span: 15 },
};
const addItem = () => {
form
.validateFields()
.then(values => {
onSubmit(values, fieldName);
form.resetFields();
closeModal(fieldName);
})
.catch(() => {});
};
const canceledModal = () => {
form.resetFields();
closeModal(fieldName);
};
return (
<Modal
onSuccess={addItem}
onCancel={canceledModal}
visible={visible}
title={title}
content={
<Form {...layout} form={form}>
<Item
name="dupleName"
label="Name:"
rules={[
{
required: true,
message: 'Name field cannot be empty',
},
]}
>
<Input placeholder="Enter a name" />
</Item>
</Form>
}
/>
);
};
FormModal.propTypes = {
visible: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
FormModal.defaultProps = {};
export default FormModal;
I get a memory leak when I am in the test file when adding items in the modal. Can someone point out why this is happening and how to fix this? Thanks
Remove closeModal and form.resetFields from addItem function.
const addItem = () => {
form
.validateFields()
.then(values => {
onSubmit(values, fieldName); // when this onSubmit resolves it closes the modal, therefor these two lines below will be executed when component is unmounted, causing the memory leak warning
form.resetFields();
closeModal(fieldName);
})
.catch(() => {});
};
// instead maybe just:
const [form] = Form.useForm();
<Modal onOk={form.submit}>
<Form form={form}>
<Form.Item name="foo" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
</Modal>
Also, as far as I know you don't need to call form.validateFields as Ant Design's Form would do that automatically if rules are set in the Form.Item's.
I'm using react and trying to figure out how to trigger useEffect (on parent) from a child component.
I have a child component that contains a modal with input fields that are passed to my DB.
import React, { useState } from 'react';
import { Modal, Button, Form } from 'react-bootstrap';
const AddContact = () => {
// Modal Setup
const [ showModal, setShowModal ] = useState(false);
const handleClose = () => setShowModal(false);
const handleShow = () => setShowModal(true);
// Using state to hold the values for each field in the form
const [first, setFirst] = useState('');
const [last, setLast] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // prevents form default action
const addContact = async () => {
const result = await fetch('/api/crm/add', {
method: 'POST',
body: JSON.stringify({ first_name: first, last_name: last }),
headers: {
'Content-Type': 'application/json',
}
});
const body = await result.json();
}
addContact();
handleClose();
};
return (
<>
<Button onClick={handleShow} variant="primary" size="sm">Add Contact</Button>
<Modal show={showModal} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Add Contact</Modal.Title>
</Modal.Header>
<Modal.Body>
<label for="formfirst">First Name: </label><br />
<input
id="formfirst"
name="formfirst"
type="text"
value={first}
onChange={e => setFirst(e.target.value)}
/>
<br/>
<label for="formlast">Last Name: </label><br />
<input
id="last"
name="last"
type="text"
value={last}
onChange={e => setLast(e.target.value)}
/> <br/>
<Form.Group>
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>Cancel</Button>
<Button variant="primary" onClick={handleSubmit}>Submit</Button>
</Modal.Footer>
</Modal>
</>
);
}
export default AddContact;
The parent component has a table of data which I would like to refresh so that it shows the current data:
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { Table } from 'react-bootstrap';
import AddContactForm from '../components/AddContactForm';
// React-Table
import {
useTable,
} from 'react-table'
const ContactsPage = () => {
const [ tableData, setTableData ] = useState([]);
useEffect( () => {
const fetchData = async () => {
const result = await fetch(`api/crm/get`);
const body = await result.json();
setTableData(body);
}
fetchData();
},[]);
const columns = React.useMemo(
() => [
{
Header: 'First Name',
accessor: 'first_name',
},
{
Header: 'Last Name',
accessor: 'last_name',
}
... SIMPLIFIED FOR CONCISENESS
],
[]
)
function ReactTable({ columns, data }) {
const {
getTableProps,
... REMOVED SOME REACT-TABLE FOR CONCISENESS
} = useTable({
columns,
data,
})
return (
<>
<h2>Contacts</h2>
<hr></hr>
<div>
<AddContactForm />
</div>
<Table striped bordered hover size="sm" {...getTableProps()}>
... REMOVED TABLE TO CONCISENESS
</Table>
</>
);
}
const data = React.useMemo(() => tableData)
return (
<Styles>
<ReactTable columns={columns} data={data} />
</Styles>
)
}
export default ContactsPage;
How can I achieve this? I tried making my useEffect hook into a function which I could pass to my child component, but I got an error saying that that new function was not a function???
Not sure it makes sense to pass useEffect as props, instead pass the function used inside useEffect as props. Here
useEffect( () => {
const fetchData = async () => {
const result = await fetch(`api/crm/get`);
const body = await result.json();
setTableData(body);
}
fetchData();
},[]);
Refactor it like this;
// Wrapped in useCallback otherwise it would be recreated each time
// this component rerenders, hence triggering useEffect below
let fetchData = React.useCallback(async () => {
const result = await fetch(`api/crm/get`);
const body = await result.json();
setTableData(body);
},[])
useEffect(() => {
fetchData();
},[fetchData]);
and pass fetchData as props to the modal which you can invoke after submit.
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.
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.