React jest testing onSubmit by simulate not working - javascript

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

Related

Is it normal for react to re-render component when using promise inside a function?

I'm in progress learning react and I have created an app with a login form. When the login form is submitted the login function passed to the component is executing twice as indicated in the console (logInUser – "running") even though the login button is only clicked once.
The function (loginUser) contains a promise blogService.setToken(xyz).then(xyz) and when the promise is removed the function only executes once.
While I have a method called setToken, this is not using a react state and so this shouldn't be contributing to the issue. I'm wondering why this the promise causing the component to re-render and if anyone could point me into the direction of documentation if promises cause a re-render. I can't seem to hit the right keywords to find the answer.
Can create a GitHub repo to share if that helps. Code below.
Cheers.
Console log
[Log] handleSubmit – "running"
[Log] handleSubmit – {token: "xxx", username: "xxx", name: "xxx"}
[Log] logInUser – "running"
[Log] logInUser: returnedObject – {token: "xxx", username: "xxx", name: "xxx"}
[Log] logInUser – "running"
App.js
import { useState, useEffect } from "react";
import blogService from "./services/blogs";
import LoginForm from "./components/Login";
//...
const App = () => {
const [user, setUser] = useState(null);
const logInUser = (returnedObj) => {
console.log("logInUser", "running");
if (returnedObj) {
console.log("logInUser: returnedObject", returnedObj);
// Causing a rerender
blogService.setToken(returnedObj.token)
.then(() => {
console.log('hello')
// then go and set the user
});
}
};
//...
return (
<div>
{user === null ? (
<div>
<h1>Login</h1>
<Notification message={notificationMessage} />
<LoginForm loggedInUser={logInUser} />
</div>
) : (
<div>
<h1>Logged in</h1>
</div>
)
components/Login.js
import { useState } from "react";
import loginService from "../services/login";
const LoginForm = ({ loggedInUser }) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (event) => {
event.preventDefault();
console.clear();
const user = await loginService
.login({
username,
password,
})
.then((returnedUser) => {
console.log("handleSubmit", "running");
console.log("handleSubmit", returnedUser);
setUsername("");
setPassword("");
loggedInUser(returnedUser);
})
.catch(() => loggedInUser(null));
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Username</label>
<input
type="text"
name="Username"
value={username}
onChange={({ target }) => setUsername(target.value)}
/>
</div>
<div>
<label>Password</label>
<input
type="text"
name="Password"
value={password}
onChange={({ target }) => setPassword(target.value)}
/>
</div>
<button type="submit">Login</button>
</form>
);
};
export default LoginForm;
services/login.js
import axios from 'axios'
const baseUrl = '/api/login'
const login = async credentials => {
const response = await axios.post(baseUrl, credentials)
return response.data
}
export default { login }
service/blog.js
let token = null;
const setToken = (newToken) => {
token = `bearer ${newToken}`;
};
export default { setToken };

Getting component to re-render after form submit. Using useContext and custom hooks to fetch data

I'm having some issues getting a component to re-render after submitting a form. I created separate files to store these custom hooks to make them as reusable as possible. Everything is functioning correctly, except I haven't figured out a way to re render a list component after posting a new submit to that list. I am using axios for fetch requests and react-final-form for my actual form. Am I not able to re-render the component because I am using useContext to "share" my data across child components? My comments are set up as nested attributes to each post, which is being handled through Rails. My comment list is rendered in it's own component, where I call on the usePost() function in the PostContext.js file. I can provide more info/context if needed.
**
Also, on a slightly different note. I am having difficulty clearing the form inputs after a successful submit. I'm using react-final-form and most the suggestions I've seen online are for class components. Is there a solution for functional components?
react/contexts/PostContext.js
import React, { useContext, useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useAsync } from "../hooks/useAsync";
import { getPost } from "../services/post";
const Context = React.createContext();
export const usePost = () => {
return useContext(Context);
};
export const PostProvider = ({ children }) => {
const id = useParams();
const { loading, error, value: post } = useAsync(() => getPost(id.id), [
id.id,
]);
const [comments, setComments] = useState([]);
useEffect(() => {
if (post?.comments == null) return;
setComments(post.comments);
}, [post?.comments]);
return (
<Context.Provider
value={{
post: { id, ...post },
comments: comments,
}}
>
{loading ? <h1>Loading</h1> : error ? <h1>{error}</h1> : children}
</Context.Provider>
);
};
react/services/comment.js
import { makeRequest } from "./makeRequest";
export const createComment = ({ message, postId }) => {
message["post_id"] = postId;
return makeRequest("/comments", {
method: "POST",
data: message,
}).then((res) => {
if (res.error) return alert(res.error);
});
};
react/services/makeRequest.js
import axios from "axios";
const api = axios.create({
baseURL: "/api/v1",
withCredentials: true,
});
export const makeRequest = (url, options) => {
return api(url, options)
.then((res) => res.data)
.catch((err) => Promise.reject(err?.response?.data?.message ?? "Error"));
};
react/components/Comment/CommentForm.js
import React from "react";
import { Form, Field } from "react-final-form";
import { usePost } from "../../contexts/PostContext";
import { createComment } from "../../services/comment";
import { useAsyncFn } from "../../hooks/useAsync";
const CommentForm = () => {
const { post, createLocalComment } = usePost();
const { loading, error, execute: createCommentFn } = useAsyncFn(
createComment
);
const onCommentCreate = (message) => {
return createCommentFn({ message, postId: post.id });
};
const handleSubmit = (values) => {
onCommentCreate(values);
};
return (
<Form onSubmit={handleSubmit}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<div className="comment-form-row">
<Field name="body">
{({ input }) => (
<textarea
className="comment-input"
placeholder="Your comment..."
type="text"
{...input}
/>
)}
</Field>
<button className="comment-submit-btn" type="submit">
Submit
</button>
</div>
</form>
)}
</Form>
);
};
export default CommentForm;

REACT/REDUX Action not getting dispatched

What I am tying to do is when the user clicks on sign in button my action gets dispatch with email and password.
But, my action is not getting dispatched. Like when I checked my redux-dev-tools it is not showing anything:
There are no error message in console. I checked other answer's but nothing helped.
Here is the source code:
LoginScreen.js
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import ErrorMessage from "../../components/ErrorMessage/ErrorMessage";
import Loader from "../../components/Loader/Loader";
import { login } from "../../redux/actions/userActions";
import "./LoginScreen.scss";
const LoginScreen = ({ location, history }) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const userLogin = useSelector((state) => state.userLogin);
const { loading, error, userInfo } = userLogin;
const redirect = location.search ? location.search.split("=")[1] : "/";
useEffect(() => {
if (userInfo) {
history.push(redirect);
}
}, [history, userInfo, redirect]);
const submitHandler = (e) => {
e.preventDefault();
dispatch(login(email, password));
};
return (
<>
<div className="login-container">
<div className="login-form">
<h1>Login</h1>
{loading ? (
<Loader />
) : error ? (
<ErrorMessage error={error} />
) : (
<form onSubmit={submitHandler}>
<div className="login-form-items">
<input
className="login-input"
type="email"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
className="login-input"
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" value="submit">
Login
</button>
<h4>OR</h4>
<div className="login-form-social">
<button className="social">
<img
className="googleLogo"
src="/logo/google.svg"
alt="G"
/>{" "}
Login with Google
</button>
<button className="social social-github">
<img
className="githubLogo"
src="/logo/github.svg"
alt="GH"
/>{" "}
Login with GitHub
</button>
</div>
</div>
</form>
)}
</div>
</div>
</>
);
};
export default LoginScreen;
userAction.js
import axios from "axios";
import {
USER_LOGIN_FAIL,
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
} from "../constants/userConstants";
export const login = () => (email, password) => async (dispatch) => {
try {
dispatch({
type: USER_LOGIN_REQUEST,
});
const config = {
headers: {
"Content-Type": "appllication/json",
},
};
const { data } = await axios.post(
"/api/users/login",
{ email, password },
config
);
dispatch({
type: USER_LOGIN_SUCCESS,
payload: data,
});
localStorage.setItem("userInfo", JSON.stringify(data));
} catch (error) {
dispatch({
type: USER_LOGIN_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
userReducer.js
import {
USER_LOGIN_FAIL,
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
USER_LOGOUT,
} from "../constants/userConstants";
export const userLoginReducer = (state = {}, action) => {
switch (action.type) {
case USER_LOGIN_REQUEST:
return { loading: true };
case USER_LOGIN_SUCCESS:
return { loading: false, userInfo: action.payload };
case USER_LOGIN_FAIL:
return { loading: false, error: action.payload };
case USER_LOGOUT:
return {};
default:
return state;
}
};
store.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
// reducers
import { userLoginReducer } from "./reducers/userReducers";
const reducer = combineReducers({
userLogin: userLoginReducer,
});
const userInfoFromStorage = localStorage.getItem("userInfo")
? JSON.parse(localStorage.getItem("userInfo"))
: null;
const initialState = {
userLogin: { userInfo: userInfoFromStorage },
};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
You've defined your action wrong. With redux-thunk you define your actions like this:
export const login = (email, password) => async (dispatch) => {
// your action code
};
// The above code is equivalent to
export const login = (email, password) => {
return async (dispatch) => {
// your action code
}
}
Not like this:
export const login = () => (email, password) => async (dispatch) => {
// your action code
};
// The above code is equivalent to
export const login = () => {
return (email, password) => {
return async (dispatch) => { // this is wrong
}
}
}
So your action is returning a function which then returns another function.
The way you use it caught my attention. Out of general use. Generally, api operations are done with packages such as saga or thunk. Action is only used as a hyperlink. I suggest you review the article below. I think this build will solve your problem.
https://blog.devgenius.io/reactjs-simple-understanding-redux-with-redux-saga-f635e273e24a

I am rewriting a class component to a functional component in react-redux-firebase project

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

Test function with Jest in React Js

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

Categories

Resources