I am trying to do a unit test to an action in my react application but apparently everything works fine, I get a message that I am not understanding when making the request and the status is undefined, I don't have any specific variable with the status name so I assume it must be a problem when making the promise. How can I solve this problem?
error : undefined | TypeError: Cannot read property 'status' of
undefined
at request (C:\Users\amils\OneDrive\Documentos\Bootcamp - Training\Project\tracking-tool-webapp\src\api\utilities\fetch.js:45:26)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at Object.getAll (C:\Users\amils\OneDrive\Documentos\Bootcamp -
Training\Project\tracking-tool-webapp\src\api\utilities\provider.js:18:9)
console log result:
console.log
[
{
title: 'Candidates',
errorMessages: [],
candidates: [],
reports: null,
loading: false,
programsInProgress: [],
programVersions: [],
statusType: []
},
{ onLoadCandidates: [Function: onLoadCandidates] }
]
The code:
it('Should get all candidates', async () => {
const mockResponse = {
candidates: [
{
id: '4fffc534-1d83-14d5-b264-1e17f2abd322',
name: 'Homer Simpson',
status: 'InProgress',
},
{
id: '4fffc535-1d83-14d5-b264-1e17f2abd322',
name: 'Junior Santos',
status: 'InProgress',
},
],
};
global.fetch = jest.fn(() => {
Promise.resolve({
status: 200,
json: () => Promise.resolve(mockResponse),
});
});
const result = await customRenderHook();
const actions = result.current[1];
console.log(result);
await act(async () => {
actions.onLoadCandidates();
});
const state = result.current[0];
expect(state.candidates).toEqual(mockResponse);
});
code customRenderHook:
const customRenderHook = () => {
const wrapper = ({ children }) => <CandidatesDataProvider>{children}</CandidatesDataProvider>;
const { result } = renderHook(() => useCandidatesContext(), { wrapper });
return result;
};
I find the problem, currently, I cant execure my promise without a tokes 'Bearer', now the problem here is how can I create a mock of token:
function onLoadCandidates(dispatch) {
dispatch({ type: CandidatesActionTypes.loading, payload: true });
const token = localStorage.getItem('token');
apiCandidate
.getAll(token)
.then((response) => {
dispatch({ type: CandidatesActionTypes.loading, payload: response.data });
})
.catch((err) => {
dispatch({ type: CandidatesActionTypes.Error, payload: err.message });
LoggerService.error(err);
})
.finally(() => {
dispatch({ type: CandidatesActionTypes.loading, payload: false });
});
}
You could mock localStorage.getItem to return a token in the format required by apiCandidate.getAll(token):
localStorage.getItem = jest.fn().mockReturnValue('your-token');
Related
I have a thunk which fetches data from firestore and maps doc IDs to each doc. For some reason it's fulfilled and returns undefined before the mapping finishes. I can verify this because the log before return statement appears a second or two after the fulfilled reducer logs.
My thunk:
export const fetchNotesByCustomerId = createAsyncThunk(
'fetchNotesByCustomerId',
async (custId, { getState, dispatch }) => {
const customerId = custId;
if (customerId !== undefined) {
notesService
.fetchNotesByCustomerId(customerId)
.then((snapshot) => {
if (snapshot.docs.length !== 0) {
const notes = snapshot.docs.map(
(doc) =>
({
...doc.data(),
docId: doc.id
} as INote)
);
console.log('notes inside thunk: ', notes); // This shows after the fulfilled reducer logs
return {
error: false,
data: notes
};
} else
return {
error: true,
message: 'No notes found in firebase',
data: []
};
})
.catch((error) => {
return { error: true, message: error.message, data: [] };
});
} else return { error: true, message: 'No Customer Id' };
}
);
I know .map is an async function that returns an array of promises, but when I use await, intellisense notifies me that it makes no difference on the behavior of the function.
So as an alternative, I tried to resolve the array of promises like this, but saw no difference:
.then(async (snapshot) => {
if (snapshot.docs.length !== 0) {
const notesPromisesArray = snapshot.docs.map(
(doc) =>
({
...doc.data(),
docId: doc.id
} as INote)
);
await Promise.all(notesPromisesArray).then((notes) => {
console.log('notes inside thunk: ', notes);
return {
error: false,
data: notes
};
});
} else
return {
error: true,
message: 'No notes found in firebase',
data: []
};
})
How can I get this .map to return before the thunk is fulfilled?
The problem you're facing has nothing to do with Redux Async Thunk but basic JavaScript only.
In the if condition that you have:
if (customerId !== undefined) {
notesService
.fetchNotesByCustomerId(customerId)
.then((snapshot) => {
if (snapshot.docs.length !== 0) {
const notes = snapshot.docs.map(
(doc) =>
({
...doc.data(),
docId: doc.id,
} as INote)
)
console.log('notes inside thunk: ', notes) // This shows after the fulfilled reducer logs
return {
error: false,
data: notes,
}
} else
return {
error: true,
message: 'No notes found in firebase',
data: [],
}
})
.catch((error) => {
return { error: true, message: error.message, data: [] }
})
}
You're using noteService.fetchNotesByCustomerId() which is an async function.
When the execution goes to this block, since JavaScript Event loop will forward the async function to its thread pool, it goes to the next step without even the execution of noteService.fetchNotesByCustomerId() getting over and resolves the thunk without returning anything.
You can easily resolve this by adding a return statement next to your call:
export const fetchNotesByCustomerId = createAsyncThunk('fetchNotesByCustomerId', async (custId, { getState, dispatch }) => {
const customerId = custId
if (customerId !== undefined) {
return notesService
.fetchNotesByCustomerId(customerId)
.then((snapshot) => {
if (snapshot.docs.length !== 0) {
const notes = snapshot.docs.map(
(doc) =>
({
...doc.data(),
docId: doc.id,
} as INote)
)
console.log('notes inside thunk: ', notes) // This shows after the fulfilled reducer logs
return {
error: false,
data: notes,
}
} else
return {
error: true,
message: 'No notes found in firebase',
data: [],
}
})
.catch((error) => {
return { error: true, message: error.message, data: [] }
})
} else return { error: true, message: 'No Customer Id' }
})
How can I catch the errors from async method in the thunk into the Functional Components?
For example I have the following thunk:
export const updateCostCenter = (data: Record<string, unknown>) => async (dispatch: Dispatch<IWorkforceState>) => {
dispatch(requestUpdateCostCenter());
return api('put', `${costCenterUrl}/${data.rowId}`, data)
.then(response => {
return dispatch(receiveUpdateCostCenter(response.data));
})
.catch(err => {
return dispatch(errorUpdateCostCenter(err.response?.data?.description));
});
};
and in the functional component the following asynchronous method that calls the thunk:
props.updateCostCenter(valueToSubmit).then(
() => {
props.showToastNotification('success', 'Successful', props.translate('cost_center_successfully_updated'));
AmplitudeService.logEvent(props.translate('edit_cost_center'));
props.hideDialog();
resetForm();
setSubmitting(false);
if (props.loadData) {
props.loadData();
}
return
}
).catch(() => {
props.showToastNotification('error', 'Error', props.translate('cost_center_update_error'))
});
Unfortunately, I don't know why in case of error it doesn't enter into the catch from the functional component. I tried to add throw TypeError() after the dispatch of the error action, it works, but the thunk unit test fails on the pipeline.
This are the tests:
it('update cost center success', function() {
mockAdd.mockImplementation(
() =>
Promise.resolve({
data: costCenter,
} as any)
);
const expectedActions = [
{ type: WorkforceActions.REQUEST_UPDATE_COST_CENTER },
{ type: WorkforceActions.RECEIVE_UPDATE_COST_CENTER, costCenter },
];
store.dispatch(updateCostCenter({ data: costCenter }) as any).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(api).toHaveBeenCalled();
return
}).catch((unexpectedErr: any) => console.log(`Unexpectedly rejected promise ${unexpectedErr}`));
});
it('update cost center error', function() {
mockAdd.mockImplementation(
() =>
Promise.reject({
response: { data: { description: 'dummy-message' } },
} as any)
);
const expectedActions = [
{ type: WorkforceActions.REQUEST_UPDATE_COST_CENTER },
{ type: WorkforceActions.ERROR_UPDATE_COST_CENTER, message: 'dummy-message' },
];
store.dispatch(updateCostCenter({ data: costCenter }) as any).catch(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(api).toHaveBeenCalled();
});
});
Because you don't return an error.
export const updateCostCenter = (data: Record<string, unknown>) => async (dispatch: Dispatch<IWorkforceState>) => {
dispatch(requestUpdateCostCenter());
return api('put', `${costCenterUrl}/${data.rowId}`, data)
.then(response => {
dispatch(receiveUpdateCostCenter(response.data));
return response;
})
.catch(err => {
dispatch(errorUpdateCostCenter(err.response?.data?.description));
throw err; // or throw new Error();
});
};
I have implemented the socket, which was not able to established after pressing F5 button,
when users logged into the app, socket has been establishes successfully, when I hit F5, page refreshes but socketConnection is not invoked because props is undefined due to which conditional statement has been failed to execute.
After inspecting I have found that, I invoked the main socket creation function inside App.js, it takes props as as argument and one conditional statement. I have found that the props itself is undefined due to which the socket i not able t re-established.
App.js
import React, { useEffect } from 'react';
import './App.scss';
import Routes from './Routes';
import { connect } from 'react-redux';
import socketConnection from '#Hoc/SocketComponent';
const App = () => {
useEffect((props) => {
if (props?.session?.user?.extraDetails?.extensionNo) {
socketConnection(props);
}
}, []);
return (
<div>
<Routes />
</div>
);
};
const mapStateToProps = (state) => ({
session: state.session
});
export default connect(mapStateToProps)(App);
SocketComponent
export default function socketConnection(socketEvent) {
if (!window?.socket)
sessionService.loadUser().then((currentUser) => {
if (
currentUser?.extraDetails?.extensionNo &&
currentUser?.params?.AgentID
)
socket = window.socket = io(REACT_APP_SOCKET_URL, {
query: {
agentId: currentUser.params.AgentID,
extensionNo: currentUser.extraDetails.extensionNo,
},
});
socket.on("connect", (data) => {
console.info(data);
});
socket.on("EventEstablished", (data) => {
eventEstablished(data, currentUser, socketEvent);
});
socket.on("EventAgentLogout", () => {
notification.error({
message: "Softphone loggedout, please re-login",
duration: 0,
});
socketEvent.history.push("/home");
});
socket.on("EventPropertiesChanged", (data) => {
manualGeocode(data);
});
socket.on("EventAttachedDataChanged", (data) => {
if (data.data?.incidentNumber) {
store.dispatch({
type: SET_LIVE_CALL_DATA,
payLoad: { psapReferenceId: data?.data?.incidentNumber },
});
}
if (data.data?.updateLocation && data.data?.isRetransmit) {
let functionCall = "retransmit" ;
// if (data.data?.isRetransmit) {
// functionCall = "retransmit" ;
// }
getCallData( functionCall );
}
// manualGeocode(data);
});
socket.on("EventDestinationBusy", (data) => {
console.log("EventDestinationBusy", data);
});
socket.on("EventAbandoned", (data) => {
notification.error({
message: "Call abandoned",
description: "The caller abandoned the call before it was answered",
duration: 0,
});
});
socket.on("EventDNOutOfService", (data) => {
notification.error({
message: "Extension Out of Service !",
description:
"This extension is out of service and cannot make or receive calls. ",
duration: 0,
});
});
socket.on("EventAgentReady", (data) => {
console.log("EventAgentReady", data);
});
socket.on("EventAgentNotReady", (data) => {
console.log("EventAgentNotReady", data);
});
socket.on("EventReleased", (data) => {
eventReleased(data);
});
socket.on("EventPartyDeleted", (data) => {
eventPartyDeleted(data);
});
socket.on("EventInvite", (data) => {
console.log(data);
if (
!store?.getState()?.secondAgent?.secondAgent?.isSecondAgent &&
socketEvent.history.location.pathname === "/waiting"
) {
eventInvite(data, currentUser, socketEvent);
}
});
socket.on("disconnect", (data) => {
console.log(data);
});
socket.on("workflow", (data) => {
store.dispatch({ type: SET_WORKFLOW_DATA, payLoad: data });
});
socket.on("workflowUpdatedComponent", (data) => {
store.dispatch({ type: SET_WORKFLOW_OPTIONS, payLoad: data });
});
socket.on("gather-info", (data) => {
console.log(data);
if (data?.data?.extensionNo != currentUser.extraDetails.extensionNo) {
store.dispatch({
type: SET_GATHER_INFO,
payLoad: data?.data?.gatherInfo,
});
}
});
socket.on("geoCodeSession", (data) => {
console.log(data);
if (data?.data?.extensionNo != currentUser.extraDetails.extensionNo) {
if (data?.data?.updateLocation) {
store.dispatch({
type: SET_MANUAL_GEOCODE,
payLoad: { updateLocation: true },
});
}
// let timeFrame = new Date(data.data.timestamp);
// timestamp = timeFrame.toLocaleDateString() + ' ' + timeFrame.toLocaleTimeString();
store.dispatch({
type: SET_MANUAL_GEOCODE,
payLoad: {
status: true,
latitude: data?.data?.latitude,
longitude: data?.data?.longitude,
address: data?.data?.address,
country: data?.data?.country,
region: data?.data?.region,
timestamp: data?.data?.timestamp,
},
});
}
});
socket.on("EventSessionInfo", (data) => {
if (data?.data?.extensionNo !== currentUser?.extraDetails.extensionNo) {
if (data.data.sessionStatus === "Over") {
store.dispatch({
type: SET_SECOND_AGENT,
payLoad: { status: "Disconnected", isSecondAgent: false },
});
} else if (
data.data.sessionStatus === "Alive" &&
data.data.agentNameChat
) {
store.dispatch({
type: SET_SECOND_AGENT,
payLoad: {
isSecondAgent: true,
status: "Connected",
anotherAgent: data.data.messageText,
isSecondAgent: true,
},
});
} else if (
data.data.sessionStatus === "Alive" &&
!data.data.messageText.includes("Leaving ChatRoom..") &&
!data.data.messageText.includes("Join Chat Session")
) {
chatStore(data);
}
}
});
});
}
I am not able to figured it out what went wrong, however I am trying to load when props are there but not able to do the same.
PROBLEM
It seems like props are not loaded while execution of `useEffect
Solution might be require some kind of delay to useEffect, so once props properly loaded form localstorage
My comments are dissappearing from my component after didMount() initializes them? It's really strange!
React component:
componentDidMount = (post) => {
const postId = this.props.post.id
console.log('postpreview', postId)
this.props.fetchComments(postId)
console.log('postpreview comments:', this.props.comments)
}
Redux Actions:
export const beginFetchComments = () => ({
type: C.BEGIN_FETCH_COMMENTS,
})
export const fetchCommentsFailed = (error) => ({
type: C.FETCH_COMMENTS_FAILED,
payload: { error },
})
export const fetchCommentsSuccess = (comments) => ({
type: C.FETCH_COMMENTS_SUCCESS,
payload: { comments }
})
export function fetchComments(postId) {
return dispatch => {
dispatch(beginFetchComments());
return fetch(`${api}/posts/${postId}/comments`, { headers })
.then(
res => res.json(),
error => console.log('An error occurred at fetchComments', error)
)
.then(json => {
dispatch(fetchCommentsSuccess(json));
return json;
});
};
}
Redux Reducer (switch case):
case C.BEGIN_FETCH_COMMENTS:
return {
...state,
loading: true,
error: null
};
case C.FETCH_COMMENTS_SUCCESS:
console.log(action.payload.comments);
const comments = _.mapKeys(action.payload.comments)
return {
...state,
loading: false,
comments,
};
The console displays this for the same console.log(), (I can't get my hands on my props!):
(2) [{…}, {…}]0: {id: "894tuq4ut84ut8v4t8wun89g", parentId: "8xf0y6ziyjabvozdd253nd", timestamp: 1468166872634, body: "Hi there! I am a COMMENT.", author: "thingtwo", …}1: {id: "8tu4bsun805n8un48ve89", parentId: "8xf0y6ziyjabvozdd253nd", timestamp: 1469479767190, body: "Comments. Are. Cool.", author: "thingone", …}length: 2__proto__: Array(0)
commentsReducer.js:22 []
I don't know what is the use of mapKeys here but what I would do is do a console.log to see if I'm getting an object and under what key there is a comments array:
case C.FETCH_COMMENTS_SUCCESS:
console.log(action.payload.comments); // is this logging an array?
return {
...state,
loading: false,
comments: action.payload.comments,
};
The bottom code I posted is the console.log - the object appears populated and then rerenders empty
I followed the steps from documentation to test epic.
...
store.dispatch({ type: FETCH_USER });
expect(store.getActions()).toEqual([
{ type: FETCH_USER },
{ type: FETCH_USER_FULFILLED, payload }
]);
...
But I get failed because second action is been received some later like following.
Test failed
Expected value to equal:
[{"type": "FETCH_USER"}, {"type": "FETCH_USER_FULFILLED", "payload": [some]}]
Received:
[{"type": "FETCH_USER"}]
Difference:
- Expected
+ Received
## -1,20 +1,5 ##
Array [
Object {"type": "FETCH_USER"},
Object {"type": "FETCH_USER_FULFILLED", "payload": [some]} ] // this is what should be.
So I think I should know when the dispatch is finished or some like that.
How can I solve this?
I used fetch() and Rx.Observable.fromPromise instead of ajax.getJSON()
Here is my epic.
const fetchUserEpic = (action$) =>
action$
.ofType(FETCH_USER)
.mergeMap(() => {
return Rx.Observable.fromPromise(api.fetchUser())
.map((users) => ({
type: FETCH_USER_FULFILLED,
payload: { users }
}))
.catch((error) => Rx.Observable.of({
type: FETCH_USER_ERROR,
payload: { error }
}))
.takeUntil(action$.ofType(FETCH_USER_CANCELLED))
})
The reason is that promises always resolve on the next microtask so your api.fetchUser() isn't emitting synchronously.
You'll need to either mock it out, use something like Promise.resolve().then(() => expect(store.getActions).toEqual(...) to wait until the next microtask, or you can experiment with testing your epics directly without using redux.
it('Epics with the appropriate input and output of actions', (done) => {
const action$ = ActionsObservable.of({ type: 'SOMETHING' });
somethingEpic(action$, store)
.toArray() // collects everything in an array until our epic completes
.subscribe(actions => {
expect(actions).to.deep.equal([
{ type: 'SOMETHING_FULFILLED' }// whatever actions
]);
done();
});
});
This will be our preferred testing story in the docs when I (or someone else) has time to write them up. So instead of using redux and the middleware in your tests, we just call the epic function directly with our own mocks. Much easier and cleaner.
With that approach, we can leverage the new dependency injection feature of redux-observable: https://redux-observable.js.org/docs/recipes/InjectingDependenciesIntoEpics.html
import { createEpicMiddleware, combineEpics } from 'redux-observable';
import { ajax } from 'rxjs/observable/dom/ajax';
import rootEpic from './somewhere';
const epicMiddleware = createEpicMiddleware(rootEpic, {
dependencies: { getJSON: ajax.getJSON }
});
// Notice the third argument is our injected dependencies!
const fetchUserEpic = (action$, store, { getJSON }) =>
action$.ofType('FETCH_USER')
.mergeMap(() =>
getJSON(`/api/users/${payload}`)
.map(response => ({
type: 'FETCH_USER_FULFILLED',
payload: response
}))
);
import { ActionsObservable } from 'redux-observable';
import { fetchUserEpic } from './somewhere/fetchUserEpic';
const mockResponse = { name: 'Bilbo Baggins' };
const action$ = ActionsObservable.of({ type: 'FETCH_USERS_REQUESTED' });
const store = null; // not needed for this epic
const dependencies = {
getJSON: url => Observable.of(mockResponse)
};
// Adapt this example to your test framework and specific use cases
fetchUserEpic(action$, store, dependencies)
.toArray() // buffers all emitted actions until your Epic naturally completes()
.subscribe(actions => {
assertDeepEqual(actions, [{
type: 'FETCH_USER_FULFILLED',
payload: mockResponse
}]);
});
First, use isomorphic-fetch instead of Observable.ajax for nock support, like this
const fetchSomeData = (api: string, params: FetchDataParams) => {
const request = fetch(`${api}?${stringify(params)}`)
.then(res => res.json());
return Observable.from(request);
};
So my epic is:
const fetchDataEpic: Epic<GateAction, ImGateState> = action$ =>
action$
.ofType(FETCH_MODEL)
.mergeMap((action: FetchModel) =>
fetchDynamicData(action.url, action.params)
.map((payload: FetchedData) => fetchModelSucc(payload.data))
.catch(error => Observable.of(
fetchModelFail(error)
)));
Then, you may need an interval to decide when to finish the test.
describe("epics", () => {
let store: MockStore<{}>;
beforeEach(() => {
store = mockStore();
});
afterEach(() => {
nock.cleanAll();
epicMiddleware.replaceEpic(epic);
});
it("fetch data model succ", () => {
const payload = {
code: 0,
data: someData,
header: {},
msg: "ok"
};
const params = {
data1: 100,
data2: "4"
};
const mock = nock("https://test.com")
.get("/test")
.query(params)
.reply(200, payload);
const go = new Promise((resolve) => {
store.dispatch({
type: FETCH_MODEL,
url: "https://test.com/test",
params
});
let interval: number;
interval = window.setInterval(() => {
if (mock.isDone()) {
clearInterval(interval);
resolve(store.getActions());
}
}, 20);
});
return expect(go).resolves.toEqual([
{
type: FETCH_MODEL,
url: "https://test.com/assignment",
params
},
{
type: FETCH_MODEL_SUCC,
data: somData
}
]);
});
});
enjoy it :)