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"
/>
Related
I have a simple form with just one text field. I have used react material ui library along with formik for validations and verification. I'm unable to get the textfield within the form by using shallow method of enzyme. Not sure what I'm doing wrong.
declarationForm.js
import React, { useState, useEffect } from 'react';
import * as Yup from 'yup';
import { Formik } from 'formik';
import { useNavigate, Prompt } from "react-router-dom";
import { Box, Button, Card, CardContent, CardHeader, Divider, Grid, TextField } from '#material-ui/core';
import { get_declarations_details, put_declarations_details } from './../../utils/service';
const DeclarationsForm = () => {
const [ initialData, setInitialData ] = useState({ declarations:'' });
useEffect(()=>{
get_data();
},[]);
let navigate = useNavigate();
const get_data = async() =>{
try {
const result = await get_declarations_details();
if(result){
setInitialData({
...initialData,
...result,
});
}
} catch (error) {
console.log(error);
}
}
const put_data = async(data) =>{
try {
const result = await put_declarations_details(data);
if(result){
setInitialData({
...initialData,
...result,
});
}
} catch (error) {
console.log(error);
}
}
return (
<Formik
initialValues={initialData}
enableReinitialize={true}
validationSchema={
Yup.object().shape({
declarations: Yup.string().max(255).required('Declarations is required'),
})
}
onSubmit={(values) => {
if(JSON.stringify(initialData) !== JSON.stringify(values)){
put_data(values);
}
navigate(`/summary`, { replace: true })
}}
>
{({
errors,
handleBlur,
handleChange,
handleSubmit,
isSubmitting,
touched,
isValid,
values
}) => (
<form onSubmit={handleSubmit}>
<Card>
<Prompt
when={!isValid}
message="Are you sure you want to proceed without saving?"
/>
<CardHeader
subheader="The information can be edited"
title="Declarations"
/>
<Divider />
<CardContent>
<Grid
container
spacing={3}
>
<Grid
item
md={6}
xs={12}
>
<TextField
fullWidth
error={Boolean(touched.declarations && errors.declarations)}
helperText={touched.declarations && errors.declarations}
label="Declarations"
name="declarations"
handleBlur={handleBlur}
onChange={handleChange}
required
value={values.declarations}
variant="outlined"
/>
</Grid>
</Grid>
</CardContent>
<Divider />
<Box
display="flex"
justifyContent="flex-end"
p={2}
>
<Button
color="primary"
variant="contained"
disabled={isSubmitting}
onClick={handleSubmit}
>
Continue
</Button>
</Box>
</Card>
</form>
)}
</Formik>
);
};
export default DeclarationsForm;
My test file is as below
declarationForm.test.js
import { createShallow } from '#material-ui/core/test-utils';
import React from 'react';
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import DeclarationsForm from './declarationsForm';
import { ThemeProvider as MuiThemeProvider } from '#material-ui/core/styles';
import { TextField } from '#material-ui/core';
configure({ adapter: new Adapter() });
describe('<DeclarationsForm />', () => {
let shallow, wrapper;
beforeAll(() => { // This is Mocha; in Jest, use beforeAll
shallow = createShallow();
});
beforeEach(() => {
wrapper = shallow(<MuiThemeProvider><DeclarationsForm /></MuiThemeProvider>);
});
it('should work', () => {
console.log(wrapper.find(TextField).debug());
expect(wrapper.find(TextField)).toHaveLength(1);
});
});
When I run the test below is the result
FAIL src/views/declarations/declarationsForm.test.js
● Console
console.error node_modules/prop-types/checkPropTypes.js:20
Warning: Failed prop type: The prop `theme` is marked as required in `ThemeProvider`, but its value is `undefined`.
in ThemeProvider (at declarationsForm.test.js:19)
console.log src/views/declarations/declarationsForm.test.js:23
● <DeclarationsForm /> › should work
expect(received).toHaveLength(expected)
Expected length: 1
Received length: 0
Received object: {}
22 |
23 | console.log(wrapper.find(TextField).debug());
> 24 | expect(wrapper.find(TextField)).toHaveLength(1);
| ^
25 | });
26 | });
at Object.<anonymous> (src/views/declarations/declarationsForm.test.js:24:37)
wrapper is unable to find text field or form.
How to debug this? What is the right approach to write test when we have also used useNavigate method within the component?
Just like Drew's above, you need to pass in a theme prop to resolve the Failed prop type error.
shallow(<MuiThemeProvider theme={{}}><DeclarationsForm /></MuiThemeProvider>);
Also, wrapper cannot find TextField cos you're using shallow. Use dive() to expand the nested form
console.log(wrapper.find(Formik).dive().find(TextField).debug());
Im a bit new to the react ecosytem. Im having a weird behaviour with passing tests.
Im using CRA, prop-types and react-test-library.
Here is my component:
import React from 'react';
import PropTypes from 'prop-types';
export default function Navbar({
Logo, MenuItems, className,
}) {
return (
<nav aria-label="navigation bar" className={className}>
{Logo}
<div>
{ MenuItems.map((MenuItem) => MenuItem) }
</div>
</nav>
);
}
Navbar.defaultProps = {
className: '',
};
Navbar.propTypes = {
className: PropTypes.string,
Logo: PropTypes.node.isRequired,
MenuItems: PropTypes.arrayOf(PropTypes.node).isRequired,
};
I want to test that prop-types complains when params are not receiving the right type.
import React from 'react';
import { render } from '#testing-library/react';
import Navbar from './Navbar';
describe('<Navbar />', () => {
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});
beforeEach(() => {
console.error.mockClear();
});
afterAll(() => {
console.error.mockRestore();
});
it('renders', () => {
render(<Navbar
Logo={<p data-test="logo">My logo</p>}
MenuItems={[
<p key="spanish">Spanish</p>,
<p key="english">english</p>,
]}
/>);
expect(console.error).not.toHaveBeenCalled();
});
it('errors to console when Logo is missing', () => {
render(<Navbar MenuItems={[
<p key="spanish">Spanish</p>,
<p key="english">English</p>,
]}
/>);
expect(console.error).toHaveBeenCalled();
});
it('does not error to console when Logo is missing', () => {
render(<Navbar MenuItems={[
<p key="spanish">Spanish</p>,
<p key="english">English</p>,
]}
/>);
expect(console.error).toHaveBeenCalled();
});
});
My thinking is that Im not resetting properly the mocks, they have some state that it is not clear or something similar.
What am i missing?
PropTypes.checkPropTypes(...) only console.errors a given message once. To reset the error warning cache in tests, call PropTypes.resetWarningCache()
Source
Try invoke resetWarningCache in your beforeEach hooks
import PropTypes from 'prop-types';
beforeEach(() => {
PropTypes.resetWarningCache()
});
I have a functional component written in React with Redux using React
Hooks.
I am testing using Jest with Enzyme.
The component renders Material UI Radio buttons (code example below):
<RadioGroup>
<FormControlLabel
value="batchName"
label="Batch Name"
name="batchName"
control={
<Radio
disableRipple
name="batchName"
color="primary"
checked={searchBy === 'batchName'}
onClick={() => {dispatch(actions.setBatchSearchBy('batchName'))}}
/>
}
/>
<FormControlLabel
value="firstPaymentDate"
label="First Payment Date"
name="firstPaymentDate"
control={
<Radio:
disableRipple
name="firstPaymentDate"
color="primary"
checked={searchBy === 'firstPaymentDate'}
onClick={() => {dispatch(actions.setBatchSearchBy('firstPaymentDate'))}}
/>
}
/>
</RadioGroup>
Test file:
import React from 'react';
import { BatchHeaderComponent } from '../../../components/batchManagement/BatchHeaderComponent';
import configureStore from '../../../store';
import {Provider} from "react-redux";
import Enzyme, { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Radio from "#material-ui/core/Radio";
Enzyme.configure({ adapter: new Adapter() });
describe('BatchHeaderComponent', () => {
it('mounts to the DOM with its sub-components', () => {
const wrapper = mount(<Provider store={configureStore()}>
<BatchHeaderComponent/>
</Provider>);
expect(wrapper.find(BatchHeaderComponent)).toBeDefined();
});
it('changes searchBy when a new option has been selected', () => {
const wrapper = mount(<Provider store={configureStore()}>
<BatchHeaderComponent />
</Provider>);
const radio = wrapper.find(Radio).last();
console.log(radio.debug());
// radio.simulate('change', {target: {name: 'firstPaymentDate', checked: true}});
// radio.prop('onChange', {target: { name: 'firstPaymentDate', checked: true}});
radio.simulate('click');
console.log(radio.debug());
expect(radio.props().checked).toEqual(true);
});
});
I can't get the 'checked' value to change when simulating a 'click' or 'change' event.
Regardless of which path I take, the checked value remains false.
Any advice would be greatly appreciated.
Thanks!
I figured it out. I needed to run wrapper.find again to see the updated change.
it('changes searchBy when a new option has been selected', () => {
const wrapper = mount(<Provider store={configureStore()}>
<BatchHeaderComponent />
</Provider>);
const radio = wrapper.find(Radio).last();
radio.simulate('click');
expect(wrapper.find(Radio).last().props().checked).toEqual(true);
});
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'm trying to use react-testing-library to test react-final-form. It works with click-events, like changing a checkbox, but I can't get it working with change-events.
Below is an example using jest as a test runner:
TestForm.js:
import React from 'react';
import { Form, Field } from 'react-final-form';
const TestForm = () => (
<Form
initialValues={{ testInput: 'initial value', testCheckbox: false }}
onSubmit={() => null}
render={({ values, initialValues }) => (
<>
{console.log('VALUES', values)}
<label>
Checkbox label
<Field name="testCheckbox" component="input" type="checkbox"/>
</label>
<Field name="testInput" component="input" placeholder="placeholder" />
{values.testCheckbox !== initialValues.testCheckbox && <div>Checkbox has changed</div>}
{values.testInput !== initialValues.testInput && <div>Input has changed</div>}
</>
)}
/>
);
export default TestForm;
test.js:
import React from 'react';
import { cleanup, fireEvent, render, waitForElement } from 'react-testing-library';
import TestForm from './TestForm';
afterEach(cleanup);
describe('TestForm', () => {
it('Change checkbox', async () => {
const { getByLabelText, getByText } = render(<TestForm />);
const checkboxNode = getByLabelText('Checkbox label');
fireEvent.click(checkboxNode);
await waitForElement(() => getByText('Checkbox has changed'));
});
it('Change input', async () => {
const { getByPlaceholderText, getByText } = render(<TestForm />);
const inputNode = getByPlaceholderText('placeholder');
fireEvent.change(inputNode, { target: { value: 'new value' } });
await waitForElement(() => getByText('Input has changed'));
});
});
I run this using npx jest test.js and the first test passes but not the second.
The crucial part that doesn't seem to work is
fireEvent.change(inputNode, { target: { value: 'new value' } });
Any suggestions?