So this is the scenario / premises:
In order to populate a chat queue in real time I need to open a connection to a websocket, send a message and then set the data to a websocket store. This store will basically manage all the websocket state.
Before populating the chat queue there's two parameters I need: a shiftId coming from one http API request and a connectionId coming from the websocket. Using those two parameters I finally can subscribe to a third http API and start receiving messages to populate the chat queue.
The problem is that due to the async behaviour of the websocket (or that's what I think, please feel to correct me if I'm wrong) I always get an empty "connectionId" when trying to make the put to that "subscription" API. I have tried with async/await and promises but nothing seems to work. I'm pretty new to async/await and websockets with Vuex so pretty sure I'm doing something wrong.
This is the user vuex module where I do all the login/token operations and dispatch a "updateEventsSubscription" action from the shift vuex module. In order for the "updateEventsSubscription" action to work I need to get the response from the "processWebsocket" action (to get the connectionId parameter) and from the "startShift" action (to get the shiftId parameter) coming from the shifts vuex module:
import UserService from '#/services/UserService.js'
import TokenService from '#/services/TokenService.js'
import router from '#/router'
export const namespaced = true
export const state = {
accessToken: '',
errorMessage: '',
errorState: false,
userEmail: localStorage.getItem('userEmail'),
userPassword: localStorage.getItem('userPassword'),
}
export const mutations = {
SET_TOKEN(state, accessToken) {
state.accessToken = accessToken
TokenService.saveToken(accessToken)
},
SET_USER(state, authUserJson) {
state.userEmail = authUserJson.email
state.userPassword = authUserJson.password
localStorage.setItem('userPassword', authUserJson.password)
localStorage.setItem('userEmail', authUserJson.email)
},
SET_ERROR(state, error) {
state.errorState = true
state.errorMessage = error.data.error_description
},
CLOSE_NOTIFICATION(state, newErrorState) {
state.errorState = newErrorState
},
}
export const actions = {
signIn({ commit, dispatch, rootState }, authUserJson) {
return UserService.authUser(authUserJson)
.then((result) => {
commit('SET_USER', authUserJson)
commit('SET_TOKEN', result.data.access_token)
dispatch('token/decodeToken', result.data.access_token, {
root: true,
})
dispatch(
'shifts/updateEventsSubscription',
rootState.token.agentId,
{
root: true,
}
)
router.push('/support')
})
.catch((error) => {
console.log(error)
if (error.response.status === 400) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
commit('SET_ERROR', error.response)
} else {
console.log(error.response)
}
})
},
signOut({ commit }) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
localStorage.removeItem('userPassword')
localStorage.removeItem('userEmail')
TokenService.removeToken()
router.push('/')
},
closeNotification({ commit }, newErrorState) {
commit('CLOSE_NOTIFICATION', newErrorState)
},
}
export const getters = {
getToken: (state) => {
return state.accessToken
},
errorState: (state) => {
return state.errorState
},
errorMessage: (state) => {
return state.errorMessage
},
isAuthenticated: (state) => {
return state.accessToken
},
userEmail: (state) => {
return state.userEmail
},
userPassword: (state) => {
return state.userPassword
},
}
This is websocket store: I pass the connectionId to the state in order to be able to use it in another vuex action to subscribe for new chats:
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
console.log(`We got chats !!`)
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
And finally this is the shift store (where the problem is), as you can see I have a startShift action (everything works fine with it) and then the "updateEventsSubscription" where I'm trying to wait for the response from the "startShift" action and the "processWebsocket" action. Debugging the app I realize that everything works fine with the startShift action but the websocket action sends the response after the "updateEventsSubscription" needs it causing an error when I try to make a put to that API (because it needs the connectionId parameter coming from the state of the websocket).
import ShiftService from '#/services/ShiftService.js'
export const namespaced = true
export const state = {
connectionId: '',
shiftId: '',
agentShiftInfo: '{}',
}
export const actions = {
startShift({ commit }, agentId) {
return ShiftService.startShift(agentId)
.then((response) => {
if (response.status === 200) {
commit('START_SHIFT', response.data.aggregateId)
}
})
.catch((error) => {
console.log(error)
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
async updateEventsSubscription({ dispatch, commit, rootState }, agentId) {
await dispatch('startShift', agentId)
const shiftId = state.shiftId
await dispatch('websocket/processWebsocket', null, { root: true })
let agentShiftInfo = {
aggregateId: state.shiftId,
connectionId: rootState.websocket.connectionId,
}
console.log(agentShiftInfo)
return ShiftService.updateEventsSubscription(shiftId, agentShiftInfo)
.then((response) => {
commit('UPDATE_EVENTS_SUBSCRIPTION', response.data)
})
.catch((error) => {
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
}
export const mutations = {
START_SHIFT(state, shiftId) {
state.shiftId = shiftId
console.log(`Retrieving Shift ID: ${state.shiftId}`)
},
UPDATE_EVENTS_SUBSCRIPTION(state, agentShiftInfo) {
state.agentShiftInfo = agentShiftInfo
},
}
You should convert your WebSocket action into a promise that resolves when WebSocket is connected.:
export const actions = {
processWebsocket({ commit }) {
return new Promise(resolve=> {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
resolve();
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
});
},
}
So I realized that I have to resolve the promise on the this.ws.message instead. By doing that all my data is populated accordingly, there's still sync issues (I can't feed the websocket state at the moment because due to its async behaviour the state is not there yet when other components try to use it via: rootGetters.websocket.incomingChats for example) but I guess that's part of another question. Here's the final version of the module action:
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://ws.rubiko.io')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
Anyways, thanks #Eldar you were in the right path.
I am using Jest and moxios to write a test for my async function:
export function getData(id) {
return dispatch => {
return axios({
method: "get",
url: `${'url'}/id`
})
.then(response => {
dispatch(setData(response.data));
})
.catch(() => alert('Could not fetch data');
};
}
Test:
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import moxios from "moxios";
import getData from '../redux/getData';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const store = mockStore({});
describe('Test fetch data', () => {
beforeEach(function() {
moxios.install();
store.clearActions();
});
afterEach(function() {
moxios.uninstall();
});
it('should fetch data and set it', () => {
const data = [{ name: 'John', profession: 'developer'}];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: data
});
const expectedActions = [setData(data)];
return store.dispatch(getData()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
})
})
My test is passing, but when I check the code coverage report generated by Jest, it shows that the then block of getData was not covered/called. How can I fix this?
moxios.wait return Promise your test function return before running except functions.
you need to use done callback in your test function
it('should fetch data and set it', (done) => {
const data = [{
name: 'John',
profession: 'developer'
}];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: data
});
const expectedActions = [setData(data)];
store.dispatch(getData()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
done();
});
});
});
jest.mock('../redux/storeConfigure', () => () => ({
getState: () => ({ auth: { token: 'TEST_TOKEN' } })
}))
import { prepareRequestHeaders } from './fetch'
describe('prepareRequestHeaders', () => {
it('returns header with Authorization if token is set', () => {
expect(prepareRequestHeaders()['Authorization']).toBe('Bearer TEST_TOKEN')
})
it('returns header without Authorization if token is not set', () => {
?????
})
})
In prepareRequestHeaders I import ../redux/storeConfigure
How to remock ../redux/storeConfigure with other implementation?
EDIT:
/* fetch.js */
import { store } from '../app'
export const prepareRequestHeaders = () => {
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json'
}
const { token } = store.getState().auth
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
return headers
}
/* app.js */
import storeConfigure from './redux/storeConfigure'
export const store = storeConfigure()
/* Directories strucutre */
- redux
- storeConfigure.js
- api
- fetch.js
- api.test.js
app.js
Something like this should work out:
let mockGetState;
jest.mock('../redux/storeConfigure', () => () => {
mockGetState = jest.fn().mockReturnValue({ auth: { token: 'TEST_TOKEN' } })
return { getState: mockGetState }
})
import { prepareRequestHeaders } from './fetch'
describe('prepareRequestHeaders', () => {
it('returns header with Authorization if token is set', () => {
expect(prepareRequestHeaders()['Authorization']).toBe('Bearer TEST_TOKEN')
})
it('returns header without Authorization if token is not set', () => {
mockGetState.mockReturnValueOnce({auth: {token: 'SOMETHING_ELSE'}})
// make your assertion
})
})
When you assign to a Jest mock (jest.fn()) you can change its return value whenever you want. The name of it has to start with mock because of mock hoisting
I'm having trouble getting the correct output from an async redux action. I am using Jest, redux-mock-adapter, and thunk as the tools.
According to redux's documentation on testing async thunks (https://redux.js.org/docs/recipes/WritingTests.html#async-action-creators), my tests should be returning an array of two actions. However, my test is only returning the first action, and not the second one that should return on a successful fetch. I think I'm just missing something small here, but it has been bothersome to say the least.
Redux Action
export const getRemoveFileMetrics = cacheKey => dispatch => {
dispatch({ type: IS_FETCHING_DELETE_METRICS });
return axios
.get("GetRemoveFileMetrics", { params: { cacheKey } })
.then(response => dispatch({ type: GET_REMOVE_FILE_METRICS, payload: response.data }))
.catch(err => err);
};
Test
it("getRemoveFileMetrics() should dispatch GET_REMOVE_FILE_METRICS on successful fetch", () => {
const store = mockStore({});
const cacheKey = "abc123doremi";
const removeFileMetrics = {
cacheKey,
duplicateFileCount: 3,
uniqueFileCount: 12,
};
const expectedActions = [
{
type: MOA.IS_FETCHING_DELETE_METRICS,
},
{
type: MOA.GET_REMOVE_FILE_METRICS,
payload: removeFileMetrics,
}
];
mockRequest.onGet(`/GetRemoveFileMetrics?cacheKey=${cacheKey}`).reply(200, removeFileMetrics);
return store.dispatch(MOA.getRemoveFileMetrics(cacheKey)).then(() => {
const returnedActions = store.getActions();
expect(returnedActions).toEqual(expectedActions);
});
});
The Output
Expected value to equal:
[{ "type": "IS_FETCHING_DELETE_METRICS" }, { "payload": { "cacheKey": "abc123doremi", "duplicateFileCount": 3, "uniqueFileCount": 12 }, "type": "GET_REMOVE_FILE_METRICS" }]
Received:
[{ "type": "IS_FETCHING_DELETE_METRICS" }]
I am using jest-fetch-mock and no axios. The following is working for me with the actions. You could refactor to async await as first step. For me it only worked that way.
I am now trying to figure out how to test the side effect (showErrorAlert(jsonResponse);). If I mock out the showErrorAlert implementation at the top of the test file (commented out in my example) then I get the same problem just like you. Actions that uses fetch won't get triggered for some reason.
export const submitTeammateInvitation = (data) => {
const config = {
//.....
};
return async (dispatch) => {
dispatch(submitTeammateInvitationRequest());
try {
const response = await fetch(inviteTeammateEndpoint, config);
const jsonResponse = await response.json();
if (!response.ok) {
showErrorAlert(jsonResponse);
dispatch(submitTeammateInvitationError(jsonResponse));
throw new Error(response.statusText);
}
dispatch(submitTeammateInvitationSuccess());
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.log('Request failed', error);
}
}
};
};
test
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
// jest.mock('../../../../_helpers/alerts', ()=> ({ showAlertError: jest.fn() }));
const middlewares = [thunk];
const createMockStore = configureMockStore(middlewares);
......
it('dispatches the correct actions on a failed fetch request', () => {
fetch.mockResponse(
JSON.stringify(error),
{ status: 500, statusText: 'Internal Server Error' }
);
const store = createMockStore({});
const expectedActions = [
{
type: 'SUBMIT_TEAMMATE_INVITATION_REQUEST',
},
{
type: 'SUBMIT_TEAMMATE_INVITATION_FAILURE',
payload: { error }
}
];
return store.dispatch(submitTeammateInvitation(data))
.then(() => {
// expect(alerts.showAlertError).toBeCalled();
expect(store.getActions()).toEqual(expectedActions);
});
});
I have a function that successfully makes requests, but am having trouble translating it to the React-Redux data flow because I can't figure out how to return the object that is received in the callback.
What I currently have doesn't work, but I am confident it makes the request successfully because I can see it when I console.log it.
import FBSDK, { AccessToken, GraphRequest, GraphRequestManager } from 'react-native-fbsdk'
export const callback = (error, result) => {
if (error) {
console.log(error);
return error
} else {
console.log(result);
return result;
}
}
export const graphRequestFor = fields => {
AccessToken.getCurrentAccessToken()
.then(token => {
console.log(token);
const request = new GraphRequest(
'/me',
{
accessToken: token.accessToken,
parameters: {
fields: {
string: fields
}
}
}, callback
)
new GraphRequestManager().addRequest(request).start()
})
}
export const login = () => {
graphRequestFor('first_name, last_name')
.then(results => {
return results
}) //undefined
}
Not an answer to my direct question, but a solution to my problem is to make the api call directly inside the action and dispatching the object from the callback
import FBSDK, { AccessToken, GraphRequest, GraphRequestManager } from 'react-native-fbsdk'
export const login = params => dispatch => {
AccessToken.getCurrentAccessToken()
.then(token => {
const request = new GraphRequest(
'/me',
{
accessToken: token.accessToken,
parameters: {
fields: {
string: params
}
}
}, (error, result) => {
if (error) {
console.log(error);
return dispatch(receiveCurrentUser(error))
} else {
return dispatch(receiveCurrentUser(result))
}
}
)
new GraphRequestManager().addRequest(request).start()
})
}