Unable to test React useState hook using Mocha and Enzyme - reactjs

I have a login Component with two inputs one is for username and the other is for password. I have onChange function for both these inputs. I have written some test cases which works fine. I need to write few more test cases which involves testing of initial state and state after updating. I have researched a lot but couldn't find suitable example for useState hooks testing with MOCHA and Enzyme.
My last test case is failing.
Mocha is mandatory for me. Any help please?
Login Component
----------------
import React from 'react'
import { Form, FormGroup, TextInput, Button } from 'carbon-components-react'
import Login16 from '#carbon/icons-react/lib/login/16'
import { PropTypes } from 'prop-types'
function LoginComponent(props) {
const { username, password, onUsernameChange, onPasswordChange, onSubmit } = props;
return (
<div className="bx--row login-box">
<div className="bx--col-xs-6 bx--col-sm-6 bx--col-md-6 bx--col-lg-6 login-form">
<Form className="form-box">
<FormGroup legendText="Login">
<div>Sign in to your account</div>
<TextInput
className="login-input"
id="username"
name="username"
value={username}
onChange={onUsernameChange}
labelText=""
placeholder="User Name"
type="text"
/>
<TextInput
className="login-input"
id="password"
name="password"
value={password}
onChange={onPasswordChange}
labelText=""
placeholder="Password"
type="password"
/>
<Button
className=""
id="login-btn"
onClick={onSubmit}
>
<Login16 className="login-icon"/> Login
</Button>
</FormGroup>
</Form>
</div>
<div className="bx--col-xs-6 bx--col-sm-6 bx--col-md-6 bx--col-lg-6 login-image">
<img src="/ibmlogo.png" alt=""/>
</div>
</div>
)
}
LoginComponent.propTypes = {
username: PropTypes.string,
password: PropTypes.string,
onUsernameChange: PropTypes.func,
onPasswordChange: PropTypes.func,
onSubmit: PropTypes.func
}
export default LoginComponent
Login.test.js
-------------
import React from 'react'
import { shallow } from 'enzyme'
import chai, { expect } from 'chai';
import chaiEnzyme from 'chai-enzyme';
import { spy } from 'sinon'
import LoginComponent from '../components/Login';
describe('Login component testing', () => {
const handleChange = spy();
const props = {
username: "",
password: "",
onUsernameChange: handleChange,
onPasswordChange: handleChange,
onSubmit: () => {}
}
const wrapper = shallow(<LoginComponent {...props}/>);
let input;
beforeEach(() => {
input = ""
})
afterEach(() => {
})
it('Should have two inputs', () => {
input = wrapper.find('.login-input');
expect(input).to.have.length(2);
})
it('Should have one button to handle onSubmit', () => {
input = wrapper.find('#login-btn');
expect(input).to.have.length(1);
})
it('Should have an initial state for username and password to be empty string or undefined', () => {
expect(wrapper.find("#username").prop('value')).to.equal('');
expect(wrapper.find("#password").prop('value')).to.equal('');
})
it('Should have props for onUsernameChange, onPasswordChange, and onSubmit', () => {
expect(wrapper.find('#username').props().onChange).to.not.be.an('undefined');
expect(wrapper.find('#password').props().onChange).to.not.be.an('undefined');
expect(wrapper.find('#login-btn').props().onClick).to.not.be.an('undefined');
})
//below test case is failing.
it('Should update state for username and password onChange', () => {
const func = wrapper.find('#username');
func.simulate('change', { target: { value: "username" } })
console.log(wrapper.find('#username').debug())
expect(wrapper.find("#username").prop('value')).to.equal('username');
})
chai.use(chaiEnzyme());
})

Related

react page is rendering blank [duplicate]

This question already exists:
return not displaying page data react functional component
Closed 1 year ago.
I have this, my entire react page:
import React, { useState, useEffect } from "react";
import axios from "axios";
import { useHistory } from "react-router-dom";
import { useMemo } from "react";
import { connect } from "react-redux";
import AdminNav from "../../../components/admin/AdminNav"
import AdminAboutUsNav from "../../../components/admin/AdminAboutUsNav"
import Header from "../../../components/app/Header";
import { setNavTabValue } from '../../../store/actions/navTab';
import { makeStyles, withStyles } from "#material-ui/core/styles";
import "../../../styles/AddMembershipPage.css";
const AddMembershipPage = (props) => {
const history = useHistory();
const [myData, setMyData] = useState({});
let ssoDetails = {
name: props.blue.preferredFirstName + " " + props.preferredLastName,
email: props.blue.preferredIdentity,
cnum: props.blue.uid,
empType: "part-time"
}
this.state = {
cnum: ssoDetails.cnum,
empType: ssoDetails.empType,
email: ssoDetails.email,
name: ssoDetails.name,
phone: "",
// building: building,
siteList: "",
status: ""
};
const handleInputChange = (e) => {
this.setState({
[e.target.name]: e.target.value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
var date = Date().toLocaleString();
const { cnum, empType, email, name, phone, siteList, status } = this.state;
const selections = {
cnum: cnum,
empType: empType,
email: email,
name: name,
phone: phone,
// building: building,
siteList: siteList,
status: status
};
axios
.post("/newMembership", selections)
.then(
() => console.log("updating", selections),
(window.location = "/admin/services")
)
.catch(function (error) {
// alert(error)
window.location = "/admin/services/new";
});
};
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper,
},
}));
const classes = useStyles();
return (
<div className={classes.root}>
<AdminNav />
{/* <Header title="Services - Admin" /> */}
{/* <AdminAboutUsNav /> */}
<div className="App">
<form onSubmit={this.handleSubmit}>
<h1>Join Us!</h1>
<input value={ssoDetails.name} readOnly name="name" onChange={this.handleInputChange}></input>
<input type="email" value={ssoDetails.email} readOnly name="email" onChange={this.handleInputChange}></input>
<input type="hidden" value={ssoDetails.cnum} readOnly name="cnum" onChange={this.handleInputChange}></input>
<input type="text" value={ssoDetails.empType} readOnly name="empType" onChange={this.handleInputChange}></input>
<input type="text" placeholder="Phone Number" name="phone" onChange={this.handleInputChange}></input>
<input type="text" placeholder="Site List" name="siteList" onChange={this.handleInputChange}></input>
{/* <input type="password" placeholder="Password"></input> */}
<button type="submit">Register</button>
</form>
</div>
</div>
);
}
const mapStateToProps = (state) => {
return {
siteTab: state.siteTab,
blue: state.blue
}
}
const mapDispatchToProps = (dispatch, props) => ({
setNavTabValue: (value) => dispatch(setNavTabValue(value))
});
export default connect(mapStateToProps, mapDispatchToProps)(AddMembershipPage);
however, when I try to run this page, it just shows up blank. It started doing this after I added const handleInputChange, and const handleSubmit to the code. I am basically just trying to submit a form, and it is more complex then I imagined. Before I added those 2 things I just mentioned, the page was working perfectly. but now, I cannot figure it out, and really could use some guidance/help to try to fix this up. any ideas?
It's function component so you don't need to call with this.handleSubmit
Just change it to the onSubmit={handleSubmit}> and onChange={handleInputChange}>
Also remove this.state and use useState instead because this.state was available in class based component not in the function component.

Formik form submission with react-testing library

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.

React redux-form - error while testing - "TypeError: handleSubmit is not a function"

Im struggling to find a solution when testing a redux-form component. The problem is that only when I test simply if the component is rendering it gives me an error: "TypeError: handleSubmit is not a function", but the app is working fine, as expected.
I've tried to solve it just to make handleSubmit a function and not taking it from props, but then the app doesn't work. When the submit form is correct it must navigate to /landing page, but instead just re-render the login component.
The component:
import React, { Component } from 'react'
//import { Link } from 'react-router-dom'
import { Field, reduxForm } from 'redux-form'
import '../../style/style.css'
export class Login extends Component {
renderField(field) {
const { meta: { touched, error} } = field;
const className = `form-group ${touched && error ? 'has-danger' : ''}`;
return (
<div className={className}>
<input className="form-control" id="username_field" placeholder={field.label} type="text" {...field.input} />
<div className="text-help">
{ field.meta.touched ? field.meta.error : '' }
</div>
</div>
)
}
renderPasswordField(field) {
const { meta: { touched, error} } = field;
const className = `form-group ${touched && error ? 'has-danger' : ''}`;
return (
<div className={className}>
<input className="form-control" id="password_field" placeholder={field.label} type="password" {...field.input} />
<div className="text-help">
{ field.meta.touched ? field.meta.error : '' }
</div>
</div>
)
}
onSubmit(values) {
this.props.history.push('/landing')
}
// DOESN'T WORK!!!
// handleSubmit(formValues){
// //console.log(formValues);
// }
render() {
const { handleSubmit } = this.props
return (
<div>
<div className="login-form">
<form onSubmit={ /*this.*/handleSubmit(this.onSubmit.bind(this))}>
<h2 className="text-center">TQI Log in</h2>
<div className="form-group">
<Field id="username" name="username" label="username" component={this.renderField} />
</div>
<div className="form-group">
<Field id="password" name="password" label="password" component={this.renderPasswordField} />
</div>
<div className="form-group">
<button id="login_button" type="submit" className="btn btn-primary btn-block">Login </button>
</div>
</form>
</div>
</div>
);
}
}
function validate(values) {
const errors = {}
const dummyData = {
username: 'admin',
password: '123'
}
// Validate the inputs from values
if(!values.username) {
errors.username = "Enter a username"
} else if(values.username !== dummyData.username){
errors.username = "Wrong username"
}
if(!values.password) {
errors.password = "Enter a password"
} else if( values.username === dummyData.username && values.password !== dummyData.password){
errors.password = "Wrong password"
}
// if errors is empty, the form is fine to submit
// If errors has *any* properties, redux forms assumes form is invalid
return errors
}
export default reduxForm({
validate,
form: 'LoginForm'
})(Login)
The testing file:
import React from 'react'
import { shallow } from 'enzyme'
import { Login } from './login'
describe('<Login />', () => {
it('render the <Login /> component without crashing', () => {
const wrapper = shallow(<Login />)
expect(wrapper.length).toEqual(1)
})
// it('navigate to /landing page when the form is submit correctly', () => {
// })
})
You are consuming a prop function from your HOC reduxForm on your render method. But on the test file, you are importing the component without the HOC on top of it, which means that prop/function is not available. You have to provide Login with a mock handleSubmit prop function.
Try:
import React from 'react'
import { shallow } from 'enzyme'
import { Login } from './login'
describe('<Login />', () => {
it('render the <Login /> component without crashing', () => {
const wrapper = shallow(<Login handleSubmit={() => {}} />)
expect(wrapper.length).toEqual(1)
})
})
you need pass onSubmit on initialization:
export default reduxForm({
validate,
form: 'LoginForm',
onSubmit: this.onSubmit // here
})(Login)
or in props:
<Component>
<Login onSubmit={this.onSubmit}/>
</Component>
First export the redux-form decorated class
export const DecoratedLogin = reduxForm({
validate,
form: 'LoginForm'
})(Login);
Then use it instead of the plain class.
import { DecoratedLogin } from './login'
and you should be able to access the props from redux-form.

Testing react-final-form using react-testing-library

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?

Jest How to test react form submission?

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);
});
});

Resources