Vuex: Testing actions with API calls - javascript

I have been following these testing guidelines to test my vuex store.
But when I touched upon the actions part, I felt there is a lot going on that I couldn't understand.
The first part goes like:
// actions.js
import shop from '../api/shop'
export const getAllProducts = ({ commit }) => {
commit('REQUEST_PRODUCTS')
shop.getProducts(products => {
commit('RECEIVE_PRODUCTS', products)
})
}
// actions.spec.js
// use require syntax for inline loaders.
// with inject-loader, this returns a module factory
// that allows us to inject mocked dependencies.
import { expect } from 'chai'
const actionsInjector = require('inject!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
I infer that this is to mock the service inside the action.
The part which follows is:
// helper for testing action with expected mutations
const testAction = (action, payload, state, expectedMutations, done) => {
let count = 0
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count]
expect(mutation.type).to.equal(type)
if (payload) {
expect(mutation.payload).to.deep.equal(payload)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// call the action with mocked store and arguments
action({ commit, state }, payload)
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}
describe('actions', () => {
it('getAllProducts', done => {
testAction(actions.getAllProducts, null, {}, [
{ type: 'REQUEST_PRODUCTS' },
{ type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } }
], done)
})
})
This is where it I find it difficult to follow.
My store looks like:
import * as NameSpace from '../NameSpace'
import { ParseService } from '../../Services/parse'
const state = {
[NameSpace.AUTH_STATE]: {
auth: {},
error: null
}
}
const getters = {
[NameSpace.AUTH_GETTER]: state => {
return state[NameSpace.AUTH_STATE]
}
}
const mutations = {
[NameSpace.AUTH_MUTATION]: (state, payload) => {
state[NameSpace.AUTH_STATE] = payload
}
}
const actions = {
[NameSpace.ASYNC_AUTH_ACTION]: ({ commit }, payload) => {
ParseService.login(payload.username, payload.password)
.then((user) => {
commit(NameSpace.AUTH_MUTATION, {auth: user, error: null})
})
.catch((error) => {
commit(NameSpace.AUTH_MUTATION, {auth: [], error: error})
})
}
}
export default {
state,
getters,
mutations,
actions
}
And This is how I am trying to test:
import * as NameSpace from 'src/store/NameSpace'
import AuthStore from 'src/store/modules/authorization'
const actionsInjector = require('inject!../../../../../src/store/modules/authorization')
// This file is present at: test/unit/specs/store/modules/authorization.spec.js
// src and test are siblings
describe('AuthStore Actions', () => {
const injectedAction = actionsInjector({
'../../Services/parse': {
login (username, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({})
} else {
reject({})
}
}, 300)
})
}
}
})
it('Gets the user profile if the username and password matches', () => {
const testAction = (action, payload, state, mutations, done) => {
const commit = (payload) => {
if (payload) {
expect(mutations.payload).to.deep.equal(payload)
}
}
action({ commit, state }, payload)
.then(result => {
expect(state).to.deep.equal({auth: result, error: null})
})
.catch(error => {
expect(state).to.deep.equal({auth: [], error: error})
})
}
testAction(injectedAction.login, null, {}, [])
})
})
If I try to do this, I get:
"Gets the user profile if the username and password matches"
undefined is not a constructor (evaluating 'action({ commit: commit, state: state }, payload)')
"testAction#webpack:///test/unit/specs/store/modules/authorization.spec.js:96:13 <- index.js:26198:14
webpack:///test/unit/specs/store/modules/authorization.spec.js:104:15 <- index.js:26204:16"
I need help understanding what am I supposed to do to test such actions.

I know it's been awhile but I came across this question because I was having a similar problem. If you were to console.log injectedActions right before you make the testAction call you'd see that the injectedAction object actually looks like:
Object{default: Object{FUNC_NAME: function FUNC_NAME(_ref) { ... }}}
So the main solution here would be changing the testAction call to:
testAction(injectedAction.default.login, null, {}, [], done)
because you are exporting your action as defaults in your store.
A few other issues that are unrelated to your particular error... You do not need to manipulate the testAction boilerplate code. It will work as expected so long as you pass in the proper parameters. Also, be sure to pass done to testAction or your test will timeout. Hope this helps somebody else who comes across this!

Related

How to update RTK Query cache when Firebase RTDB change event fired (update, write, create, delete)

I am using redux-tookit, rtk-query (for querying other api's and not just Firebase) and Firebase (for authentication and db).
The code below works just fine for retrieving and caching the data but I wish to take advantage of both rtk-query caching as well as Firebase event subscribing, so that when ever a change is made in the DB (from any source even directly in firebase console) the cache is updated.
I have tried both updateQueryCache and invalidateTags but so far I am not able to find an ideal approach that works.
Any assistance in pointing me in the right direction would be greatly appreciated.
// firebase.ts
export const onRead = (
collection: string,
callback: (snapshort: DataSnapshot) => void,
options: ListenOptions = { onlyOnce: false }
) => onValue(ref(db, collection), callback, options);
export async function getCollection<T>(
collection: string,
onlyOnce: boolean = false
): Promise<T> {
let timeout: NodeJS.Timeout;
return new Promise<T>((resolve, reject) => {
timeout = setTimeout(() => reject('Request timed out!'), ASYNC_TIMEOUT);
onRead(collection, (snapshot) => resolve(snapshot.val()), { onlyOnce });
}).finally(() => clearTimeout(timeout));
}
// awards.ts
const awards = dbApi
.enhanceEndpoints({ addTagTypes: ['Themes'] })
.injectEndpoints({
endpoints: (builder) => ({
getThemes: builder.query<ThemeData[], void>({
async queryFn(arg, api) {
try {
const { auth } = api.getState() as RootState;
const programme = auth.user?.unit.guidingProgramme!;
const path = `/themes/${programme}`;
const themes = await getCollection<ThemeData[]>(path, true);
return { data: themes };
} catch (error) {
return { error: error as FirebaseError };
}
},
providesTags: ['Themes'],
keepUnusedDataFor: 1000 * 60
}),
getTheme: builder.query<ThemeData, string | undefined>({
async queryFn(slug, api) {
try {
const initiate = awards.endpoints.getThemes.initiate;
const getThemes = api.dispatch(initiate());
const { data } = (await getThemes) as ApiResponse<ThemeData[]>;
const name = slug
?.split('-')
.map(
(value) =>
value.substring(0, 1).toUpperCase() +
value.substring(1).toLowerCase()
)
.join(' ');
return { data: data?.find((theme) => theme.name === name) };
} catch (error) {
return { error: error as FirebaseError };
}
},
keepUnusedDataFor: 0
})
})
});

Param Redux data not populating in store correctly

I am having trouble with my Redux store in cases where I am passing params through a Thunk action. In cases were there is no param, my store is populating correctly. The action is completing successfully and I can see that the data has been returned to the front end by the success / fulfilled message of my action but there is no sign of it going into the store as state.
I had an instance previously where the array list was named incorrectly from the backend however this is not the case this time.
Is there anything that stands out why my store isn't populating with the state data?
action
export const requireUserDiveLogData = createAsyncThunk(
'users/requireData', // action name
// action expects to be called with the name of the field
async (userId) => {
// you need to define a function to fetch the data by field name
const response = await userDiveLogList(userId);
// what we return will be the action payload
return response.data;
},
// only fetch when needed: https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution
{
// _ denotes variables that aren't used - the first argument is the args of the action creator
condition: (_, { getState }) => {
const { users } = getState(); // returns redux state
// check if there is already data by looking at the didLoadData property
if (users.didLoadDiveLogData) {
// return false to cancel execution
return false;
}
}
}
)
reducer
export const userSlice = createSlice({
name: 'users',
initialState: {
userDiveLogList: [],
didLoadDiveLogData: false,
},
reducers: {
[requireUserDiveLogData.pending.type]: (state) => {
state.didLoadDiveLogData = true;
},
[requireUserDiveLogData.fulfilled.type]: (state, action) => {
return {
...state,
...action.payload
}
},
}
})
You should use extraReducers rather than reducers to handle actions produced by createAsyncThunk and createAction functions.
Besides, Redux Toolkit's createReducer and createSlice automatically use Immer internally to let you write simpler immutable update logic using "mutating" syntax. You don't need to do the shallow copy work by yourself.
E.g.
// #ts-nocheck
import {
configureStore,
createAsyncThunk,
createSlice,
} from '#reduxjs/toolkit';
async function userDiveLogList(userId) {
return { data: { userDiveLogList: [1, 2, 3] } };
}
export const requireUserDiveLogData = createAsyncThunk(
'users/requireData',
async (userId) => {
const response = await userDiveLogList(userId);
return response.data;
},
{
condition: (_, { getState }) => {
const { users } = getState();
if (users.didLoadDiveLogData) {
return false;
}
},
}
);
const userSlice = createSlice({
name: 'users',
initialState: {
userDiveLogList: [],
didLoadDiveLogData: false,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(requireUserDiveLogData.pending, (state) => {
state.didLoadDiveLogData = true;
})
.addCase(requireUserDiveLogData.fulfilled, (state, action) => {
state.userDiveLogList = action.payload.userDiveLogList;
});
},
});
const store = configureStore({
reducer: {
users: userSlice.reducer,
},
});
store.dispatch(requireUserDiveLogData()).then(() => {
console.log(JSON.stringify(store.getState(), null, 2));
});
Output in the console:
{
"users": {
"userDiveLogList": [
1,
2,
3
],
"didLoadDiveLogData": true
}
}

Can't use new redux state right after fetching a response from Socket.IO

I have a function "sendMessage" in React class:
class MessageForm extends React.Component {
...
sendMessage = async () => {
const { message } = this.state;
if (message) {
this.setState({ loading: true });
if (this.props.isPrivateChannel === false) {
socket.emit("createMessage", this.createMessage(), (response) => {
this.setState({ loading: false, message: "", errors: [] });
});
} else {
if (this.state.channel && this.state.channel._id === undefined) {
socket.emit("createChannelPM", this.state.channel, async (response) => {
const chInfo = { ...response, name: this.props.currentChannel.name };
console.log("chInfo : ", chInfo);
await this.props.setCurrentChannel(chInfo).then((data) => {
if (data) {
console.log("data : ", data);
console.log("this.props.currentChannel : ", this.props.currentChannel);
}
});
});
}
...
function mapStateToProps(state) {
return {
isPrivateChannel: state.channel.isPrivateChannel,
currentChannel: state.channel.currentChannel,
};
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentChannel: async (channel) => await dispatch(setCurrentChannel(channel)),
}
};
Here, in sendMessage function, I retrieve "response" from socket.io, then put this data into variable "chInfo" and assign this to Redux state, then print it right after assinging it.
And Redux Action function, "setCurrentChannel" looks like:
export const setCurrentChannel = channel => {
return {
type: SET_CURRENT_CHANNEL,
payload: {
currentChannel: channel
}
};
};
Reducer "SET_CURRENT_CHANNEL" looks like:
export default function (state = initialState, action) {
switch (action.type) {
case SET_CURRENT_CHANNEL:
return {
...state,
currentChannel: action.payload.currentChannel
};
...
The backend Socket.io part look like (I use MongoDB):
socket.on('createChannelPM', async (data, callback) => {
const channel = await PrivateChannel.create({
...data
});
callback(channel)
});
The console.log says:
Problem : The last output, "this.props.currentChannel" should be same as the first output "chInfo", but it is different and only print out previous value.
However, in Redux chrome extension, "this.props.currentChannel" is exactly same as "chInfo":
How can I get and use newly changed Redux states immediately after assinging it to Redux State?
You won't get the updated values immediately in this.props.currentChannel. After the redux store is updated mapStateToProps of MessageForm component is called again. Here the state state.channel.currentChannel will be mapped to currentChannel. In this component you get the updated props which will be accessed as this.props.currentChannel.
I believe you want to render UI with the latest data which you which you can do.

How to test vuex plugins store.subscribe

I'm using jest to test a vue application, I have a doubt how can I test a plugin code. This is the code I'm trying to test:
export const persistPlugin = store => {
store.subscribe(async (mutation, state) => {
// filter all keys that start with `__`
const _state = omitPrivate(state);
const storedState = await storage.get('state');
if (isEqual(_state, storedState)) return;
storage.set(store, 'state', _state);
});
};
What I'm stuck at is the store.subscribe part. store is passes as argument of the plugin method, but I don't know how to call this method from the test is a wat that triggers the function block of the plugin.
You could use testPlugin helper for this. Here it is an example which you could adapt for the state verification.
I prefer to track mutations instead of direct state changes:
import { persistPlugin } from "#/store";
export const testPlugin = (plugin, state, expectedMutations, done) => {
let count = 1;
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count];
try {
expect(type).toEqual(mutation.type);
if (payload) {
expect(payload).toEqual(mutation.payload);
}
} catch (error) {
done(error);
}
count++;
if (count >= expectedMutations.length) {
done();
}
};
// call the action with mocked store and arguments
plugin({
commit,
state,
subscribe: cb =>
cb(expectedMutations[count - 1], expectedMutations[count - 1].payload)
});
// check if no mutations should have been dispatched
if (expectedMutations.length === 1) {
expect(count).toEqual(1);
done();
}
};
describe("plugins", () => {
it("commits mutations for some cases", done => {
testPlugin(
persistPlugin,
{ resume: { firstName: "Old Name" } },
[{ type: "updateResume", payload: { firstName: "New Name" } }], // This is mutation which we pass to plugin, this is payload for plugin handler
[{ type: "updateResume", payload: { firstName: "New Name" } }], // This is mutation we expects plugin will commit
done
);
});
});

How can I get response of this.$store.dispatch on the vue.js 2?

My component is like this :
<script>
export default{
props:['search','category','shop'],
...
methods: {
getVueItems: function(page) {
this.$store.dispatch('getProducts', {q:this.search, cat:this.category, shop: this.shop, page:page}).then(response => {
console.log(response)
this.$set(this, 'items', response.body.data)
this.$set(this, 'pagination', response.body)
}, error => {
console.error("this is error")
})
},
...
}
}
</script>
The ajax call getProducts method on the product.js module
The product.js module is like this :
import { set } from 'vue'
import product from '../../api/product'
import * as types from '../mutation-types'
// initial state
const state = {
list: {}
}
// actions
const actions = {
getProducts ({ commit,state }, payload)
{
product.getProducts( payload,
data => {
let products = data
commit(types.GET_PRODUCTS,{ products });
},
errors => {
console.log('error load products ')
}
)
}
}
// mutations
const mutations = {
[types.GET_PRODUCTS] (state, { products }) {
state.list = {}
products.data.forEach(message => {
set(state.list, message.id, message)
})
}
}
export default {
state,
actions,
mutations
}
Then, the module call getProducts method again on the product.js api
The product.js api is like this :
import Vue from 'vue'
import Resource from 'vue-resource'
Vue.use(Resource)
export default {
// api to get filtered products
getProducts (filter, cb, ecb = null ) {
Vue.http.post(window.Laravel.baseUrl+'/search-result',filter)
.then(
(resp) => cb(resp.data),
(resp) => ecb(resp.data)
);
}
}
When executed, I check on the console, the response not show. The response undefined
How can I solve the error?
UPDATE
If I use normal ajax like this :
<script>
export default{
props:['search','category','shop'],
...
methods: {
getVueItems: function(page) {
const q = this.search
const cat = this.category
const shop = this.shop
this.$http.get('search-result?page='+page+'&q='+q+'&cat='+cat+'&shop'+shop).then((response) => {
console.log(JSON.stringify(response))
this.$set(this, 'items', response.body.data)
this.$set(this, 'pagination', response.body)
});
},
...
}
}
</script>
It works. It get the response
But, Why when I use vuex store, it does not work?
You should return an Promised in your actions.
Try:
// actions
const actions = {
getProducts ({ commit,state }, payload)
{
return new Promise((resolve, reject) => {
product.getProducts( payload,
data => {
let products = data
commit(types.GET_PRODUCTS,{ products });
resolve(data)
},
errors => {
console.log('error load products ')
reject(errors)
}
)
})
}
}
or simply, you could just pass return Vue.http.post() up.

Categories