I have a component which uses the react-datepicker package.
I am writing to write a unit test which will edits the dates and thereafter run some logic. However, i am unable to detect the field which for me to change using userEvent.type(). I have tried to use getByText, getByRole, getAllByText.
Form.tsx
import React, { useState } from 'react';
import DatePicker from "react-datepicker";
import { Form } from 'react-bootstrap';
import "react-datepicker/dist/react-datepicker.css";
const Form = () => {
const [data, setData] = useState({ date1: new Date(), date2: new Date() })
return (
<div>
<Form>
...some other fields
<Form.Group controlId="date1">
<Form.Label>Date1</Form.Label>
<DatePicker name='date1'selected={data.date1} onChange={(date: Date) => setData({...data, date1: date})}
</Form.Group>
<Form.Group controlId="date2">
<Form.Label>Date2</Form.Label>
<DatePicker name='date2' selected={data.date2} onChange={(date: Date) => setData({...data, date2: date})}
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
</div>
)
}
export default Form
Form.test.tsx
import React from 'react';
import Form from './Form';
import {render} from '#testing-library/react';
import userEvent from '#testing-library/user-event';
describe('Form Component', () => {
it('able to change the date', () => {
const { getByRole } = render(<Form/>)
const date1Field = getByRole('textbox', { name: /date1/i })
act(() => userEvent.type(date1Field, '01/01/1990'))
... any other action to submit the form
})
})
However, my terminal showed me, which is the same for both date, which it was unable to detect the input field:
TestingLibraryElementError: Unable to find an accessible element with the role"textbox" and name "/date1/i"
textbox:
Name=""
<input
class=''
name='date1'
type='text'
value='05/23/2021'
/>
Name=""
<input
class=''
name='date2'
type='text'
value='05/23/2021'
/>
There is an issue with the library itself and not being able to pass in any ARIA props into the datepicker in react-datepicker.
With using the other library you mentioned react-day-picker it is possible to pass props into the input and set aria labels.
import DayPickerInput from 'react-day-picker/DayPickerInput';
<DayPickerInput inputProps={{'aria-label':'Date input 2'}} />
Sandbox: https://codesandbox.io/s/react-day-picker-examplesinput-forked-lj8pp
For anyone who is looking for a solution which I have adopted Jonathan S. answer,
Form.tsx
import React, { useState } from 'react';
import DayPickerInput from "react-datepicker/DayPickerInput";
import { Form } from 'react-bootstrap';
import "react-day-picker/lib/style.css";
const Form = () => {
const [data, setData] = useState({ date1: new Date(), date2: new Date() })
return (
<div>
<Form>
...some other fields
<Form.Group controlId="date1">
<Form.Label>Date1</Form.Label>
<DayPickerInput inputProps={{ 'aria-label': 'date1' }} value={data.date1} onChange={(date: Date) => setData({...data, date1: date})}/>
</Form.Group>
<Form.Group controlId="date2">
<Form.Label>Date2</Form.Label>
<DayPickerInput inputProps={{ 'aria-label': 'date2' }} value={data.date2} onChange={(date: Date) => setData({...data, date2: date})}/>
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
</div>
)
}
export default Form
Form.test.tsx
import React from 'react';
import Form from './Form';
import {render} from '#testing-library/react';
import userEvent from '#testing-library/user-event';
describe('Form Component', () => {
it('able to change the date', () => {
const { getByLabelText } = render(<Form/>)
let date1Field = getByLabelText('date1') as HTMLInputElement
// Selects your default value of the date field
date1Field.setSelectRange(0, date1Field.value.length)
// Replaces it
userEvent.type(date1Field, '1990-01-01')
... any other action to submit the form
})
})
It might not be needed but in case, you can pass a prop placeholderText='some text' and then get the input using screen.getByPlaceholderText('some text');
try this solution, this perfectly works for me
const startDate = await container.find('.ant-picker-input input').first();
await startDate.simulate('mousedown');
await waitFor(async () => {
await container.update();
const dateChart = await container.find('.ant-picker-today-btn');
await dateChart.simulate('click');
});
After Trying many solution, and trying and error.
I found a solution that work perfectly fine for me.
describe('Date Picker Test', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<Provider store={store}>
<Router>
<DatePikerFunction />
</Router>
</Provider>,
);
});
it('Date Picker Change', async () => {
const datePicker = await wrapper.find('.ant-picker-input input').first();
await datePicker.simulate('mousedown');
await waitFor(async () => {
await wrapper.update();
const today = await wrapper.find('.ant-picker-cell-today'); // put classname that is used to select the perticular date
const next = today.nextElementSibling;
await next.click();
});
});
});
Here I have find the today's date and then selected tomorrow date.
You can find base on class or any thing you want and use it.
Hope It will work for you too.
Thanks
Related
I use React JS and Strapi. I am trying to send comment to my API. I worked but without one essential thing : the post for which I am writing a comment. How can I possibly add the id of my post so that the relation is made between my comment and my post ?
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import TextField from '#material-ui/core/TextField';
import { Button } from '#material-ui/core'
import CommentsAPI from '../../Services/CommentsAPI'
export default function CommentForm() {
const [comment, setComment] = useState({})
const {id} = useParams()
const handleSubmit = async (event) => {
event.preventDefault();
try {
CommentsAPI.create(JSON.parse(`{"data":${JSON.stringify(comment)}}`))
} catch (error) {
console.log(error)
}
}
const handleChange = (event) => {
const {name, value} = event.currentTarget
setComment({
...comment,
[name]: value
})
}
return (
<form onSubmit={handleSubmit}>
<div>
<TextField
id="pseudo"
label="Pseudo"
type="text"
onChange={handleChange}
name="pseudo"
/>
</div>
<div>
<TextField
id="comment"
label="Comment"
multiline
minRows={2}
onChange={handleChange}
name="content"
/>
</div>
<div>
<Button variant="contained" color="primary" type="submit">
Send
</Button>
</div>
</form>
)
}
import { URL_COMMENTS } from '../config'
import axios from 'axios'
function create(id_post, comment) {
return axios.post(URL_COMMENTS, id_post, comment)
}
const CommentsAPI = {
create
}
export default CommentsAPI
Thank you for your help.
i've got a problem. I'm trying to test material ui's datePicker (https://mui.com/api/date-picker/) with Jest and Enzyme. I've searched alot but couldnt find anything that would help me... I hope that you will guide me.
Here's what i got:
DatePickerFilter.tsx
import React, {useState} from 'react';
import AdapterDateFns from '#mui/lab/AdapterDateFns';
import LocalizationProvider from '#mui/lab/LocalizationProvider';
import DatePicker from '#mui/lab/DatePicker';
import TextField from '#mui/material/TextField';
import styled from '#emotion/styled';
export const StyledDatePicker = styled(DatePicker)`
width: 320px !important;
`;
const DatePickerFilter = () => {
const [date, setDate] = useState('10/10/2021')
const handleChange = (newValue: any) => {
setDate(newValue)
};
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<StyledDatePicker
label="Date"
views={['day']}
value={date}
onChange={handleChange}
renderInput={(params) => <TextField {...params} />}
/>
</LocalizationProvider>
);
};
MyBookings.test.tsx
export default DatePickerFilter;
import React from 'react';
import { mount } from 'enzyme';
import { Render } from '../../../utilities/TestsUtils';
import DatePickerFilter from '../../common/DatePickerFilter';
describe('MyBookings > DatePickerFilter', () => {
it('should not change date if its before today', () => {
const wrapper = mount(
<Render>
<DatePickerFilter />
</Render>
);
wrapper.find('input').simulate('change', {target: {value: '11/10/2021'}});
console.log(wrapper.find('input').debug())
wrapper.unmount();
});
});
And here's a function util that gives me access to redux's store
export const store = configureStore({
reducer: {
booking: bookingReducer,
myBookings: myBookingsSlice,
officeSpaces: officeSpacesSlice,
filters: filtersSlice
},
});
export const Render = ({ children }: any) => {
return (
<Provider store={store}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</Provider>
);
};
I'm trying to change input's value by simulate 'change' event, but it doesnt want to change.
Here's log of wrapper.find('input').debug()
<input aria-invalid={false} aria-describedby={[undefined]} autoComplete={[undefined]}
autoFocus={false} defaultValue={[undefined]} disabled={[undefined]} id={[undefined]}
onAnimationStart={[Function: handleAutoFill]} name={[undefined]} placeholder={[undefined]}
readOnly={true} required={false} rows={[undefined]} value="10/10/2021"
onKeyDown={[Function (anonymous)]} onKeyUp={[undefined]} type="text" aria-readonly={true}
aria-label="Choose date, selected date is Oct 10, 2021" onClick={[Function: openPicker]}
className="MuiOutlinedInput-input MuiInputBase-input css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input"
onBlur={[Function: handleBlur]} onChange={[Function: handleChange]} onFocus={[Function: handleFocus]} />
The answer to this problem is mentioned here https://github.com/mui/material-ui/issues/27038.
If you don't need the Mobile variants, you can do what I did and import the DesktopDatePicker. This way I don't reproduce the problem with triggerin a change on an input.
component.tsx
import React, { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { Form, FormControl, FormGroup, FormLabel } from "react-bootstrap";
import Button from "react-bootstrap/Button";
import Card from "react-bootstrap/Card";
import { useHistory } from "react-router-dom";
import { StorageKeys } from "../ProtectedRoute";
import "./styles.scss";
const Login = () => {
const history = useHistory();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSetEmail = (event: ChangeEvent<HTMLInputElement>) =>
setEmail(event.target.value);
const handleSetPassword = (event: ChangeEvent<HTMLInputElement>) =>
setPassword(event.target.value);
const handleSubmit = (event: FormEvent<HTMLElement>) => {
event.preventDefault();
console.log("email::", email);
console.log("password::", password);
localStorage.setItem(StorageKeys.TOKEN, "TODO: Auth");
history.push("/");
};
useEffect(() => {
localStorage.removeItem(StorageKeys.TOKEN);
}, []);
return (
<div id="login">
<Card id="loginCard">
<Card.Header>Login</Card.Header>
<Card.Body>
<Form onSubmit={handleSubmit}>
<FormGroup>
<FormLabel>Email address</FormLabel>
<FormControl type="email" id="email" placeholder="Enter email"
value={email} onChange={handleSetEmail}
required={true} />
</FormGroup>
<FormGroup>
<FormLabel>Password</FormLabel>
<FormControl type="password" id="password" placeholder="Password"
value={password} onChange={handleSetPassword}
required={true} />
</FormGroup>
<div className={"button-container"}>
<Button id="submit" variant="primary" type="submit">
Submit
</Button>
</div>
</Form>
</Card.Body>
</Card>
</div>
);
};
export default Login;
This works when using shallow to render the component:
login.test.tsx
import { mount, shallow } from "enzyme";
import React from "react";
import Login from "./index";
describe("Login Component", () => {
test("can properly submit form", () => {
jest.spyOn(window.localStorage.__proto__, "removeItem");
const wrapper = shallow(<Login />);
// This works just find, finds only the one #email input.
const emailInput = wrapper.find("#email");
emailInput.simulate("change", { target: { value: testLoginData.email } });
});
});
When using mount this throws the error:
Error: Method “simulate” is meant to be run on 1 node. 2 found instead
import { mount, shallow } from "enzyme";
import React from "react";
import Login from "./index";
describe("Login Component", () => {
test("can properly submit form", () => {
jest.spyOn(window.localStorage.__proto__, "removeItem");
const wrapper = mount(<Login />);
const emailInput = wrapper.find("#email");
// This will now complain about there being too many nodes.
emailInput.simulate("change", { target: { value: testLoginData.email } });
});
});
What gives? I need to use mount for the test that I'm working on, why is it finding multiple elements when there is for sure ONLY ONE.
I can patch it to work using the following, but I shouldn't have to... right?!
emailInput.at(0).simulate("change", { target: { value: testLoginData.email } });
So it's because your <FormControl is the first with this id and <input is second(or vice versa).
There are wide list of approaches:
.at(0) will work, but this way you will never know if you(because of error in the code) renders multiple elements. It might happen if conditions in conditional rendering {someFlag && <.... that suppose to be mutually exclusive are not. So really, it's a bad way.
Mock FormControl to be final element - so <input will not be returned anymore by .find()(honestly never used that, just assume it will work - but still looks messy and need additional boilerplate code for each test file, so not really handful way):
jest.mock('../FormControl.jsx', () => null);
use hostNodes() to filter only native elements(like <span> to be returned):
const emailInput = wrapper.find("#email").hostNodes();
I vote for 3rd option as most reliable and still safe for catching code logic's errors.
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.