I'm new to React and testing in general so forgive the naivety of the question. I have a React form component which onChance on the inputs runs a function handleChange. Tried to test it with Jest but can't make it work.
Here's the Login component:
class Login extends React.Component {
constructor() {
super();
this.state = {username: '', password: ''}
this.disableSubmit = this.disableSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({
[e.target.name]: e.target.value
});
}
render() {
return(
<div className="login">
<form>
<h3 className="login__title">LOGIN</h3>
<div className="input-group">
<input onChange={this.handleChange} value={this.state.username} className="form-control login__input username" type="text" placeholder="user name" name={'username'} autoFocus/>
</div>
<div className="input-group">
<input onChange={this.handleChange} value={this.state.password} className="form-control login__input password" type="password" placeholder="password" name={'password'}/>
</div>
<div>
<button className="btn btn-primary btn-block login__button" type="submit">Login</button>
</div>
</form>
</div>
)
}
}
export default Login;
Here's my test:
import React from 'react'
import { shallow, mount } from 'enzyme'
import { shallowToJson } from 'enzyme-to-json'
import {Login} from '../../../src/base/components/index'
describe('Given the Login component is rendered', () => {
describe('Snapshots', () => {
let component
beforeEach(() => {
component = shallow(<Login />)
})
it('should be as expected', () => {
expect(shallowToJson(component)).toMatchSnapshot()
})
})
})
test('Submitting the form should call handleSubmit', () => {
const startState = {username: ''};
const handleChange = jest.fn();
const login = mount(<Login />);
const userInput = login.find('.username');
userInput.simulate('change');
expect(handleChange).toBeCalled();
})
The snapshot test passes fine, but in this last attempt my function test fails with:
TypeError: Cannot read property 'target' of undefined
Guess I need to pass something to the function? Bit confused!
Thanks in advance for your help.
UPDATE:
changed the test as follows but test fails with: expect(jest.fn()).toBeCalled() Expected mock function to have been called.
test updated:
test('Input should call handleChange on change event', () => {
const login = mount(<Login />);
const handleChange = jest.spyOn(login.instance(), 'handleChange');
const userInput = login.find('.username');
const event = {target: {name: "username", value: "usertest"}};
userInput.simulate('change', event);
expect(handleChange).toBeCalled();
})
Yes, you'll need to pass an event object to you simulate function.
const event = {target: {name: "special", value: "party"}};
element.simulate('change', event);
EDIT: Oh, and you'll also need to do something like:
jest.spyOn(login.instance(), 'handleChange')
but that's unrelated to your error
Found the solution in here: Enzyme simulate an onChange event
test('Input should call handleChange on change event', () => {
const event = {target: {name: 'username', value: 'usertest'}};
const login = mount(<Login />);
const handleChange = jest.spyOn(login.instance(), 'handleChange');
login.update(); // <--- Needs this to force re-render
const userInput = login.find('.username');
userInput.simulate('change', event);
expect(handleChange).toBeCalled();
})
It needed this login.update(); in order to work!
Thank everyone for your help!
handleChange isn't currently being mocked. A couple of approaches:
Pass change event handler as prop to Login component.
<div className="input-group">
<input
onChange={this.props.handleChange}
value={this.state.username}
className="form-control login__input username"
type="text"
placeholder="user name"
name={'username'}
autoFocus
/>
</div>
login.spec.js
...
const handleChange = jest.fn();
const login = mount(<Login handleChange={handleChange}/>);
...
Replace handleChange with the mock function.
...
const handleChange = jest.fn();
const login = mount(<Login />);
login['handleChange'] = handleChange // replace instance
...
expect(handleChange).toBeCalled();
Use jest spyOn to create a mock function that wraps the original function.
...
const handleChange = jest.spyOn(object, 'handleChange') // will call the original method
expect(handleChange).toBeCalled();
Replace handleChange on the Login component with a mock function.
...
const handleChange = jest.spyOn(object, 'handleChange').mock // will call the original method
expect(handleChange).toBeCalled();
Related
I am rebuilding this component.
https://github.com/ayush221b/MarioPlan-react-redux-firebase-app/blob/master/src/Components/projects/CreateProject.js
https://github.com/ayush221b/MarioPlan-react-redux-firebase-app/blob/master/src/store/actions/projectActions.js
however, I don't know how to rewrite mapStateToProps and mapDispatchToProps
The error says
FirebaseError: Function addDoc() called with invalid data. Document fields must not be empty (found in field `` in document projects/5to35LFKROA5aKMXpjqy)
The project seems not be dispatched??
import {Component ,useState} from 'react'
import {connect} from 'react-redux'
import {createProject} from '../../store/actions/projectActions'
import { useForm } from "react-hook-form";
import { Redirect } from 'react-router-dom'
const CreateProject = (props) => {
const [state, setState] = useState({
title: "",
content: ""
});
const handleChange = event => {
setState({ ...state, [event.target.name]: event.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(state);
props.createProject(state);
props.history.push('/');
}
const { auth } = props;
return (
<div className="container">
<form className="white" onSubmit={handleSubmit}>
<h5 className="grey-text text-darken-3">Create a New Project</h5>
<div className="input-field">
<input type="text" id='title' onChange={handleChange} />
<label htmlFor="title">Project Title</label>
</div>
<div className="input-field">
<textarea id="content" className="materialize-textarea" onChange={handleChange}></textarea>
<label htmlFor="content">Project Content</label>
</div>
<div className="input-field">
<button className="btn pink lighten-1">Create</button>
</div>
</form>
</div>
)
}
const mapDispatchToProps = (dispatch) => {
console.log("a"+dispatch);
return {
createProject: (project) => dispatch(createProject(project))
}
}
const mapStateToProps = (state) =>{
return{
auth: state.firebase.auth
}
}
export default connect(mapStateToProps,mapDispatchToProps)(CreateProject)
in the functional component, you can use hooks like "useSelector" to get the store states
const firebase = useSelector(state => state.firebase)
and "useDispatch" to trigger an action
const dispatch = useDispatch()
<button onClick={() => dispatch({ type: 'GET_DATA' })} >Click me</button>
don't forget to import
import { useSelector, useDispatch } from 'react-redux'
Problem: Missing name Property on Inputs
FirebaseError: Function addDoc() called with invalid data. Document fields must not be empty (found in field `` in document projects/5to35LFKROA5aKMXpjqy)
This error doesn't have anything to do with mapStateToProps. You are failing this test by passing a an object with an empty key.
{
title: "Some Title",
content: "some content",
'': "some value"
}
So where does that empty key come from? Well you are setting values in the state with a dynamic key based on the event.target.name.
const handleChange = (event) => {
setState({
...state,
[event.target.name]: event.target.value
});
};
When you change the input or the textarea, what is event.target.name? Take a look at your code.
<input type="text" id="title" onChange={handleChange} />
There is no name property!
You must either:
A) Add a name to each of the inputs that corresponds to the property which you want to update in the state.
<input type="text" id="title" name="title" onChange={handleChange} />
or B) Change your setState to use event.target.id, which is already set.
const handleChange = (event) => {
setState({
...state,
[event.target.id]: event.target.value
});
};
I recommend B) as it looks like that's what you were doing before.
Redux Hooks
Integrating with the redux hooks is very simple. Easier than dealing with connect, in my opinion.
Access auth from a selector.
const auth = useSelector((state) => state.firebase.auth);
Call useDispatch add the top-level of your component to access dispatch.
const dispatch = useDispatch();
In your handleSubmit, call dispatch with the results of your action creator.
dispatch(createProject(state));
Complete Code
const CreateProject = (props) => {
const auth = useSelector((state) => state.firebase.auth);
const dispatch = useDispatch();
const [state, setState] = useState({
title: "",
content: ""
});
const handleChange = (event) => {
setState({ ...state, [event.target.id]: event.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(state);
dispatch(createProject(state));
props.history.push("/");
};
return (
<div className="container">
<form className="white" onSubmit={handleSubmit}>
<h5 className="grey-text text-darken-3">Create a New Project</h5>
<div className="input-field">
<input type="text" id="title" onChange={handleChange} />
<label htmlFor="title">Project Title</label>
</div>
<div className="input-field">
<textarea
id="content"
className="materialize-textarea"
onChange={handleChange}
/>
<label htmlFor="content">Project Content</label>
</div>
<div className="input-field">
<button className="btn pink lighten-1">Create</button>
</div>
</form>
</div>
);
};
I am learning reactjs form with hooks, now I would like to test form on submit using jest and enzyme.
here is my login component.
import React from 'react'
function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
// ....api calLS
}
return (
<div>
<form onSubmit={handleSubmit} className="login">
<input type="email" id="email-input" name="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" id="password-input" name="password" value={password} onChange={e =>setPassword(e.target.value)} />
<input type="submit" value="Submit" />
</form>
</div>
)
}
export default Login
Here is is login.test.js file
it('should submit when data filled', () => {
const onSubmit = jest.fn();
const wrapper = shallow(<Login />)
const updatedEmailInput = simulateChangeOnInput(wrapper, 'input#email-input', 'test#gmail.com')
const updatedPasswordInput = simulateChangeOnInput(wrapper, 'input#password-input', 'cats');
wrapper.find('form').simulate('submit', {
preventDefault: () =>{}
})
expect(onSubmit).toBeCalled()
})
Unfortunately when I run npm test I get the following error
What do I need to do to solve this error or tutorial on testing form?
Issue here is you created a mock but it is not being consumed by the component you are testing.
const onSubmit = jest.fn(); // this is not being used by <Login />
A solution to this would be to mock the api calls you described on your code with the comment // ....api calLS and verify those are called successfully.
import { submitForm } from './ajax.js'; // the function to mock--called by handleSubmit
jest.mock('./ajax.js'); // jest mocks everything in that file
it('should submit when data filled', () => {
submitForm.mockResolvedValue({ loggedIn: true });
const wrapper = shallow(<Login />)
const updatedEmailInput = simulateChangeOnInput(wrapper, 'input#email-input', 'test#gmail.com')
const updatedPasswordInput = simulateChangeOnInput(wrapper, 'input#password-input', 'cats');
wrapper.find('form').simulate('submit', {
preventDefault: () =>{}
})
expect(submitForm).toBeCalled()
})
Useful links
very similar question
mocking modules
understanding jest mocks
Disclaimer: I am not experienced with the Enzyme framework.
Because your mocked function onSubmit is not binded to your form. You can't test it this way. If you gonna call some api onSubmit, you can mock this api and check if it was called (mockedApiFunction).
The state is updated only on the next keystroke but with the previous state. Screen 1
When you click on updateForm (), it is also empty, only after the second click, the state is updated. Screen 2
I understand that this is due to asynchrony, but in this case I do not know how to use it.
Home.jsx
import React, { useState } from 'react';
import { Form } from '../components/Form/Form';
const Home = () => {
const [dateForm, setDataForm] = useState({});
const updateForm = eachEnry => {
setDataForm(eachEnry);
console.log(dateForm);
};
return (
<div>
<Form updateForm={updateForm} />
</div>
);
};
export default Home;
Form.jsx
import React, { useState } from 'react';
import './Form.scss';
export const Form = ({ updateForm }) => {
const initInputState = {
name: '',
password: ''
};
const [dataForm, setDataForm] = useState(initInputState);
const { name, password } = dataForm;
const onChange = e => {
setDataForm({
...dataForm,
[e.target.name]: e.target.value
});
};
const onSubmit = e => {
e.preventDefault();
updateForm(dataForm);
};
return (
<div>
<form onSubmit={onSubmit}>
<input
type="text"
placeholder="Name"
value={name}
onChange={onChange}
name="name"
/>
<input
placeholder="Password"
onChange={onChange}
value={password}
type="text"
name="password"
/>
<button type="submit">Login</button>
</form>
</div>
);
};
Your code is working fine. You just doing console.log before the state is updated. State updates happen not when you using an update state function. It's happening when all component action and nested components actions are done.
Check your code with console log on another place click to check
As you can see I placed a console log on every Home component rerender. You can check that all works fine.
P.S. I did some improvements to your code. Check if u like it. And add a comment to updateForm function. Check this one too, please.
You evidently are not setting your state properly, here
setDataForm({
...dataForm,
[e.target.name]: e.target.value
});
should be
setDataForm(c => ({
...c,
[e.target.name]: e.target.value
}));
this is my first week dealing with testing, and i get confused, i'm trying to test SignIn component, i have test the snapshot to ensure that mockup behavior not changing, then i want to test the submit behavior, here is my code:
signIn-component.jsx
import React, { useState } from 'react';
import FormInput from '../form-input/form-input.component';
import CustomButton from '../custom-button/custom-button.component';
import { connect } from 'react-redux';
import {
googleSignInStart,
emailSignInStart,
} from '../../redux/user/user.actions';
import './sign-in.styles.scss';
export const SignIn = ({ emailSignInStart, googleSignInStart }) => {
const [userCredentials, setCredentials] = React.useState({
email: '',
password: '',
});
const { email, password } = userCredentials;
const handleSubmit = async (event) => {
event.preventDefault();
emailSignInStart(email, password);
};
const handleChange = (event) => {
const { value, name } = event.target;
setCredentials({ ...userCredentials, [name]: value });
};
return (
<div className="sign-in">
<h2>I already have an account</h2>
<span>Sign in with your email and password</span>
<form onSubmit={handleSubmit}>
<FormInput
name="email"
type="email"
handleChange={handleChange}
value={email}
label="email"
required
/>
<FormInput
name="password"
type="password"
value={password}
handleChange={handleChange}
label="password"
required
/>
<div className="buttons">
<CustomButton type="submit"> Sign in </CustomButton>
<CustomButton
type="button"
onClick={googleSignInStart}
isGoogleSignIn
>
Sign in with Google
</CustomButton>
</div>
</form>
</div>
);
};
const mapDispatchToProps = (dispatch) => ({
googleSignInStart: () => dispatch(googleSignInStart()),
emailSignInStart: (email, password) =>
dispatch(emailSignInStart({ email, password })),
});
export default connect(null, mapDispatchToProps)(SignIn);
sign.test.js
import { shallow , mount } from 'enzyme';
import React from 'react';
import toJson from 'enzyme-to-json';
import { SignIn } from '../sign-in.component';
describe('Sign In component', () => {
let wrapper;
const mockemailSignInStart = jest.fn();
const mockgoogleSignInStart = jest.fn();
const mockHandleSubmit = jest.fn();
beforeEach(() => {
wrapper = shallow(<SignIn
emailSignInStart={mockemailSignInStart}
googleSignInStart={mockgoogleSignInStart}/>
);
});
it('expect to render signIn component', () => {
expect(toJson(wrapper)).toMatchSnapshot();
});
it('expect call fn on submit', () => {
wrapper.find('form').simulate('submit');
expect(mockHandleSubmit).toBeCalled();
});
});
I have tried mount and render but always expect toBeCalled always return 0
I see 2 problems in your code:
1) I think this:
expect(mockHandleSubmit).toBeCalled();
should actually be
expect(mockemailSignInStart).toBeCalled();
because handleSubmit dispatches emailSignInStart which you mock with googleSignInStart.
2) You should pass some argument to your simulate('submit') or the handleSubmit will throw an error when calling event.preventDefault();. For instance you can just use:
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() });
Since I'm learning how to build React forms with hooks, I went through the 3 quicks posts that culminate with this one. Everything is going well until I get to the last step when you create your custom hook with:
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
};
}
The Input is:
const Input = ({ type, name, onChange, value, ...rest }) => (
<input
name={name}
type={type}
value={value}
onChange={event => {
event.preventDefault();
onChange(name, event.target.value);
}}
{...rest}
/>
);
And the Form is:
const Form = () => {
const email = useFormInput("");
const password = useFormInput("");
return (
<form
onSubmit={e =>
e.preventDefault() || alert(email.value) || alert(password.value)
}
>
<Input
name="email"
placeholder="e-mail"
type="email"
{...email}
/>
<Input
name="password"
placeholder="password"
type="password"
{...password}
/>
<input type="submit" />
</form>
);
};
So in useFormInput() Chrome complains about
TypeError: Cannot read property ‘value’ of undefined at handleChange
which I'm pretty sure is pointing me to
function handleChange(e) {
setValue(e.target.value);
}
If I console.log(e) I get 'email', as expected (I think?), but if I try console.log(e.target) I get undefined. So obviously e.target.value doesn't exist. I can get it working by just using
setValue(document.getElementsByName(e)[0].value);
but I don't know what kind of issues this might have. Is this a good idea? Are there drawbacks to getting it to work this way?
Thanks
The issue comes from the onChange prop in the Input component
onChange={event => {
event.preventDefault();
onChange(name, event.target.value);
}}
you're calling onChange like this onChange(name, event.target.value); (two arguments, the first one is a string), while in your custom hook you define the callback like this
function handleChange(e) {
setValue(e.target.value);
}
it's expecting one argument, an event.
So either call onChange with one argument (the event) :
onChange={event => {
event.preventDefault();
onChange(event);
}}
or change the implementation of the callback.
Try this out:
const handleChange = e => {
const { inputValue } = e.target;
const newValue = +inputValue;
setValue(newLimit);
};
Had this issue with a calendar picker library react-date-picker using Register API. Looking at the documentation found out that there's another way of handling components that don't return the original event object on the onChange function using the Controller API.
More details on Controller API Docs
Example:
/*
* React Function Component Example
* This works with either useForm & useFormContext hooks.
*/
import { FC } from 'react'
import { Controller, useFormContext } from 'react-hook-form'
import DatePicker,{ DatePickerProps } from 'react-date-picker/dist/entry.nostyle'
const FormDateInput: FC<Omit<DatePickerProps, 'onChange'>> = ({
name,
...props
}) => {
const formMethods = useFormContext()
const { control } = formMethods ?? {}
return (
<Controller
render={({ field }) => <DatePicker {...props} {...field} />}
name={name ?? 'date'}
control={control}
/>
)
}
export default FormDateInput