Im new to javascript, react and jest. I wanted to mock a function which is gonna be passed as a context value. Im able to pass the mock function and im able to see that the mock function is triggered but when I try to inspect the mock to assert the results. im getting error which is attached below.
This is my class to be tested:
class Login extends Component {
constructor(props) {
super(props);
this.state = {
authInProgress: true,
authSuccess: false
};
}
componentDidMount() {
if (this.context.isAuthenticated) {
this.setState({
authInProgress: false,
authSuccess: true
});
} else {
isUserLoggedIn().then((response) => {
this.context.logIn();
this.setState({
authInProgress: false,
authSuccess: true
});
}).catch((err) => {
console.log(err);
});
}
}
render() {
if (this.state.authInProgress) {
return <div>Rendering login</div>;
} else if (this.state.authSuccess) {
return <Redirect to={ROOT}/>;
}
}
}
Login.contextType = Context;
export default Login;
this is my test:
it("When user is logged In, then update the context and redirect to ROOT.", () => {
const resp = {
data: {
responseCode: 600
}
};
isUserLoggedIn.mockResolvedValue(resp);
const mockLogIn = jest.fn(() => console.log("im hit"));
act(() => {
render(
<MemoryRouter>
<Context.Provider value={{isAuthenticated: false, logIn: mockLogIn}}>
<Login/>
</Context.Provider>
</MemoryRouter>
, container);
});
expect(mockLogIn.mock.calls.length).toBe(1);
});
This is the test results:
console.log src/__tests__/auth/components/Login.test.jsx:62
im hit
Error: expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 0
<Click to see difference>
I see that the mock function is triggered. but the mock's state is not updated. what am I doing wrong here?
Is it something to do with the scopes? I tried calling the mock function outside of the act block and that state is updated to the mock. when the same call happens inside the block, its not working.
Well the async was the problem here. I started using the awesome react-testing-library and added waits for my tests. then it worked as expected.
Now my test looks like this,
it("When the user's session is still active (auth tokens are valid), then refresh the tokens and the user is redirected to root.", async () => {
const resp = {
data: {
responseCode: 600
}
};
isUserLoggedIn.mockResolvedValue(resp);
refreshAuthData.mockImplementation();
const mockLogin = jest.fn();
const {history} = renderWithContextAndRouter(
<Login/>, {
route: "/login",
context: {
isAuthenticated: false,
logIn: mockLogin
}
}
);
await waitFor(() => {
expect(history.location.pathname).toBe("/");
expect(mockLogin.mock.calls.length).toBe(1);
});
});
renderWithContextAndRouter is a HOC that injects my context and routes. and my actual test doesn't even have this verification as those are just the implementation details. this is just to help some fellow beginners :)
Getting really fed up now! I am trying to get a Spinner element to appear while 3 functions run in the componentDidMount function.
From what I gather the render comes before componentDidMount, so I am running the Spinner in the render, while:
a cookie value is retrieved from this.getValidToken()
then an axios post request sets state of isLoggedin (using above value as payload)
then the logic() function runs a simple if statement to either log user in or redirect to
error page.
I keep getting errors about Promises, I feel there is a better way to do this?
constructor(props){
super(props);
this.state = {
isLoggedIn: false
}
}
componentDidMount() {
const post =
axios.post(//api post request here)
.then(function(response) {
this.setState({ isLoggedIn: true });
})
.catch(function(error) {
this.setState({ isLoggedIn: false });
})
const LoggedIn = this.state.isLoggedIn;
const logic = () => {
if (LoggedIn) {
//log user in
} else {
//redirect user to another page
}
};
this.getValidToken()
.then(post)
.then(logic);
//getValidToken firstly gets a cookie value which is then a payload for the post function
}
render() {
return <Spinner />;
}
Firstly, you assign axios post to a variable, it is executed immediately and not after the getValidToken promise is resoved
Secondly the state update in react is async so you cannot have loggedIn logic based on state in promise resolver
You could handle the above scenario something like
constructor(props){
super(props);
this.state = {
isLoggedIn: false
}
}
componentDidMount() {
const post = () => axios.post(//api post request here)
.then(function(response) {
this.setState({ isLoggedIn: true });
return true;
})
.catch(function(error) {
this.setState({ isLoggedIn: false });
return false;
})
const logic = (isLoggedIn) => { // use promise chaining here
if (isLoggedIn) {
//log user in
} else {
//redirect user to another page
}
};
this.getValidToken()
.then(post)
.then(logic);
//getValidToken firstly gets a cookie value which is then a payload for the post function
}
render() {
return <Spinner />;
}
I want to generate a menu dynamically depending on user connected state and user role. I have a json file from which feeds the React app with all menu choices. The problem is that it does offer the "login" and "contact" options, which don't require any specific role or for a user to be logged in, but when I log in with the App's login() method, in which I use the fetch API and set the new state in the response, it doesn't refresh the menu choices (which is done in componentDidMount() method. It keeps serving me the login and contact options. I want to switch the login for logout when a user is connected.
I tried a bunch of stuff, but putting logs and debuggers in my code, I noticed the component doesn't re-render after the setState that's called in the login() fetch operation, but the state is indeed getting changed. I'm curious as to why the setState is not firing componentDidMount()?
menu.json
[
{
"txt": "login",
"idRole": null
},
{
"txt": "logout",
"idRole": null
},
{
"txt": "register",
"idRole": [
1
]
},
{
"txt": "profile",
"idRole": [
1,
2,
3
]
},
{
"txt": "contact",
"idRole": null
}
]
App.js
import React, { Component } from 'react'
import Header from 'container/Header.js'
import Footer from './container/Footer'
import Login from './container/Login'
import menu from '../json-form-file/menu.json'
export default class App extends Component {
constructor (props) {
super(props)
this.state = {
isMenuOpen: false,
isLoggedIn: false,
menu: null,
page: null,
user: null
}
this.toggleMenu = this.toggleMenu.bind(this)
this.selectPage = this.selectPage.bind(this)
this.login = this.login.bind(this)
this.logout = this.logout.bind(this)
}
toggleMenu () {
this.setState({ isMenuOpen: !this.state.isMenuOpen })
}
selectPage (event) {
this.setState({ isMenuOpen: !this.state.isMenuOpen, page: event.target.textContent })
const toggler = document.getElementsByClassName('toggler')[0]
toggler.checked = !toggler.checked
}
login (event) {
event.preventDefault()
const requestBody = createLoginRequestBody(Array.from(event.target.parentElement.children))
clearLoginFields(Array.from(event.target.parentElement.children))
if (requestBody.username !== undefined && requestBody.pwd !== undefined) {
fetch('www.someLoginUrl.login', {
method: 'post',
body: JSON.stringify(requestBody),
headers: { 'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(response => this.setState({ user: response, isLoggedIn: true, page: null }))
}
}
logout (event) {
event.preventDefault()
const toggler = document.getElementsByClassName('toggler')[0]
toggler.checked = !toggler.checked
this.setState({ user: null, isLoggedIn: false, page: null, isMenuOpen: !this.state.isMenuOpen })
}
componentDidMount () {
console.log('im mounting')
const newMenu = this.refreshMenuSelection(menu)
this.setState({ menu: newMenu })
}
refreshMenuSelection (list) {
const newMenu = []
list.map((item) => {
if (item.txt === 'login' && this.state.isLoggedIn === false) newMenu.push(item)
if (item.txt === 'logout' && this.state.isLoggedIn === true) newMenu.push(item)
if (item.idRole === null && item.txt !== 'login' && item.txt !== 'logout') newMenu.push(item)
if (this.state.user !== null && item.idRole.includes(this.state.user.id_role)) newMenu.push(item)
})
return newMenu
}
render () {
return (
<div>
<Header
menu={this.state.menu}
toggleMenu={this.toggleMenu}
selectPage={this.selectPage}
logout={this.logout}
color={this.state.isMenuOpen ? secondaryColor : primaryColor} />
{this.state.page === 'login' ? <Login login={this.login} /> : null}
<Footer color={this.state.isMenuOpen ? secondaryColor : primaryColor} />
</div>
)
}
}
const createLoginRequestBody = (inputs) => {
const requestObject = {}
inputs.map((input) => {
if (input.id === 'username') Object.assign(requestObject, { username: input.value })
if (input.id === 'pwd') Object.assign(requestObject, { pwd: input.value })
})
return requestObject
}
When a user is not logged in, he could see only login and contact. When logged in, he could see logout instead of login, contact and all other choices relevant to his role.
Nothing causes a componentDidMount to run again, it's a lifecycle hook which runs only one time through the component's lifecycle. Everything that goes inside componentDidMount will only run once (after the first render), so if you need to react to a change to perform imperative code, take a look at componentDidUpdate. You should also take a look in the documentation
As said, componentDidMount only runs once when the component is first mounted. If you use setState() after the first mount, the function will not respond to it. If you want to do that, maybe you ought to use componentDidUpdate which does react to this type of change. And also, there's another thing wrong with your code. If you use setState() and use componentDidUpdate then it will change the state again and calling the function again until the program crashes. So if you don't want to cause that, maybe also remove that or move it to a new componentDidMount function.
Thanks to everyone who guided me to the componentDidUpdate() method. This modified bit of code helped me achieve what I wanted. In the future I'll clean up the code to remove the justLoggedIn, as that might not be necessary, but without that it was giving my the setState depth error.
login (event) {
event.preventDefault()
const requestBody = createLoginRequestBody(Array.from(event.target.parentElement.children))
clearLoginFields(Array.from(event.target.parentElement.children))
if (requestBody.username !== undefined && requestBody.pwd !== undefined) {
fetch('http://127.0.0.1:8080/user/login', {
method: 'post',
body: JSON.stringify(requestBody),
headers: { 'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(response => this.setState({ user: response, justLoggedIn: true, isLoggedIn: true }))
}
}
logout (event) {
event.preventDefault()
const toggler = document.getElementsByClassName('toggler')[0]
toggler.checked = !toggler.checked
this.setState({ user: null, justLoggedOut: true, isLoggedIn: false, isMenuOpen: !this.state.isMenuOpen })
}
componentDidMount () {
if (this.state.user === null) this.setState({ menu: this.refreshMenuSelection(menu) })
}
componentDidUpdate () {
if (this.state.user !== null && this.state.justLoggedIn) this.setState({ menu: this.refreshMenuSelection(menu), justLoggedIn: false, page: null })
if (this.state.user === null && this.state.justLoggedOut) this.setState({ menu: this.refreshMenuSelection(menu), justLoggedOut: false, page: null })
}
I am using the following Container code to show a SnackBar if users email already exists with another account, If i remove this line of code (this.setState({ open: true })) The snack bar shows up fine by setting the state using a button :
class HomeContainer extends Component {
static propTypes = {
authError: PropTypes.string,
removeError: PropTypes.func.isRequired
}
static contextTypes = {
router: PropTypes.object.isRequired
}
constructor (props) {
super(props)
this.state = {
open: false
}
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// the following componetdid mount function is only required when using SigninwithRedirect Method - remove when using SigninWithPopup Method
componentDidMount () {
firebaseAuth.getRedirectResult().then((authUser) => {
// The signed-in user info.
console.log('User Data from Oauth Redirect: ', authUser)
const userData = authUser.user.providerData[0]
const userInfo = formatUserInfo(userData.displayName, userData.photoURL, userData.email, authUser.user.uid)
return (this.props.fetchingUserSuccess(authUser.user.uid, userInfo))
})
.then((user) => {
return saveUser((user.user))
})
.then((user) => {
return (this.props.authUser(user.uid))
}).then((user) => {
this.context.router.replace('feed')
}).catch(function (error) {
// Handle Errors here.
const errorCode = error.code
const errorMessage = error.message
// The email of the user's account used.
const email = error.email
// The firebase.auth.AuthCredential type that was used.
const credential = error.credential
// if error is that there is already an account with that email
if (error.code === 'auth/account-exists-with-different-credential') {
// console.log(errorMessage)
firebaseAuth.fetchProvidersForEmail(email).then(function (providers) {
// If the user has several providers,
// the first provider in the list will be the "recommended" provider to use.
console.log('A account already exists with this email, Use this to sign in: ', providers[0])
if (providers[0] === 'google.com') {
this.setState({ open: true })
}
})
}
})
}
handleRequestClose = () => {
this.setState({
open: false
})
}
// /////////////////////////////////////////////////////////////////////////////////////////////////////
render () {
return (
<div>
<Snackbar
open={this.state.open}
message= 'A account already exists with this email'
autoHideDuration={4000}
onRequestClose={this.handleRequestClose.bind(this)}/>
<Home />
</div>
)
}
}
I was using older function syntax at function (providers) & function (error) . switched them to arrow functions and the outside scope of this went down is defined. :)
Need to load my main component and, in case a localstorage with the pair value "logged: true" exists redirect to "/app" using react-router.
I am using react-redux and this is my code:
class Main extends Component {
componentWillMount(){
// Return true in redux state if localstorage is found
this.props.checkLogStatus();
}
componentDidMount(){
// redirect in case redux state returns logged = true
if(this.props.logStatus.logged){
hashHistory.push('/app');
}
}
render() {
return (
<App centered={true} className="_main">
{this.props.children}
</App>
);
}
}
My redux action:
checkLogStatus() {
// check if user is logged and set it to state
return {
type: LOGIN_STATUS,
payload: window.localStorage.sugarlockLogged === "true"
};
}
But when the component gets to the componentDidMount stage, my redux state has still not been updated.
Y manage to get this to work by using:
componentWillReceiveProps(nextProps){
if (nextProps.logStatus.logged && nextProps.logStatus.logged !== this.props.logStatus.logged){
hashHistory.push('/app');
}
}
But I am not sure it is the most elegant solution.
Thanks in advance!
Using componentWillReceiveProps is the way to go here since your logStatus object is being passed in as a prop that is being changed.
There is a more elegant way to this using the Redux-thunk middleware which allows you to dispatch a function (which receives dispatch as an argument instead of the object actions. You can then wrap that function in a promise and use it in componentWillMount.
In your actions file:
updateReduxStore(data) {
return {
type: LOGIN_STATUS,
payload: data.logInCheck
};
}
validateLocalStorage() {
...
}
checkLogStatus() {
return function(dispatch) {
return new Promise((resolve, reject) => {
validateLocalStorage().then((data) => {
if (JSON.parse(data).length > 0) {
dispatch(updateReduxStore(data));
resolve('valid login');
} else {
reject('invalid login');
}
});
});
};
}
Then in your component:
componentWillMount() {
this.props.checkLogStatus()
.then((message) => {
console.log(message); //valid login
hashHistory.push('/app');
})
.catch((err) => {
console.log(err); //invalid login
});
}
Redux-thunk middleware is made for such use cases.