Vuex with Jest - Cannot read property <getterName> of undefined - javascript

I'm trying to use Jest to test a Vue component which makes use of a getter in Vuex. The getter returns a function which in turn returns an array:
questions: state => pageNumber => state.pages[pageNumber].questions
I make use of it in my component like so:
computed: {
inputs() {
return this.$store.getters.questions(this.pageNumber);
},
},
This seems to work fine in terms of rendering the UI, but when trying to test the component I get Cannot read property 'questions' of undefined
My test is a pretty simple one, but I've not used Jest with Vuex before so I could be misunderstanding how you would test components which use getters:
import Vuex from 'vuex';
import { mount, createLocalVue } from '#vue/test-utils';
import SurveyQuestionBuilder from '../components/SurveyQuestionBuilder.vue';
import store from '../store';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('SurveyQuestionBuilder.vue', () => {
it('renders a value from $store.state', () => {
const wrapper = mount(SurveyQuestionBuilder, { store, localVue });
expect(wrapper.exists()).toBeTruthy();
});
});
I'm presuming it's to do with pages[pageNumber] in the getter, but not sure what to do to resolve it.
Store.js imports a couple of modules:
import Vue from 'vue';
import Vuex from 'vuex';
import surveyBuilderStore from './survey_builder';
import matrixStore from './matrix';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
survey: surveyBuilderStore,
matrix: matrixStore,
},
});
The module in question is surveyBuilderStore:
const surveyBuilderStore = {
state: {
pages: [],
},
getters: {
pages: state => state.pages,
questions: state => pageNumber => state.pages[pageNumber].questions,
config: state => (pageNumber, questionNumber) =>
state.pages[pageNumber].questions[questionNumber].config,
},
mutations: {
// my mutations
}
};

In your questions getter, you search at probably unknown index in the pages array.
So questions: state => pageNumber => state.pages[pageNumber] is undefined because state.pages is empty and pageNumber is above 0.
To avoid this you can do:
questions: state => pageNumber => {
return state.pages[pageNumber]
? state.pages[pageNumber].questions
: [] // <-- here your default value
}
You can set in your test the value of pageNumber but I don't know if it's a props or data of the component:
For data:
mount(SurveyQuestionBuilder, {
store,
localVue,
data:() => ({ pageNumber: 0 })
})
For props:
mount(SurveyQuestionBuilder, {
store,
localVue,
propsData: { pageNumber: 0 }
})

Related

How to set my state from vuex to it's "original" form? [duplicate]

My state in vuex store is huge.
Is there a way to reset all the data in state in one go, instead of manually setting everything to null?
I have just found the great solution that works for me.
const getDefaultState = () => {
return {
items: [],
status: 'empty'
}
}
// initial state
const state = getDefaultState()
const actions = {
resetCartState ({ commit }) {
commit('resetState')
},
addItem ({ state, commit }, item) { /* ... */ }
}
const mutations = {
resetState (state) {
// Merge rather than replace so we don't lose observers
// https://github.com/vuejs/vuex/issues/1118
Object.assign(state, getDefaultState())
}
}
export default {
state,
getters: {},
actions,
mutations
}
Thanks to Taha Shashtari for the great solution.
Michael,
Update after using the below solution a bit more
So it turns out that if you use replaceState with an empty object ({}) you end up bricking reactivity since your state props go away. So in essence you have to actually reset every property in state and then use store.replaceState(resetStateObject). For store without modules you'd essentially do something like:
let state = this.$store.state;
let newState = {};
Object.keys(state).forEach(key => {
newState[key] = null; // or = initialState[key]
});
this.$store.replaceState(newState);
Update (from comments): What if one needs to only reset/define a single module and keep the rest as they were?
If you don't want to reset all your modules, you can just reset the modules you need and leave the other reset in their current state.
For example, say you have mutliple modules and you only want to reset module a to it's initial state, using the method above^, which we'll call resetStateA. Then you would clone the original state (that includes all the modules before resetting).
var currentState = deepClone(this.state)
where deepClone is your deep cloning method of choice (lodash has a good one). This clone has the current state of A before the reset. So let's overwrite that
var newState = Object.assign(currentState, {
a: resetStateA
});
and use that new state with replaceState, which includes the current state of all you modules, except the module a with its initial state:
this.$store.replaceState(newState);
Original solution
I found this handy method in Vuex.store. You can clear all state quickly and painlessly by using replaceState, like this:
store.replaceState({})
It works with a single store or with modules, and it preserves the reactivity of all your state properties. See the Vuex api doc page, and find in page for replaceState.
For Modules
IF you're replacing a store with modules you'll have to include empty state objects for each module. So, for example, if you have modules a and b, you'd do:
store.replaceState({
a: {},
b: {}
})
You can declare an initial state and reset it to that state property by property. You can't just do state = initialState or you lose reactivity.
Here's how we do it in the application I'm working on:
let initialState = {
"token": null,
"user": {}
}
const state = Vue.util.extend({}, initialState)
const mutations = {
RESET_STATE(state, payload) {
for (let f in state) {
Vue.set(state, f, initialState[f])
}
}
}
I am not sure what you use case is, but I had to do something similar. When a user logs out, I want to clear the entire state of the app - so I just did window.reload. Maybe not exactly what you asked for, but if this is why you want to clear the store, maybe an alternative.
If you do a state = {}, you will remove the reactivity of the properties and your getters mutations will suddenly stop working.
you can have a sub-property like:
state: {
subProperty: {
a: '',
lot: '',
of: '',
properties: '',
.
.
.
}
}
Doing a state.subProperty = {} should help, without losing the reactivity.
You should not have a state too big, break them down to different modules and import to your vuex store like so:
import Vue from 'vue'
import Vuex from 'vuex'
import authorization from './modules/authorization'
import profile from './modules/profile'
Vue.use(Vuex)
export const store = new Vuex.Store({
modules: {
authorization,
profile
}
})
now in your individual files:
// modules/authorization.js
import * as NameSpace from '../NameSpace'
import { someService } from '../../Services/something'
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) => {
someService.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
}
If you should want to clear the state you can just have a mutation implement:
state[NameSpace.AUTH_STATE] = {
auth: {},
error: null
}
Here's a solution that works in my app. I created a file named defaultState.js.
//defaultState.js
//the return value is the same as that in the state
const defaultState = () => {
return {
items: [],
poles: {},
...
}
}
export default defaultState
And then Where you want to use it
//anywhere you want to use it
//for example in your mutations.js
//when you've gotten your store object do
import defaultState from '/path/to/defaultState.js'
let mutations = {
...,
clearStore(state){
Object.assign(state, defaultState())
},
}
export default mutations
Then in your store.js
import Vue from 'vue';
import Vuex from 'vuex';
import actions from './actions';
import getters from './getters';
import mutations from './mutations'; //import mutations
import state from './state';
Vue.use(Vuex);
export default new Vuex.Store({
actions,
mutations,
state,
getters,
});
and That's it
If you want to reset your entire state you can use the built in replaceState method.
Given a state set in index.js:
const state = { user: '', token: '', products: [] /* etc. */ }
const initialStateCopy = JSON.parse(JSON.stringify(state))
export const store = new Vuex.Store({ state, /* getters, mutations, etc. */ })
export function resetState() {
store.replaceState(initialStateCopy)
}
Then in your vue component (or anywhere) import resetState:
import { resetState } from '#/store/index.js'
// vue component usage, for example: logout
{
// ... data(), computed etc. omitted for brevity
methods: {
logout() { resetState() }
}
}
Based on these 2 answers (#1 #2) I made a workable code.
My structure of Vuex's index.js:
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import { header } from './header'
import { media } from './media'
Vue.use(Vuex)
const store = new Vuex.Store({
plugins: [createPersistedState()],
modules: {
header,
media
}
})
export default store
Inside each module we need to move all states into separated var initialState and in mutation define a function resetState, like below for media.js:
const initialState = () => ({
stateOne: 0,
stateTwo: {
isImportedSelected: false,
isImportedIndeterminate: false,
isImportedMaximized: false,
isImportedSortedAsc: false,
items: [],
stateN: ...
}
})
export const media = {
namespaced: true,
state: initialState, // <<---- Our States
getters: {
},
actions: {
},
mutations: {
resetState (state) {
const initial = initialState()
Object.keys(initial).forEach(key => { state[key] = initial[key] })
},
}
}
In Vue component we can use it like:
<template>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
name: 'SomeName',
data () {
return {
dataOne: '',
dataTwo: 2
}
},
computed: {
},
methods: {
...mapMutations('media', [ // <<---- define module
'resetState' // <<---- define mutation
]),
logout () {
this.resetState() // <<---- use mutation
// ... any code if you need to do something here
}
},
mounted () {
}
} // End of 'default'
</script>
<style>
</style>
Call router.go() or this.$router.go()
That will refresh the page and your state will be reset to how it was when the user first loaded the app.
Myself has read above and implemented a solution. could help you as well!!
All objects stored in Vue act as an observable. So if reference of a value is changed/mutated it triggers the actual value to be changed too.
So, Inorder to reset the state the initial store modules has to be copied as a value.
On logging out of an user, the same value has to be assigned for each modules as a copy.
This can be achieved as following:
Step 1: Create a copy of your initial module.
// store.ts
// Initial store with modules as an object
export const initialStoreModules = {
user,
recruitment,
};
export default new Vuex.Store({
/**
* Assign the modules to the store
* using lodash deepClone to avoid changing the initial store module values
*/
modules: _.cloneDeep(initialStoreModules),
mutations: {
// reset default state modules by looping around the initialStoreModules
[types.RESET_STATE](state: any) {
_.forOwn(initialStoreModules, (value: IModule, key: string) => {
state[key] = _.cloneDeep(value.state);
});
},
}
});
Step 2: Call the action to mutate the state to initial state.
// user_action.ts
const logout = ({ commit }: any) => {
commit(types.LOGOUT_INIT);
new UserProxy().logout().then((response: any) => {
router.push({
name: 'login',
});
// reset the state
commit(types.RESET_STATE);
}).catch((err: any) => {
commit(types.LOGOUT_FAIL, err);
});
};
You could take it easy by tiny package: vuex-extensions
Check out the example on CodeSandbox.
Creating Vuex.Store
import Vuex from 'vuex'
import { createStore } from 'vuex-extensions'
export default createStore(Vuex.Store, {
plugins: []
modules: {}
})
Store resets to initial State
// Vue Component
this.$store.reset()
// Vuex action
modules: {
sub: {
actions: {
logout() {
this.reset()
}
}
}
}
You can do this
index.js
...
const store = new Vuex.Store({
modules: {
...
}
})
store.initialState = clone(store.state)
store.resetState = () => {
store.replaceState(store.initialState)
}
export default store
Other place
this.$store.resetState()
function initialState () {
return { /* .. initial state ... */ }
}
export default {
state: initialState,
mutations: {
reset (state) {
// acquire initial state
const s = initialState()
Object.keys(s).forEach(key => {
state[key] = s[key]
})
}
}
}
This is an official recommendation
issue
if you clear your complete vuex store use:
sessionStorage.clear();

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()
})

Mock vuex with Jest, uknown getters

I'm quite stuck on how I test components that use vuex.
The test i'm trying to run simply tests that when run to set edit to true. The method is as follows:
editMovie: function(movie){
console.log(movie)
this._originalMovie = Object.assign({}, movie);
movie.edit = true;
},
Some methods and on mount dispatches to the store.
I was getting errors about it can't find dispatch so followed https://vue-test-utils.vuejs.org/guides/using-with-vuex.html and added in the Mock store (I think...), with this:
Error:
console.error node_modules/vuex/dist/vuex.common.js:899 [vuex] unknown
getter: movies
console.error node_modules/vue/dist/vue.runtime.common.dev.js:621 [Vue
warn]: Error in render: "TypeError: Cannot read property '0' of
undefined"
Code:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
//import Actions from '../../src/store/Actions'
import MoviesList from '#/components/MoviesList.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('MoviesList.vue', () => {
let actions
let store
beforeEach(() => {
actions = {
getMovies: jest.fn(),
}
store = new Vuex.Store({
actions
})
})
it('Edit should be true', () => {
const wrapper = shallowMount(MoviesList, { store, localVue })
wrapper.setData({
movies: { 0: {
deleted:false,
edit:false,
name:"Full Metal Jacket",
released_on:"1987",
}}
})
console.log(wrapper.vm.movies[0])
wrapper.vm.editMovie(wrapper.vm.movies[0])
expect(wrapper.vm.movies[0].edit).toBe(true)
})
})

Why the state doesn't update in computed?

I'm struggling for a couple of hours with vuex.
It's the first time I use it and even though I follow the documentation something is not working but I really can't figure it out.
So I have the first example of the vuex doc that doesn't work.
I have a count computed value that return the store state.
It's display on the page.
The started value is 0 and the view is displaying 0.
But hitting the buttons doesn't change the display.
If I put directly the store into the App.js in a constant, no problem it works.
I know I'm missing something...
homeStore.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
},
decrement (state) {
state.count--
}
}
})
App.js
import store from '../store/homeStore'
new Vue({
el: '#app',
store,
computed: {
count () {
return store.state.count
},
},
methods: {
increment () {
store.commit('increment')
},
decrement () {
store.commit('decrement')
}
}
})
Make use of Vuex store actions
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: { count: 0 },
mutations: {
increment (state) {
state.count++
},
decrement (state) {
state.count--
}
},
actions: {
increase ({ commit }) {
commit('increment')
},
decrease ({ commit }) {
commit('decrement')
}
}
})
access the action method using this.$store.dispatch('increase') to increment the count and vice versa for decrement
If you try to console.log(store), it will returns undefined.
The right way to access the store is to use this.$store (should be a component).
As mentioned in the doc:
By providing the store option to the root instance, the store will be injected into all child components of the root and will be available on them as this.$store.
Therefore, your updated code should be:
methods: {
increment () {
this.$store.commit('increment')
},
decrement () {
this.$store.commit('decrement')
}
}

Categories