I'm working on a survey builder in Vue, and survey questions which the user creates are committed to Vuex so they can be retrieved later like so:
computed: {
inputs() {
return this.$store.getters.questions(this.pageNumber);
},
},
pageNumber is a prop the component receives and inputs() returns an array of questions. This all seems to work in terms of rendering the correct questions on screen but I'm having trouble with Jest tests.
In order to test I was hoping I could mock the store with getters like my attempt below (omitting some parts):
const localVue = createLocalVue();
localVue.use(Vuex);
beforeEach(() => {
state = {
survey: {
pages: [
// pages objects
],
},
};
getters = {
questions: () => [
{ type: 'Radio', config: { label: 'Test label', options: [{ label: 'Test option label' }] }, validation: [] },
],
};
store = new Vuex.Store({
state,
getters,
});
});
But this results in the error:
TypeError: this.$store.getters.questions is not a function
However, removing that arrow function from getters.questions gives me:
[vuex] getters should be function but "getters.questions" is [{"type":"Radio","config":{"label":"Test label","options":[{"label":"Test option label"}]},"validation":[]}].
So I think I could be completely misunderstanding. Could someone point me in the right direction?
The getters of a store are just like computed properties on components, they are defined using functions but accessed as properties, without the parentheses.
Given this line:
return this.$store.getters.questions(this.pageNumber);
it would appear that your questions getter is returning a function that accepts a pageNumber. That isn't what you're currently defining in your test getter, you're just returning an array.
So either the invocation needs to change to use square brackets:
return this.$store.getters.questions[this.pageNumber];
or the getter needs to return a function:
getters = {
questions: () => () => [
{ type: 'Radio', config: { label: 'Test label', options: [{ label: 'Test option label' }] }, validation: [] }
]
};
If it helps to clarify, this is equivalent to:
getters = {
questions: function () {
return function () {
const questions = [
{ type: 'Radio', config: { label: 'Test label', options: [{ label: 'Test option label' }] }, validation: [] }
];
return questions;
};
}
};
Note that I'm completely ignoring the passed pageNumber as I assume your test getter is hard-coded to return the correct array of questions.
You may wish to consult with the non-test version of this getter as I expect you'll see it returns an extra level of function.
Under vue-test-utils and Jest you could easily mock a getter (especially deep nested module getter) if you replace your mocked store with the following:
Approach #1
store: new Vuex.Store({
getters: {
'top-level-module/nested-module-level-1/more-nested-module/dataItem': function () {
return 'any fake data you want'
}
}
}),
This soulution meakes you free from writing deep nested store objects mocks if you just want to test a component on a mocked getter (say you obtain value from the nested store module in your component's computed). Just return the fake (!) object of your preference from the mock getter.
Note you do not need to mock store state if you do not test it.
As well do not test the getter's returned object in this test. Extract it to the separate dedicated test.
Approach #2
You would not even need Vuex. Just use vue-test-utils mocks as follows:
mocks: {
$store: {
getters: {
'top-level-module/nested-module-level-1/more-nested-module/dataItem': (function () {
return 'any fake data you want'
})()
}
}
},
Note that vs Approach #1 you have to make your mock getter to be an IIFE to get the value from mock as the mock does not invoke getter as Vuex does.
Write the questions method in getters like this:
getters = {
questions: (state) => (page_number)=>{ [
{ type: 'Radio', config: { label: 'Test
label',
options: [{ label: 'Test option label' }]
},
validation: [] },
],
}
};
In my case, I created a new module but I forgot to register it in the index.js file.
New file in src/store/modules: entities.js
Then in the index.js file:
import entities from "./modules/entities";
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
...
entities,
},
});
Related
How do I solve this Vue Js error on Shopware 6 Administration. The module is suppose to select a column in the database table.
PS. This is the complete code. I'm trying to read data from the database and view it in the twig template.
const { Component, Mixin } = Shopware;
const { Criteria } = Shopware.Data;
import template from './store-settings-page.html.twig'
Component.register('store-settings-page', {
template,
inject: [
'repositoryFactory'
],
metaInfo() {
return {
title: this.$createTitle()
};
},
data: function () {
return {
entity: undefined,
storeData: null,
entityId: '4e2891496c4e4587a3a7efe587fc8c80',
secret_key: 'hdkkjjsmk538dncbjmns',
public_key: '1destinoDet2123lefmoddfk##$$%O',
}
},
computed: {
storeKeysRepository() {
return this.repositoryFactory.create('store_keys');
},
},
created() {
this.storeKeysRepository
.get(this.entityId, Shopware.Context.api)
.then(entity => {
this.entity = entity;
});
console.log(entity);
},
});
Apologies if my knowledge of Vue & JS is a bit off, based on how I see Shopware codes it, I recommend data to be written like this:
data() {
return {
...
};
}
I would also try to strip your file to the bear minimum to see when the error disappears.
Another thing to check is if you are running a JS file or TS file. Maybe it's having a hard time parsing your file because you are extending store-settings-page and it assumes it should be TypeScript?
this.storeKeysRepository
.get(this.entityId, Shopware.Context.api)
.then(entity => {
this.entity = entity;
console.log(this.entity);
});
This will do the trick
I use vuex for my state as well as fetching data and display it in my application.
But I wonder if I'm doing it right. At the moment I dispatch an fetchDataAsync action from the component mounted hook, and I have an getter to display my data. Below is a code example of how I do it currently.
I wonder if it's necessary. What I really want is a getter, that looks at the state, checks if the data is already there and if the data is not there it is able to dispatch an action to fetch the missing data.
The API of vuex does not allow it so I need to put more logic into my components. E.g. if the data is depended of a prop I need a watcher that looks at the prop and dispatches the fetchDataAsync action.
For me it just feels wrong and I wonder if there is a better way.
let store = new Vuex.Store({
state: {
posts: {}
},
mutations: {
addPost(state, post) {
Vue.set(state.posts, post.id, post);
}
},
actions: {
fetchPostAsync({ commit }, parameter) {
setTimeout(
() =>
commit("addPost", { id: parameter, message: "got loaded asynchronous" }),
1000
);
}
},
getters: {
// is it somehow possible to detect: ob boy, I don't have this id,
// I'd better dispatch an action trying to fetch it...?
getPostById: (state) => (id) => state.posts[id]
}
});
new Vue({
el: "#app",
store,
template : "<div>{{ postToDisplay ? postToDisplay.message : 'loading...' }} </div>",
data() {
return {
parameter: "a"
};
},
computed: {
...Vuex.mapGetters(["getPostById"]),
postToDisplay() {
return this.getPostById(this.parameter);
}
},
methods: {
...Vuex.mapActions(["fetchPostAsync"])
},
mounted() {
this.fetchPostAsync(this.parameter);
}
});
I also created a codepen
Personally I think the solution you suggested (adding a watcher that dispatches fetchPostAsync if the post is not found) is the best one. As another commenter stated, getters should not have side effects.
I have code like this for a store in Vuex, if do it like the code below it works fine.
state: {
APIData: {},
},
getters: {
getFeed: state => {return state.APIData },
},
mutations: {
SET_FEED_DATA(state, {folder_id, data}) {
state.APIData = data
}
},
However, if I try to use a key to set and get the value it return undefined in the getter. I want to do this because I have a for loop in my template calling an action multiple times but with different id's that I will use here for the key value.
state: {
APIData: {},
},
getters: {
getFeed: state => {return state.APIData["test"] },
},
mutations: {
SET_FEED_DATA(state, {folder_id, data}) {
state.APIData["test"] = data
}
},
Edit
Looking in Vue debugger I find the following, so it seems the mutation is setting that data but the getter can access it.
Inside the mutation, try to set it like below:
state.APIData = { ...state.APIData, test: data };
The getter is for APIData and it will not recognize the change if you update an individual property directly. You need to reset the whole APIData object.
To replace the test with a variable:
state.APIData = { ...state.APIData, [VARIABLE_NAME]: data };
For example, if you want to pass the key name as folder_id in your code:
state.APIData = { ...state.APIData, [folder_id]: data };
I have a module in my VUex state
mutations: {
appendTasks(state, tasks) {
tasks.forEach((task) => {
state.tasks[task.id] = task;
});
},
},
state: {
tasks: {},
},
getters: {
getTasks(state) {
return Object.values(state.tasks);
},
getTasksCount(state) {
return Object.keys(state.tasks).length;
},
},
I want to store tasks as dictionary because I need to access them by id. Also I need a method to get a list if tasks. But getTasks doesn't work for me. And getTasks returns an empty Array Why?
Here is the explanation of my problem and how to solve it
Just use Vue.set()
https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects
How can I watch for store values changes when params are used? I normally would do that via a getter, but my getter accepts a param which makes it tricky as I've failed to find documentation on this scenario or a stack Q/A.
(code is minimized for demo reasons)
My store.js :
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
let report = {
results: [],
};
export const store = new Vuex.Store({
state: {
broken: Object.assign({}, report),
},
results: (state) => (scan) => {
return state[scan].results
},
});
vue-component.vue :
computed: {
...mapGetters([
'results',
]),
watch: {
results(){ // How to pass the param ??
// my callback
}
So basically I would like to find out how to pass the param so my watch would work.
In my opinion, there is no direct solution for your question.
At first, for watch function, it only accept two parameters, newValue and oldValue, so there is no way to pass your scan parameter.
Also, your results property in computed, just return a function, if you watch the function, it will never be triggered.
I suggest you just change the getters from nested function to simple function.
But if you really want to do in this way, you should create a bridge computed properties
computed: {
...mapGetters([
'results',
]),
scan() {
},
mutatedResults() {
return this.results(this.scan);
},
watch: {
mutatedResults() {
}
}
}