Mocking Vuex module action in component unit test - javascript

I'm currently trying to mock an action from a store's module. I can't seem to properly stub it, as I continue to get a message in my unit tests that says:
[vuex] unknown action type: moduleA/filterData
Here is a simplified version of the component under test:
Item.vue
<template>
<li class="list-item"
#click="toggleActive()">
{{ itemName }}
</li>
</template>
<script>
import store from '../store'
export default {
name: 'item',
props: {
itemName: {
type: String
}
},
data () {
return {
store,
isActive: false
}
},
methods: {
toggleActive () {
this.isActive = !this.isActive;
this.$store.dispatch('moduleA/filterData', { name: itemName } );
}
}
}
</script>
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import moduleA from './modules/moduleA'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
moduleA
}
});
export default store;
moduleA.js
/* imports */
const state = {
/* state */
}
const mutations = {
/* ... */
}
const actions = {
filterData({ state, commit }, payload) {
/* filter data and commit mutations */
}
}
const getters = {
/* getters */
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
Item.spec.js
import Vue from 'vue'
import { mount } from '#vue/test-utils'
import { expect } from 'chai'
import sinon from 'sinon'
import Item from '../src/components/Item.vue'
import Vuex from 'vuex'
Vue.use(Vuex);
describe('Item.vue', () => {
let componentProps = {};
let wrapper;
let actions;
let store;
beforeEach(() => {
let name = 'Item Name';
// mock vuex action on click
actions = {
filterData: sinon.stub()
}
let moduleA = {
state: {},
actions
}
store = new Vuex.Store({
modules: {
moduleA
}
});
componentProps.itemName = name;
wrapper = mount(Item, {
store: store,
propsData: componentProps
});
})
it('Has a root element of list-item', () => {
expect(wrapper.is('.list-item')).to.equal(true);
})
it('Item getting prop name', () => {
expect(wrapper.text()).to.equal('Item Name');
})
it('Item is not active on load', () => {
expect(wrapper.vm.$data.isActive).to.equal(false);
})
it('Item is active after click', () => {
wrapper.trigger('click');
expect(wrapper.vm.$data.isActive).to.equal(true);
})
it('Item is not active after two clicks', () => {
wrapper.trigger('click');
wrapper.trigger('click');
expect(wrapper.vm.$data.isActive).to.equal(false);
})
})
This isn't causing my tests to fail, but I've been unable to find out how to properly mock/stub module actions from Vuex. Any help is appreciated.

So I've looked into this, and it turns out that I wasn't defining that my store within my test was namespaced, hence it wasn't recognizing my action:
beforeEach(() => {
let name = 'Item Name';
// mock vuex action on click
actions = {
filterData: sinon.stub()
}
let moduleA = {
namespaced: true,
state: {},
actions
}
store = new Vuex.Store({
modules: {
moduleA
}
});
componentProps.itemName = name;
wrapper = mount(Item, {
store: store,
propsData: componentProps
});
})
After including this my errors went away.

Related

How do I get multiple actions and state with Vuex namespaced modules and the Vue Composition API?

With the Vue Options API I get my actions and state by doing:
computed: {
...mapState("myStore", ["myState", "myOtherState"]),
},
methods: {
...mapActions("myStore", ["myAction", "myOtherAction"]),
}
My question is how do I get multiple namespaced actions and state with Vuex and the Vue Composition API?
Right now I have a wrapper called useStoreModule.js that looks like this:
import { createNamespacedHelpers } from "vuex-composition-helpers/dist";
export default function () {
function getState(module, name) {
const { useState } = createNamespacedHelpers(module);
return useState([name]);
}
function getAction(module, name) {
const { useActions } = createNamespacedHelpers(module);
return useActions([name]);
}
return {
getState,
getAction,
};
}
and get the state/actions by doing this:
setup() {
const { getState } = useStoreModule();
const { myState } = getState("myStore", "myState");
}
So how do I write the wrapper to do this:
setup() {
const { getState } = useStoreModule();
const { myState, myOtherState, myOtherOtherState } = getState("myStore", ["myState", "myOtherState", "myOtherOtherState"]);
}
Any help is appreciated :)
This is a way to do it:
component.vue
import { getCurrentInstance } from 'vue';
setup() {
const _instance = getCurrentInstance();
const { $store } = _instance.appContext.app.config.globalProperties;
},
createStore
"use strict";
import { createStore } from 'vuex';
// Stores
import moduleA from '#/store/moduleA.js';
import moduleB from '#/store/moduleB.js';
export const store = createStore({
strict: true,
modules: {
moduleA,
moduleB,
}
});

Vue unit testing vuex store action with jest

I'm using Vue version 2.6.11, Vuex version 3.4.0. I have a function in the vuex store (timeTravel) which moves array element from given index to another given index. I have a component with a button that trigger this function. I'm new to unit testing and it's confusing for me.
ActionList.vue component
// other codes
<v-btn color="primary" dark #click="timeTravel({from:action.from_index,to:action.to_index,action_list_index:index})">
<v-icon left dark>
mdi-clock-outline
</v-icon>
Submit
</v-btn>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
name: "ActionsList",
computed: mapGetters(["commitedActionsList"]),
methods: {
...mapActions(["timeTravel"]),
},
};
</script>
Vuex store
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
posts: [],
commitedActions: [],
},
getters: {
postsList: (state) => state.posts,
commitedActionsList: (state) => state.commitedActions,
},
mutations: {
setPosts: (state, posts) => (state.posts = posts),
setCommitedActions: (state, commitedActions) =>
state.commitedActions.push(commitedActions),
},
actions: {
async fetchPosts({ commit }) {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/posts/?_limit=5"
);
commit("setPosts", response.data);
},
timeTravel({ commit, getters }, data) {
var from = data["from"];
var to = data["to"];
var action_list_index = data["action_list_index"];
console.log(from,to,action_list_index);
var posts_copy = getters.postsList;
var f = posts_copy.splice(from, 1)[0]; // remove `from` item and store it
posts_copy.splice(to, 0, f); // insert stored item into position `to`
commit("setPosts", posts_copy);
this.state.commitedActions.splice(action_list_index,1)
},
},
modules: {},
});
So I want to write a unit test for this timeTravel function with jest. I found some tutorials but it's confusing for me. So far I've come up with this.
import Vuetify from 'vuetify';
import Vuex from 'vuex'
import { createLocalVue ,shallowMount } from '#vue/test-utils'
import ActionsList from '#/components/ActionsList.vue';
let localVue = createLocalVue();
describe('App',()=>{
let store
let actions
let state
let getters
beforeEach(()=>{
localVue.use(Vuex)
localVue.use(Vuetify)
state = {posts:[]}
getters = {
postsList: (state) => state.posts,
}
actions = {
timeTravel: jest.fn()
}
store = new Vuex.Store({
state,
getters,
actions
})
})
it('dispatches timeTravel', ()=>{
const wrapper = shallowMount({
localVue,
store
})
wrapper.find('v-btn').trigger('click')
expect()
})
})
I want to test if timeTravel function is moving element from given index to another given index. So it would be great if someone can point me in the right direction which helps me to write this unit test.

How to organize Vue unit test with factory functions?

I'm trying to write unit tests for my Dashboard.vue component using factory functions so that I could overwrite the store and wrapper as per needed.
Here is the code
import { mount, createLocalVue } from '#vue/test-utils'
import mergeWith from 'lodash.mergewith'
import mutationobserver from 'mutationobserver-shim'
import Vuex from 'vuex'
import BootstrapVue from 'bootstrap-vue'
import Dashboard from '#/views/dashboard/Dashboard'
import { FontAwesomeIcon } from '#fortawesome/vue-fontawesome'
import { library as faLibrary } from '#fortawesome/fontawesome-svg-core'
import { faUser, faThumbsUp, faSignOutAlt, faBorderAll, faAlignJustify, faTrashAlt, faRandom } from '#fortawesome/free-solid-svg-icons'
import flushPromises from 'flush-promises'
jest.mock('#/services/app.service.js')
faLibrary.add(faUser, faThumbsUp, faSignOutAlt, faBorderAll, faAlignJustify, faTrashAlt, faRandom)
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(BootstrapVue)
localVue.use(mutationobserver) // This is a necessary polyfill for Bootstrap-Vue
localVue.component('font-awesome-icon', FontAwesomeIcon)
function customizer (ovjValue, srcValue) {
/*
If the property that takes precedence is an array,
overwrite the value rather than merging the arrays
*/
if (Array.isArray(srcValue)) {
return srcValue
}
/*
If the property that takes precedence is an empty object,
overwrite the property with an empty object
*/
if (srcValue instanceof Object && Object.keys(srcValue).length === 0) {
return srcValue
}
}
describe('DashBoard component tests', () => {
let state
// let actions
// let getters
let store
let wrapper
let dashBoardData = [
{ db_name: 'Jobs', dxp_dashboardref: 1, dxp_hidden: 0, dxp_position: 1, dxp_ref: 926 },
{ db_name: 'Firms', dxp_dashboardref: 2, dxp_hidden: 0, dxp_position: 2, dxp_ref: 927 },
{ db_name: 'CRM', dxp_dashboardref: 5, dxp_hidden: 0, dxp_position: 3, dxp_ref: 987 }
]
// beforeEach(() => {
state = {
auth: {
user: {
auids: '',
md_clock: 0,
md_picture: '',
ps_fname1: '',
ps_surname: '',
psname: 'Test Test',
psref: 0
}
},
app: {
dashBoardData: []
}
}
function createStore (overrides) {
const defaultStoreConfig = {
// state: {
// state
// },
getters: {
getDashBoardData: () => dashBoardData
},
actions: {
loadDashboard: jest.fn(),
updateDashBoardData: jest.fn()
}
}
return new Vuex.Store(
state,
mergeWith(defaultStoreConfig, overrides, customizer)
)
}
function createWrapper (overrrides) {
const defaultMountingOptions = {
localVue,
store: createStore()
}
return mount(
Dashboard,
mergeWith(
defaultMountingOptions,
overrrides,
customizer)
)
}
// START: Testing existence of DOM Elements tests
it('is a Vue instance', () => {
const wrapper = createWrapper({})
expect(wrapper.isVueInstance).toBeTruthy()
})
})
Essentially, I'm trying to use a createWrapper method which has a default store unless overrides or customizer are passed. When I run the test I get the following errors
console.error node_modules/vuex/dist/vuex.common.js:899
[vuex] unknown getter: getDashBoardData
console.error node_modules/vue/dist/vue.runtime.common.dev.js:621
[Vue warn]: Error in render: "TypeError: Cannot read property 'length' of undefined"
Now, I have two questions:
Why am I being thrown unknown getter when I have declared it in the defaultStoreConfig ?
The second error comes from the state. For some reason it doesn't recognize the state variable I'm passing. Any ideas why ?
If I simply declare the wrapper inside a beforeEach like so I can pass some of my test but for others which I need to overwrite either getters or actions, I'm not able to do that unless I have the factory functions
getters = {
getDashBoardData: () => dashBoardData
},
actions = {
loadDashboard: jest.fn(),
updateDashBoardData: jest.fn()
}
store = new Vuex.Store({
state,
actions,
getters
})
})
Any help will be highly appreciated!
Solved this by passing the state inside defaultStoreConfig rather than separately when creating the store
Code:
const defaultStoreConfig = {
state: {
auth: {
user: {
auids: '',
md_clock: 0,
md_picture: '',
ps_fname1: '',
ps_surname: '',
psname: 'Test Test',
psref: 0
}
},
app: {
dashBoardData: []
}
},
getters: {
getDashBoardData: () => dashBoardData
},
actions: {
loadDashboard: jest.fn(),
updateDashBoardData: jest.fn()
}
}
Test:
it('is a Vue instance', () => {
const wrapper = createWrapper()
expect(wrapper.isVueInstance).toBeTruthy()
})

'unknown action type' with Vuex mapAction

I am new to Vuex and trying to use mapActions to fetch some data from my store, and send it into my component's template. I keep getting the error message
[vuex] unknown action type: getItemDetail but I don't know why.
In my store, the action I'm trying to dispatch is getItemDetail. My full store is
import fetch from './fetch';
const url = 'items';
const defaults = {
id: '',
rating: '',
type: '',
};
const state = { ...defaults, };
const getters = {};
const actions = {
getItemDetail ({ commit, }, itemId) {
fetch.get(`${url}/${itemId}`).then((response) => {
commit('setItemDetail', { ...defaults, ...response.data, });
});
},
};
const mutations = {
setItemDetail (state, item) {
state.item = item;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
In my component I have:
<template>
<div>
<p> {{ itemDetail }} </p>
</div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
computed: {
itemDetail () {
this.getItemDetail('23451');
return this.$store.state;
},
},
methods: {
...mapActions([
'getItemDetail',
]),
},
};
</script>
Any help would be much appreciated!!!
From what I see of your code, you are in a namespaced store module.
To access a namespaced action, you need to map it with the name of the store module as the first parameter (the same should be applied for any mapState or mapGetter) :
methods: {
...mapActions("yourModuleName", [
'getItemDetail',
]),
},

vuex unknown action type while trying to post data to Django backend

I am trying to send my vuejs front end data using Vuex and Vue-axios to the backend. I have create a vuex store and vue-axios services But I get an error saying [vuex] unknown action type: addGeneral when I try to pass the data.
This is my vuex folder structure:
-store
-modules
-app
-mutations.js
-state.js
-general.js
-index.js
-actions.js
-getters.js
-index.js
-mutations.js
-state.js
This is module/general.js :
import { ApiService } from '#/services/api.service'
import { FETCH_GENERAL,
ADD_GENERAL
} from '../actions'
import { FETCH_START,
FETCH_END,
SET_GENERAL,
SET_ERROR,
} from '../mutations'
const state = {
general: [],
errors: {},
loading: false
};
const getters = {
general (state) {
return state.general;
},
isLoading (state) {
return state.loading;
}
};
const actions = {
[FETCH_GENERAL] (context, payload) {
context.commit(FETCH_START);
return ApiService
.get('general')
.then(({data}) => {
context.commit(FETCH_END);
context.commit(SET_GENERAL, data.general.results);
})
.catch(({response}) => {
context.commit(SET_ERROR, response.data.errors)
})
},
[ADD_GENERAL] (context, payload) {
context.commit(FETCH_START);
return ApiService
.postGeneral(`general`, '',payload)
.then(({data}) => {
context.commit(FETCH_END);
context.commit(SET_GENERAL, data.general.results);
})
.catch(({response}) => {
context.commit(SET_ERROR, response.data.errors)
})
}
};
const mutations = {
[FETCH_START] (state) {
state.loading = true
},
[FETCH_END] (state) {
state.loading = false
},
[SET_GENERAL] (state, pgeneral) { // can pass in payload
state.components = pgeneral;
state.errors = {}
},
[SET_ERROR] (state, errors) {
state.errors = errors
}
};
export default {
state,
getters,
actions,
mutations
}
This is module/index.js :
const requireModule = require.context('.', true, /\.js$/)
const modules = {}
requireModule.keys().forEach(fileName => {
if (fileName === './index.js') return
// Replace ./ and .js
const path = fileName.replace(/(\.\/|\.js)/g, '')
const [moduleName, imported] = path.split('/')
if (!modules[moduleName]) {
modules[moduleName] = {
namespaced: true
}
}
modules[moduleName][imported] = requireModule(fileName).default
})
export default modules
This is store/actions.js :
export const FETCH_GENERAL = "fetchGeneral";
export const ADD_GENERAL = "addGeneral";
This is store/index.js :
import Vue from 'vue'
import Vuex from 'vuex'
// Store functionality
import actions from './actions'
import getters from './getters'
import modules from './modules'
import mutations from './mutations'
import state from './state'
Vue.use(Vuex)
// Create a new store
const store = new Vuex.Store({
actions,
getters,
modules,
mutations,
state
})
export default store
This is store/mutations.js :
export const FETCH_START = "loadingOn";
export const FETCH_END = "loadingOff";
export const SET_ERROR = "setError";
// related to general
export const SET_GENERAL = 'setGeneral';
This is my vue-axios folder structure:
-services
-api.services.js
-config.js
This is services/api.serviecs.js :
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
import { API_URL } from './config'
import Cookies from 'js-cookie'
let CSRF_TOKEN = Cookies.get('csrftoken');
export const ApiService = {
init () {
Vue.use(VueAxios, axios)
Vue.axios.defaults.baseURL = API_URL
},
get (resource, slug='') {
return Vue.axios
.get(`${resource}\${slug}`,)
.catch((error) => {
throw new Error(`ApiService ${error}`)
})
},
postGeneral (resource, slug='', obj) {
return axios
.post(`${API_URL}\\${resource}\\${slug}`,{
systemName: obj.systemName,
regionOfDeployment: obj.regionOfDeployment,
operatingMode: obj.operatingMode,
solution: obj.solution,
baselineMode: obj.baselineMode,
baselineDetails: obj.baselineDetails,
projectDuration: obj.projectDuration,
},
{
headers: {
'X-CSRFToken': CSRF_TOKEN,
'Content-Type': 'application/json',
}
})
.catch((error) => {
throw new Error (`ApiService ${error}`)
})
},
}
export default ApiService
This is config.js:
export default {}
export const API_URL = 'http://127.0.0.1:8000/api';
and finally this is my vuejs component:
...
<v-btn class="mt-5 mr-2 font-weight-light" color="blue"
#click="addGeneral" >
...
methods: {
addGeneral() {
let obj = {
systemName: '',
regionOfDeployment: '',
operatingMode: '',
solution: '',
baselineMode: '',
baselineDetails: '',
projectDuration: ''
};
this.postGeneral(obj)
},
postGeneral(obj) {
this.$store.dispatch(ADD_GENERAL, obj)
}
}
Why do I get the error and what's the best way to solve it?
You're using namespaced: true, so you need to pass module name in dispatch
postGeneral(obj) {
this.$store.dispatch('general/' + ADD_GENERAL, obj)
}

Categories