I'm using redux for a react app, with a thunk middleware.
I have this method:
export function login(username, password) {
return function (dispatch, getState) {
dispatch(loginRequest());
return sessionService.login(dispatch, username, password).then(redirectTo => {
dispatch(handleSuccessfulAuthentication(redirectTo));
}).catch(error => {...}
};
}
So I'm nocking the result of the service, and get the actions ok. But I want to test I'm dispatching the method handleSuccessfulAuthentication but spying on that method does not seems to work, it always return it was not called (besides I see the action returned)
This is basically my test:
describe('WHEN logging in from sessionExpired', () => {
let action;
beforeAll(() => {
...
testHelper.getUnAuthenticatedNock()
.post(`/${endpoints.LOGIN}`, credentials)
.reply(200, {
...loginResponse
});
action = sinon.spy(sessionActions, 'handleSuccessfulAuthentication');
return store.dispatch(sessionActions.login(credentials.username, credentials.password))
}
it('Should redirect to previous visited page', () => {
console.log(action.called); // false
});
You need to call the exported function (please note we're calling handleSuccessfulAuthentication bound to exports):
export function login(username, password) {
return function (dispatch, getState) {
dispatch(loginRequest());
return sessionService.login(dispatch, username, password).then(redirectTo => {
dispatch(exports.handleSuccessfulAuthentication(redirectTo));
}).catch(error => {...}
};
}
And then your test:
it('Should redirect to previous visited page', () => {
console.log(action.called); // false
});
It's a weird issue where you have to call the function bound to exports. More information found here: https://github.com/facebook/jest/issues/936#issuecomment-214939935
Related
I'm working on a React Native app. I have a signup screen which has a button, onclick:
const handleClick = (country: string, number: string): void => {
dispatch(registerUser({ country, number }))
.then(function (response) {
console.log("here", response);
navigation.navigate(AuthRoutes.Confirm);
})
.catch(function (e) {
console.log('rejected', e);
});
};
The registerUser function:
export const registerUser = createAsyncThunk(
'user/register',
async ({ country, number }: loginDataType, { rejectWithValue }) => {
try {
const response = await bdzApi.post('/register', { country, number });
return response.data;
} catch (err) {
console.log(err);
return rejectWithValue(err.message);
}
},
);
I have one of my extraReducers that is indeed called, proving that it's effectively rejected.
.addCase(registerUser.rejected, (state, {meta,payload,error }) => {
state.loginState = 'denied';
console.log(`nope : ${JSON.stringify(payload)}`);
})
But the signup component gets processed normally, logging "here" and navigating to the Confirm screen. Why is that?
A thunk created with createAsyncThunk will always resolve but if you want to catch it in the function that dispatches the thunk you have to use unwrapResults.
The thunks generated by createAsyncThunk will always return a resolved promise with either the fulfilled action object or rejected action object inside, as appropriate.
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an unwrapResult function that can be used to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:
import { unwrapResult } from '#reduxjs/toolkit'
// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then(originalPromiseResult => {})
.catch(rejectedValueOrSerializedError => {})
}
So, I have this login() action that redirects the user to the feed if the login is successful. I also have this register() action that creates a new user and calls the login() action after.
The problem is that login() isn't receiving the props when called from register(), so I can't call the this.navigation.navigate('Feed') from there.
userActions.js
function register(user) {
return dispatch => {
dispatch(request(user));
let { username, password } = user
userService.register(user)
.then(
user => {
dispatch(success(user));
dispatch(login(username, password)) //calls login() after the user is created
},
error => {
dispatch(failure(error.toString()));
}
);
};
function request(user) { return { type: userConstants.REGISTER_REQUEST, user } }
function success(user) { return { type: userConstants.REGISTER_SUCCESS, user } }
function failure(error) { return { type: userConstants.REGISTER_FAILURE, error } }
}
function login(username, password) {
return dispatch => {
dispatch(request({ username }));
userService.login(username, password)
.then(
user => {
dispatch(success(user));
this.navigation.navigate('Feed') //this is throwing "undefined is not an object" when login() is dispatched from register()
},
error => {
dispatch(failure(error.toString()));
}
);
};
function request(user) { return { type: userConstants.LOGIN_REQUEST, user } }
function success(user) { return { type: userConstants.LOGIN_SUCCESS, user } }
function failure(error) { return { type: userConstants.LOGIN_FAILURE, error } }
}
What am I missing here?
I think your code needs to be fixed a little.
It's not good solution to deal navigation in redux actions. To avoid this, Saga is recommended to be used with Redux.
However, with this your code, you didn't pass navigation property to action so you need to pass props variable to register action first.
Please try like this.
function register(user, props) {
...
dispatch(login(username, password, props))
...
function login(username, password, props) {
...
props.navigation.navigate('Feed')
Then navigation will work.
Hope this helps you to understand)
I recommend to use Redux and Saga as a stack according to my experience.
I'm trying to test this function:
function login(username, password) {
let user = { userName: username, password: password };
return dispatch => {
localStorageService.login(username, password).then((response) => {
dispatch(resetError());
dispatch(success( { type: userConstants.LOGIN, user} ));
}, (err) => {
dispatch(error(err));
});
};
function success(user) { return { type: userConstants.LOGIN, payload: user } };
};
Here is my test
const mockStore = configureStore([thunk]);
const initialState = {
userReducer: {
loggedInUser: "",
users: [],
error: ""
}
};
const store = mockStore(initialState);
jest.mock('./../../services/localStorageService');
describe("Login action should call localstorage login", () => {
let localStorage_spy = jest.spyOn(localStorageService, 'login');
store.dispatch(userActions.login(test_data.username, test_data.password)()).then( () => {
expect(localStorage_spy).toHaveBeenCalled();
});
});
The error I get:
Actions must be plain objects. Use custom middleware for async actions.
A lot of resources online keep telling me to use thunk in my test for these actions but it's not working. The last thing it calls is dispatch(resetError()); and it breaks. I've never really found a resource online which is similar enough to my problem. My function returns a dispatch which returns a promise which returns another dispatch when the promise resolves. I'm just trying to get the function to return. I've put a spy on localStorageService.login and also mocked it out and I have an expect to make sure it was called. But of course the function is not returning
I am trying to mock my api call with jest but for some reason it's not working. I don't really understand why. Anyone has an idea?
(the test keep call the original api call function and not the mock)
my test.js
import { getStuff } from '../stuff';
import * as api from '../../util/api';
describe('Action getStuff', () => {
it('Should call the API to get stuff.', () => {
api.call = jest.fn();
getStuff('slug')(() => {}, () => {});
expect(api.call).toBeCalled();
jest.unmock('../../util/api.js');
});
});
stuff.js redux action
import api from '#util/api';
import { STUFF, API } from '../constant';
export const getStuff = slug => (dispatch, getState) => {
const state = getState();
api.call(API.STUFF.GET, (err, body) => {
if (err) {
console.error(err.message);
} else {
dispatch({
type: STUFF.GET,
results: body,
});
}
}, {
params: { slug },
state
});
};
The import are immutable so it won't work, what you should is mock the whole module. Either with a __mock__ directory or simply with:
jest.mock('../../util/api');
const { call } = require('../../util/api');
call.mockImplementation( () => console.log("some api call"));
I am in the process of trying to setup redux, react-redux and redux-thunk. Thinks are generally going well but I have a question about how things are supposed to look when chaining multiple async actions together.
Specifically I have a scenario where the actions can be invoked individually or indirectly by another action which can invoke them. My question is how should selectItem be authored if I want to be idiomatic?
action.js
export function fetchByContext(contextId) {
return dispatch => {
_fetchByContext(messages => {
dispatch({ type: RECEIVE_MESSAGES, ... });
});
};
};
export function subscribeByContext(contextId) {
return dispatch => {
_subscribeByContext(messages => {
dispatch({ type: RECEIVE_MESSAGES, ... });
});
};
};
export function selectItem(contextId) {
return dispatch => {
subscribeByContext(contextId)(dispatch);
fetchByContext(contextId)(dispatch);
};
};
I believe the key is that (ref):
Any return value from the inner function will be available as the return value of dispatch itself
If the inner functions of fetchByContext(), subscribeByContext() return a promise, these could be chained in series or run in parallel from selectItem(). An untested implementation, assuming neither _fetchByContext() nor _subscribeByContext() return a promise would be:
export function fetchByContext(contextId) {
return dispatch => {
return new Promise((resolve, reject) => {
_fetchByContext(messages => {
dispatch({ type: RECEIVE_MESSAGES, ... });
resolve(messages);
});
});
};
};
export function subscribeByContext(contextId) {
return dispatch => {
return new Promise((resolve, reject) => {
_subscribeByContext(messages => {
dispatch({ type: RECEIVE_MESSAGES, ... });
resolve(messages);
});
});
};
};
export function selectItem(contextId) {
return dispatch => {
// CALL IN SERIES
return dispatch(subscribeByContext(contextId))
.then(() => dispatch(fetchByContext(contextId)));
// CALL IN PARALLEL (alternative to the code above; this is probably not what you want - just keeping for reference)
return Promise.all(
dispatch(subscribeByContext(contextId)),
dispatch(fetchByContext(contextId))
);
};
}
Again please note the code above is untested, just in hope of giving an idea for the general solution.