How to mock out async actions - reactjs

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

Related

How to use a thunk dispatch to create a standard promise that returns `success` or `error`

I have a toast package that receives a standard promise as an argument and does something upon success or error:
toast.promise(
updateNotePromise,
{
loading: 'Saving...',
success: (data: any) => 'Note saved!',
error: (err) => err.toString()
}
);
This is the promise I pass to the toast, but it returns a <PayloadAction> because it calls a thunk:
const updateNotePromise = await dispatch(
updateNoteInFirestore({ note: noteInput, noteDocId: noteProp.docId })
);
How can I return success or error from this dispatch thunk operation?
I thought of processing the returned <PayloadAction> by wrapping the thunk. This would be my naive approach:
const updateNotePromise = async(): Promise<{success: boolean | error: any}> => {
try {
await dispatch(updateNoteInFirestore({ note: noteInput, noteDocId: noteProp.docId
return success }))
}
catch {
(error)=> return error}
Am I on the right track?
Edit: here's the thunk code:
export const updateNoteInFirestore = createAsyncThunk(
'updateNoteInFirestore',
async (
{ note, noteDocId }: { note: string; noteDocId?: string },
{ getState, dispatch }
) => {
const poolState = (getState() as RootState).customerPool.pool;
const userState = (getState() as RootState).user;
const time = Timestamp.now();
const path = noteDocId ? noteDocId : undefined;
const message = note;
if (poolState?.docID) {
await notesService.updateNote(
{
pool: poolState.docID,
customer: userState?.user?.uid ?? 'Undefined Customer',
//we do not update dateFirstCreated
...(path ? { dateLastUpdated: time } : { dateFirstCreated: time }),
dateLastUpdated: time,
message: message,
editHistory: [],
seenByAdmin: false
},
path
);
dispatch(fetchNotesByCustomerId(userState?.user?.uid));
return { error: false };
}
return { error: true };
}
);
If you want to return an error with createAsyncThunk you can use rejectWithValue
const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
const response = await fetch(`https://example.com/api/stuff`)
if (response.status === 404)
return rejectWithValue(new Error("Impossible to do stuff"));
return response.json()
}
)
I think for your use case, it's better to use a promise-based function followed by a dispatch reducer action rather than an asyncThunk.
asyncThunks return value can only be consumed by builders that are defined within slice as far as I know.
You need to break your problem into three steps:
Creating a wrapper promiseFunction as needed by your toast.
Creating a promise helper function where you must be able to supply the variables poolState and userState as these variables were accessed through getState() in your async thunk but that isn't possible in your promiseHelperFunction If you define promiseHelperFunction within your functional component you could use useAppSelector to access those states. I have added the comment for the same in the promiseHelperFunction.
Now once you're done with this you can now consume promiseFunction in your toast.
You might need to import fetchNotesByCustomerId that you're using in your asyncThunk as it may not be accessible to the component where you're writing the toast implementation.
Here's the code for same:
const updateNotePromise = async () => {
return updatePromiseHelperFunction({
note: noteInput,
noteDocId: noteProp.docId,
});
};
const updatePromiseHelperFunction = async ({
note,
noteDocId,
}: {
note: string;
noteDocId?: string;
}) => {
/*
// Before the Promise you must ensure you're able to access these variables:
const poolState = useAppSelector(state => state.customerPool.pool);
const userState = useAppSelector(state => state.user);
*/
const time = Timestamp.now();
const path = noteDocId ? noteDocId : undefined;
if (poolState?.docID) {
await notesService.updateNote(
{
pool: poolState.docID,
customer: userState?.user?.uid ?? 'Undefined Customer',
//we do not update dateFirstCreated
...(path ? { dateLastUpdated: time } : { dateFirstCreated: time }),
dateLastUpdated: time,
message: note,
editHistory: [],
seenByAdmin: false,
},
path
);
dispatch(fetchNotesByCustomerId(userState?.user?.uid));
return { error: false };
}
return { error: true };
};

how to pass parameter to generator function in redux saga from jsx?

I have 3 generator function first is "loginUserStart" where the actual request comes then the second one is "LoginUserAsync" which is called in the "loginUserStart" and third is api call function
so I am trying to pass the parameter from my signin component to the loginUserStart function but whenever I console.log(arguments) it is showing nothing
Code:-
Sign-in component
const login = async () => {
arr.userEmail = "sample_email";
arr.userPassword = "sample_password";
console.log(arr);
signinUserStart(arr);
};
const logSubmit = () => {
login();
};
const mapDispatchToProps = (dispatch) => ({
signinUserStart: (data) => dispatch(signinUserStart(data))
});
Action file code
export const signinUserStart = (data) => ({
type: UserActionTypes.Set_SigninUser_Start,
payload: data
})
saga File code
API generator function code
export async function fetchUser(info) {
console.log(info);
const email = 'Admin#gmail.com'; //sample_email
// const passwords = info.userPassword;
const password = 'Admin#123'; //sample_password
try {
const user = await axios.post("http://localhost:5050/sign", {
data: {
email: email,
password: password,
},
});
console.log(user);
return user;
} catch (error) {
console.log(error);
return error;
}
}
LoginUserAsync function
export function* LoginUserAsync(data) {
console.log("in saga");
console.log(data);
try {
let userInfo = yield call(fetchUser, data)
console.log(userInfo);
yield put(setUserId('62b1c5ee515317d42239066a')); //sample_token
yield put(setCurrentUserName(userInfo.data.userName));
} catch (err) {
console.log(err);
}
}
loginUserStart function
export function* loginUserStart(action) {
console.log(action.payload);//not logging anything for showing in console
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync(action));
}
I can't be sure without seeing more code, but assuming that loginUserStart is either root saga or started from root saga it means there is no action for it to receive.
The main issue I think is this line
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync(action));
In the second parameter you are calling the generator function which is wrong, instead you should be passing the saga itself (as reference).
So it should look like this:
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync);
This way, the Redux Saga library will then call LoginUserAsync when Set_SigninUser_Start is dispatched with first param correctly set to the action object.

Jest async action creators:TypeError: Cannot read property 'then' of undefined

Kinda newbie to jest here.I am trying to write unit test cases for one of the async action creators in my React project using jest. I keep running into the error TypeError: Cannot read property 'then' of undefined
Below is my action creator:
import {loginService} from "./services";
export function login(email: string, password: string): (dispatch: ThunkDispatch<{}, {}, any>) => void {
return dispatch => {
dispatch(loggingIn(true));
loginService(email, password).then(
(response: any) => {
dispatch(loggingIn(false));
dispatch(loginAction(response));
},
error => {
//Code
}
dispatch(loggingIn(false));
dispatch(loginError(true, message));
}
);
};
}
./services.js
export const loginService = (username: string, password: string) => {
const requestOptions = {
method: "POST",
headers: {
//Headers
},
body: JSON.stringify({email: username, password: password})
};
return fetch(`url`, requestOptions)
.then(handleResponse, handleError)
.then((user: any) => {
//code
return user;
});
};
Given below is my test:
it("login", () => {
fetchMock
.postOnce("/users/auth", {
body: JSON.parse('{"email": "user", "password": "password"}'),
headers: {"content-type": "application/json"}
})
.catch(() => {});
const loginPayload = {email: "user", password: "password"};
const expectedSuccessActions = [
{type: types.LOGGING_IN, payload: true},
{type: types.LOGIN, loginPayload}
];
const expectedFailureActions = [
{type: types.LOGGING_IN, payload: true},
{type: types.LOGIN_ERROR, payload: {val: true, errorMessage: "error"}}
];
const store = mockStore({user: {}});
const loginService = jest.fn();
return store.dispatch(LoginActions.login("email", "password")).then(() => {
expect(store.getActions()).toEqual(expectedSuccessActions);
});
});
Please help
The end result returned by dispatching your LoginActions.login() action is void (or undefined). Not a Promise, so not something you can call .then() on in your test.
Judging by your test code, you're using fetch-mock for your fetchMock. You should be able to wait for that to finish before testing that the store dispatched the correct actions:
it("login", async () => {
// ^^^^^ --> note that you need to make your test async to use await
store.dispatch(LoginActions.login("email", "password"));
await fetchMock.flush();
expect(store.getActions()).toEqual(expectedSuccessActions);
});
Note that the comments in your code seem to indicate that your loginService does some more things before returning from the .then() callback. If that takes too long, waiting for the fetchMock to finish might not be waiting long enough. In that case, you should consider returning the Promise from your LoginActions.login() action so that you can test it. Whether or not you should depends on how much effort it would be to adjust your application to handle that, since you don't want your app to crash with any unhandled promise rejection errors in case the login fails.

Redux mock store only returning one action when multiple actions are dispatched

I'm trying to mock this axios call:
export const fetchCountry = (query) => {
return dispatch => {
dispatch(fetchCountryPending());
return axios.get(`${process.env.REACT_APP_API_URL}/api/v1/countries/?search=${query}`)
.then(response => {
const country = response.data;
dispatch(fetchCountryFulfilled(country));
})
.catch(err => {
dispatch(fetchCountryRejected());
dispatch({type: "ADD_ERROR", error: err});
})
}
}
Which on a successful call, should dispatch both action creators fetchCountryPending() and fetchCountryFullfilled(country). When I mock it like so:
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
// Async action tests
describe('country async actions', () => {
let store;
let mock;
beforeEach(function () {
mock = new MockAdapter(axios)
store = mockStore({ country: [], fetching: false, fetched: true })
});
afterEach(function () {
mock.restore();
store.clearActions();
});
it('dispatches FETCH_COUNTRY_FULFILLED after axios request', () => {
const query = 'Aland Islands'
mock.onGet(`${process.env.REACT_APP_API_URL}/api/v1/countries/?search=${query}`).replyOnce(200, country)
store.dispatch(countryActions.fetchCountry(query))
const actions = store.getActions()
console.log(actions)
expect(actions[0]).toEqual(countryActions.fetchCountryPending())
expect(actions[1]).toEqual(countryActions.fetchCountryFulfilled(country))
});
});
The second expect fails and console.log(actions) only shows an array with the one action, but it should contain both actions, fetchCountryPending and fetchCountrySuccess. When I log ('dispatched'), it shows the second action is getting dispatched in the terminal.
Can you try making your it block async and dispatch the action. I believe the tests are running before your get requests return the value
I couldn't get a then(() => {}) block to work but I was able to await the function and make it async:
it('dispatches FETCH_COUNTRY_FULFILLED after axios request', async () => {
const query = 'Aland Islands'
mock.onGet(`${process.env.REACT_APP_API_URL}/api/v1/countries/?search=${query}`).replyOnce(200, country)
await store.dispatch(countryActions.fetchCountry(query))
const actions = store.getActions()
console.log(actions)
expect(actions[0]).toEqual(countryActions.fetchCountryPending())
expect(actions[1]).toEqual(countryActions.fetchCountryFulfilled(country))
});
});

Testing Observable epic which invokes other epic

I am trying to test a Redux Observable epic which dispatches an action to invoke an other epic. Somehow the invoked epic is not called.
Lets say my epics looks like this;
const getJwtEpic = (action$, store, { api }) =>
action$.ofType('GET_JWT_REQUEST')
.switchMap(() => api.getJWT()
.map(response => {
if (response.errorCode > 0) {
return {
type: 'GET_JWT_FAILURE',
error: { code: response.errorCode, message: response.errorMessage },
};
}
return {
type: 'GET_JWT_SUCCESS',
idToken: response.id_token,
};
})
);
const bootstrapEpic = (action$, store, { api }) =>
action$.ofType('BOOTSTRAP')
.switchMap(() =>
action$.filter(({ type }) => ['GET_JWT_SUCCESS', 'GET_JWT_FAILURE'].indexOf(type) !== -1)
.take(1)
.mergeMap(action => {
if (action.type === 'GET_JWT_FAILURE') {
return Observable.of({ type: 'BOOTSTRAP_FAILURE' });
}
return api.getProfileInfo()
.map(({ profile }) => ({
type: 'BOOTSTRAP_SUCCESS', profile,
}));
})
.startWith({ type: 'GET_JWT_REQUEST' })
);
When I try to test the bootstrapEpic in Jest with the following code;
const response = {};
const api = { getJWT: jest.fn() };
api.getJWT.mockReturnValue(Promise.resolve(response));
const action$ = ActionsObservable.of(actions.bootstrap());
const epic$ = epics.bootstrapEpic(action$, null, { api });
const result = await epic$.toArray().toPromise();
console.log(result);
The console.log call gives me the following output;
[ { type: 'GET_JWT_REQUEST' } ]
Somehow the getJwtEpic isn't called at all. I guess it has something to do with the action$ observable not dispatching the GET_JWT_REQUEST action but I can't figure out why. All help is so welcome!
Assuming actions.rehydrate() returns an action of type BOOTSTRAP and the gigya stuff is a typo,
getJwtEpic isn't called because you didn't call it yourself 🤡 When you test epics by manually calling them, then it's just a function which returns an Observable, without any knowledge of the middleware or anything else. The plumbing that connects getJwtEpic as part of the root epic, and provides it with (action$, store) is part of the middleware, which you're not using in your test.
This is the right approach, testing them in isolation, without redux/middleware. 👍 So you test each epic individually, by providing it actions and dependencies, then asserting on the actions it emits and the dependencies it calls.
You'll test the success path something like this:
const api = {
getProfileInfo: () => Observable.of({ profile: 'mock profile' })
};
const action$ = ActionsObservable.of(
actions.rehydrate(),
{ type: 'GET_JWT_SUCCESS', idToken: 'mock token' }
);
const epic$ = epics.bootstrapEpic(action$, null, { api });
const result = await epic$.toArray().toPromise();
expect(result).toEqual([
{ type: 'GET_JWT_REQUEST' },
{ type: 'BOOTSTRAP_SUCCESS', profile: 'mock profile' }
]);
Then you'll test the failure path in another test by doing the same thing except giving it GET_JWT_FAILURE instead of GET_JWT_SUCCESS. You can then test getJwtEpic separately as well.
btw, ofType accepts any number of types, so you can just do action$.ofType('GET_JWT_SUCCESS', 'GET_JWT_FAILURE')

Resources