I'm using Jest with vue-test-utils trying to test if a child component reacts to an $emit event in the parent component.
VueJS test-utils library provides a parentComponent option to be passed when mounting/shallow mounting the component.
Everything is working fine except that even though I instantiate the component with a mocked Vuex store, the parent component throws a
TypeError: Cannot read property 'state' of undefined
on a this.$store.state.something.here piece of code in the parent component.
How can I mock the Vuex store there?
The component mount looks like this:
const wrapper = shallowMount(ChildComponent, {
store,
localVue,
parentComponent: ParentComponent,
mocks: {
$t: msg => msg,
},
});
Any ideas on how to solve this?
it may not be the complete answer to OP questions, but since I had been debugging around for the last 2h and finally found MY problem, I would like to posted here to help someone in the future.
I was trying to mock and mount the following component:
<template>
<div test="object-list-div">
<h1 test="component-title">{{ objectName }}</h1>
<table class="table">
<thead>
<tr test="table-row-title">
<th scope="col" test="table-column-title" v-for="(value, name, index) in objectData[0]" :key="index">{{ name }}</th>
</tr>
</thead>
<tbody>
<tr test="table-row-data" v-for="(ivalue, iname, i) in objectData" :key="i">
<td test="table-cell-data" v-for="(jvalue, jname, j) in ivalue" :key="j">{{ jvalue }}</td>
</tr>
</tbody>
</table>
</div>
export default {
props: [
'objectName',
'objectData'
],
computed: {
visibleColums() {
return this.$store.state.Config_ShowColumn;
}
}
}
with the following wrapper code
wrapper = shallowMount(ObjectList, {
mocks: {
$store: {
state: {
Config_ShowColumn: [
"Field1",
"Field2",
"Field3",
"Field4",
"Field5",
]
}
}
}
});
I got OP error, but in my case the component was expecting two Props at the moment of creation. Since it did not receive this, it got stuck.
This is working now:
import { shallowMount } from "#vue/test-utils";
import { expect } from "chai";
import ObjectList from "#/components/Object-List.vue";
wrapper = shallowMount(ObjectList, {
propsData: {
objectName: "Ticket",
objectData: [
{
Field1: "Field1",
Field2: "Field2",
Field3: "Field3",
Field4: "Field4",
Field5: "Field5",
},
]
},
mocks: {
$store: {
state: {
Config_ShowColumn: [
"Field1",
"Field2",
"Field3",
"Field4",
"Field5",
]
}
}
}
});
Hope it helps someone.
Tried the solution Richard proposed but without much success, even though his guess was right.
The solution was far simnpler than I envisioned, I just stopped instantiating the Vuex.Store and just have the mocked $store in vue-test-utils config like so:
import { createLocalVue, shallowMount, config } from '#vue/test-utils';
config.mocks.$store = {
state: {
user: {
sexy: true
},
},
};
I had no need to use an actual instance of Vuex as I only needed to mock the actual data so this worked perfectly.
How are you creating the mock store? It should be something like
const storeOptions = {
state: {...},
getters: {...},
mutations: {...}
}
const mockStore = new Vuex.Store(storeOptions)
Since this.$store is undefined, I suspect you might just be passing the options object to shallowMount.
Related
I'm using Webpack-Loader to load VueJS in my Django project. I'm very new to Vue, and i'm trying to create a simble component that fetches data from the backend using an API call and shows this data on Vuetify datatable.
The issue with my code is that while the request is executed, nothing is shown on the datatable, so the table shows but nothing is inside of it.
I'm having an hard time understanding if there is a problem with the code or if the problem is with my config or my imports.
Here is the component where i'm performing the request and show the data on the datatable:
App.vue
<template>
<v-simple-table dark>
<template v-slot:default>
<thead>
<tr>
<th class="text-left">
Asset
</th>
<th class="text-left">
Total
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in balances"
:key="item.asset">
<td>{{ item.asset }}</td>
<td>{{ item.total }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
</template>
<script>
export default {
data() {
return {
balances: [],
}
},
mounted() {
this.fetchData()
},
methods: {
fetchData() {
fetch('http://127.0.0.1:8000/binance/getbalance')
.then(response => response.json())
.then(data => {
this.balances = data
console.log(data)
})
}
}
}
</script>
main.js
import Vue from "vue/dist/vue.js";
import Vuex from "vuex";
import storePlugin from "./vuex/vuex_store_as_plugin";
import App from './App.vue'
import Vuetify from "vuetify";
import "vuetify/dist/vuetify.min.css";
Vue.use(Vuetify);
Vue.use(Vuex);
Vue.use(storePlugin);
Vue.config.productionTip = false;
new Vue({
el: '#app',
render: h => h(App),
})
Here is my vue.config.js
const BundleTracker = require("webpack-bundle-tracker");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const pages = {
'main': {
entry: './src/main.js',
chunks: ['chunk-vendors']
},
}
module.exports = {
pages: pages,
filenameHashing: false,
productionSourceMap: false,
publicPath: process.env.NODE_ENV === 'production'
? 'static/vue'
: 'http://localhost:8080/',
outputDir: '../django_vue_mpa/static/vue/',
chainWebpack: config => {
config.optimization
.splitChunks({
cacheGroups: {
moment: {
test: /[\\/]node_modules[\\/]moment/,
name: "chunk-moment",
chunks: "all",
priority: 5
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "chunk-vendors",
chunks: "all",
priority: 1
},
},
});
Object.keys(pages).forEach(page => {
config.plugins.delete(`html-${page}`);
config.plugins.delete(`preload-${page}`);
config.plugins.delete(`prefetch-${page}`);
})
config
.plugin('BundleTracker')
.use(BundleTracker, [{filename: '../vue_frontend/webpack-stats.json'}]);
// Uncomment below to analyze bundle sizes
// config.plugin("BundleAnalyzerPlugin").use(BundleAnalyzerPlugin);
config.resolve.alias
.set('__STATIC__', 'static')
config.devServer
.public('http://localhost:8080')
.host('localhost')
.port(8080)
.hotOnly(true)
.watchOptions({poll: 1000})
.https(false)
.headers({"Access-Control-Allow-Origin": ["*"]})
}
};
Versions used:
Vue 2, vuetify#2.3.21
The data is a very simple JSON array of less than 20-30 elements, it looks like this:
[{"asset": "XYZ", "total": 123}, {"asset": "XYZ2", "total": 456"} ... ]
The request is being executed, since console.log() will print the data, the table won't show any data, it's just empty.
Here are the only errors i found in my console:
vuetify.js?ce5b:42906 [Vuetify] Multiple instances of Vue detected
[Vue warn]: $attrs is readonly.
found in
---> <VSimpleTable>
<App> at src/App.vue
<Root>
[Vue warn]: $listeners is readonly.
found in
---> <VSimpleTable>
<App> at src/App.vue
<Root>
EDIT
This isn't going to append data to the table, which means that the problem is most likely not my data:
.then(data => {
this.balances = [{"asset": 1, "total": "1"}, {"asset": 2, "total": 2}, {"asset": 3, "total": 3}]
})
Vuetify didn't have simple-table in 1.xx versions. Instead they used v-data-table instead it seems.
I've made a codesandbox to test this:
https://codesandbox.io/s/vuetify-data-table-forked-4pxqb?file=/src/components/AppTable.vue
Vuetify 2.3.xx
Vuetify 1.4.xx
It looks like i was importing in the wrong way. Changing my imports to:
import Vue from "vue";
import App from "./App.vue";
import Vuetify from "vuetify";
import "vuetify/dist/vuetify.min.css";
fixed the problem entirely. I still don't understand what changes, so any kind of explanation is very welcome!
I have had problems where I am getting a null value from my store... sometimes, and only on some values. If anyone could point me in the right direction and explain why it is wrong... I would be really grateful. So here is the deal my store.getters.getApiKey is sometimes "" and sometimes not.
So... in the component vue below, it is not null on the first reference
{{this.$store.getters.getApiKey}}
and then within the mounted section, store.getters.getHostUrl is set, but store.getters.getApiKey keeps returning "".
Here are the details:
the Component.vue
<template>
<div class="countryCodes">
<p>ApiKey : {{this.$store.getters.getApiKey}}</p>
<p>CountryCode Data is {{ page }}</p>
<div class="CountryCodes">
<tr v-for="ccdata in content_list" v-bind:key="ccdata.guid">
<td>{{ ccdata.guid }}</td>
<td>{{ ccdata.name }}</td>
<td>{{ ccdata.code }}</td>
</tr>
</div>
</div>
</template>
import axios from "axios";
import store from "#/store";
export default {
name: 'CountryCodes',
data () {
return {
page: "",
content_list: []
}
},
mounted () {
axios({ method: "GET", "url": store.getters.getHostUrl + "
"/api/"+store.getters.getApiKey+"/countryCodes" }).then(result => {
this.page = result.data.page;
this.content_list = result.data.contents;
}, error => {
console.error(error);
});
},
methods: {
}
}
</script>
Then my store (store.js) looks like this...
import Vuex from "vuex";
import Vue from 'vue';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
apiKey: "",
hostUrl:""
},
mutations: {
SET_APIKEY(state, value) { state.apiKey = value; },
SET_HOST_URL(state, value) {state.hostUrl = value; }
},
getters: {
getApiKey(state) { return state.apiKey; },
getHostUrl(state) { return state.hostUrl; }
}
})
finally in my main.js I commit the data to the store... :
import Vue from 'vue'
import App from './App.vue'
import router from './router/index.js'
import store from './store.js'
new Vue({
el: '#app',
render: h => h(App),
router,
store,
mounted: function() {
console.log(this.$route.query)
store.commit('SET_APIKEY', this.$route.query.api)
store.commit("SET_HOST_URL", location.origin.toString())
}
})
I have the same problem when trying to build a http service, where the store is null for the apikey. What magic am I missing?
Usually the mounted hook of child component is called before the mounted of parent component.
From Vue Parent and Child lifecycle hooks
If you try to console.log on both mounted hook you will see the order of execution (but I'm still not sure why your store.getters.getHostUrl is set).
So you need a watcher to run your code after your store has value.
Example code:
...
computed: { // or use another getters
url () {
if (!store.getters.getHostUrl || !store.getters.getApiKey) return
return `${store.getters.getHostUrl}/api/${store.getters.getApiKey}/countryCodes`
}
},
watch: {
url (value) {
...
}
}
...
CodeSandbox
So.... there were two ways of solving this..... thank you to both comments.
Switch my mounted in the main.js to created- the diagram above explains why... as well as the nice article.
add "await store.getters.getApiKey"
If I have two components, the first one is called A:
<template>
<input required type='text' v-model.trim="number">
<input type="date" v-model="date" >
<button #click='allRecords(number,date)'>ok</button>
<table >
<thead>
<th>Coordonnées</th>
</thead>
<tbody>
<tr v-for='contact in contacts'>
<td #click="seeDetails(contact.id)" > {{ contact.data.to }}
</td>
</tr>
</tbody>
</table>
</template>
<script lang="js">
import axios from 'axios';
import Vue from 'vue';
export default {
name: 'A',
props: [],
data() {
return {
contacts: [],
number: '',
date: new Date().toISOString().slice(0,10),
nombre:0
}
},
methods: {
allRecords: function(number,date) {
axios.get(`/api/contacts?number=${number}&date=${date}`)
.then(response => {
this.contacts = response.data.list;
this.nombre = response.data.count;
})
.catch(error => {
console.log(error);
});
},
seeDetails (id) {
this.$router.push({ name: 'B', params: { id }});
},
}
</script>
the 2nd is called B:
<template>
<div> {{details.data.add }}</div>
</template>
<script lang="js">
import axios from 'axios';
export default {
name: 'B',
props: [],
mounted() {
const id = this.$router.currentRoute.params.id;
this.fetchContactData(id);
},
data() {
return {
details: []
}
},
methods: {
fetchContactData(id){
axios.get(`/api/recherche/${id}`)
.then(response => {
this.details = response.data
})
.catch(error => {
console.log(error);
});
},
},
}
</script>
I would like when I leave my component B has my component A to have the information of A which corespondent to the result that I had in B without needing to enter again the date and the number.
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
You have waded into the problem of application state, and views can differ. The recommended solution is vuex. For simple situations, I like to keep app state in a global javascript variable. Your vue components don't need to pass state, but they refer to a single source of truth outside of vue, which they can all display and modify. So you're app state is a contacts array, and your B component, which needs a better name, will just push rows onto this array. When you return to A, your page will reflect the new data.
I see you want to show the details of a specific contact based on its ID.
But is your router configured correctly?
Dynamic Route Matching:
routes: [
{ path: '/contacts/:contact', component: B }
]
See more at https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes
This was the question got me stuck for a little bit. Unfortunately, I coudn't find answer here (asking also didn't help). So after doing some research and asking here and there, it seems that I got the solution to this issue.
If you have a question that you already know the answer to, and you
would like to document that knowledge in public so that others
(including yourself) can find it later.
Of course, my answer may not be the ideal one, moreover I know it is not, that's the key point why I'm posting - to improve it.
Note, I'm not using actions in example. The idea is the same.
Let's begin with stating the problem:
Imagine we have App.vue which dynamically generates its local component named Hello.
<template>
<div id="app">
<div>
<hello v-for="i in jobs" :key="i" :id="i"></hello>
<button #click="addJob">New</button>
</div>
</div>
</template>
<script>
import Hello from './components/Hello'
export default {
components: {
Hello
}...
store.js
export const store = new Vuex.Store({
state: {
jobs: []
}
})
We are using v-for directive to generate components by iterating through an array jobs. Our store as of now consists of only state with an empty array.
Button New should do 2 things:
1) create new component Hello, in other words add element to jobs (let it be numbers), which are going to be assigned as key and id of <hello>, and passed to local component as props.
2) generate local stores - modules - to keep any data scoped to newly created components.
Hello.vue
<template>
<div>
<input type="number" :value="count">
<button #click="updateCountPlus">+1</button>
</div>
</template>
export default {
props: ['id']
}
Simple component - input with a button adding 1.
Our goal is to design something like this:
For the first operation of NEW button - generating components - we add mutation to our store.js
mutations: {
addJob (state) {
state.jobs.push(state.jobs.length + 1)
...
}
Second, creating local modules. Here we're going to use reusableModule to generated multiple instances of a module. That module we keep in separate file for convinience. Also, note use of function for declaring module state.
const state = () => {
return {
count: 0
}
}
const getters = {
count: (state) => state.count
}
const mutations = {
updateCountPlus (state) {
state.count++
}
}
export default {
state,
getters,
mutations
}
To use reusableModule we import it and apply dynamic module registration.
store.js
import module from './reusableModule'
const {state: stateModule, getters, mutations} = module
export const store = new Vuex.Store({
state: {
jobs: []
},
mutations: {
addJob (state) {
state.jobs.push(state.jobs.length + 1)
store.registerModule(`module${state.jobs.length}`, {
state: stateModule,
getters,
mutations,
namespaced: true // making our module reusable
})
}
}
})
After, we're going to link Hello.vue with its storage. We may need state, getters, mutations, actions from vuex. To access storage we need to create our getters. Same with mutations.
Home.vue
<script>
export default {
props: ['id'],
computed: {
count () {
return this.$store.getters[`module${this.id}/count`]
}
},
methods: {
updateCountPlus () {
this.$store.commit(`module${this.id}/updateCountPlus`)
}
}
}
</script>
Imagine we have lots of getters, mutations and actions. Why not use {mapGetters} or {mapMutations}? When we have several modules and we know the path to module needed, we can do it. Unfortunately, we do not have access to module name.
The code is run when the component's module is executed (when your app
is booting), not when the component is created. So these helpers can
only be used if you know the module name ahead of time.
There is little help here. We can separate our getters and mutations and then import them as an object and keep it clean.
<script>
import computed from '../store/moduleGetters'
import methods from '../store/moduleMutations'
export default {
props: ['id'],
computed,
methods
}
</script>
Returning to App component. We have to commit our mutation and also let's create some getter for App. To show how can we access data located into modules.
store.js
export const store = new Vuex.Store({
state: {
jobs: []
},
getters: {
jobs: state => state.jobs,
sumAll (state, getters) {
let s = 0
for (let i = 1; i <= state.jobs.length; i++) {
s += getters[`module${i}/count`]
}
return s
}
}
...
Finishing code in App component
<script>
import Hello from './components/Hello'
import {mapMutations, mapGetters} from 'vuex'
export default {
components: {
Hello
},
computed: {
...mapGetters([
'jobs',
'sumAll'
])
},
methods: {
...mapMutations([
'addJob'
])
}
}
</script>
Hi and thank you for posting your question and your solution.
I started learning Vuex couple days ago and came across a similar problem. I've checked your solution and came up with mine which doesn't require registering new modules. I find it to be quite an overkill and to be honest I don't understand why you do it. There is always a possibility I've misunderstood the problem.
I've created a copy of your markup with a few differences for clarity and demonstration purposes.
I've got:
JobList.vue - main custom component
Job.vue - job-list child custom component
jobs.js - vuex store module file
JobList.vue (which is responsible for wrapping the job(s) list items)
<template>
<div>
<job v-for="(job, index) in jobs" :data="job" :key="job.id"></job>
<h3>Create New Job</h3>
<form #submit.prevent="addJob">
<input type="text" v-model="newJobName" required>
<button type="submit">Add Job</button>
</form>
</div>
</template>
<script>
import store from '../store/index'
import job from './job';
export default {
components: { job },
data() {
return {
newJobName: ''
};
},
computed: {
jobs() {
return store.state.jobs.jobs;
}
},
methods: {
addJob() {
store.dispatch('newJob', this.newJobName);
}
}
}
</script>
The Job
<template>
<div>
<h5>Id: {{ data.id }}</h5>
<h4>{{ data.name }}</h4>
<p>{{ data.active}}</p>
<button type="button" #click="toggleJobState">Toggle</button>
<hr>
</div>
</template>
<script>
import store from '../store/index'
export default {
props: ['data'],
methods: {
toggleJobState() {
store.dispatch('toggleJobState', this.data.id);
}
}
}
</script>
And finally the jobs.js Vuex module file:
export default {
state: {
jobs: [
{
id: 1,
name: 'light',
active: false
},
{
id: 2,
name: 'medium',
active: false
},
{
id: 3,
name: 'heavy',
active: false
}
]
},
actions: { //methods
newJob(context, jobName) {
context.state.jobs.push({
id: context.getters.newJobId,
name: jobName,
active: false
});
},
toggleJobState(context, id) {
context.state.jobs.forEach((job) => {
if(job.id === id) { job.active = !job.active; }
})
}
},
getters: { //computed properties
newJobId(state) { return state.jobs.length + 1; }
}
}
It's possible to add new jobs to the store and as the "active" property suggest, you can control every single individual job without the need for a new custom vuex module.
I have a customer list which is actually an array of objects. I store it in Vuex. I render the list in my component and each row has a checkbox. More precisely I use keen-ui and the checkbox rendering part looks like:
<tr v-for="customer in customers" :class="{ selected: customer.selected }">
<td>
<ui-checkbox :value.sync="customer.selected"></ui-checkbox>
</td>
<td>{{ customer.name }}</td>
<td>{{ customer.email }}</td>
</tr>
So the checkbox directly changes customers array which is bad: I use strict mode in Vuex and it throws me an error.
I want to track when the array is changed and call an action in order to change the vuex state:
watch: {
'customers': {
handler() {
// ...
},
deep: true
}
However it still changes the customer directly. How can I fix this?
First and foremost, be careful when using .sync: it will be deprecated in 2.0.
Take a look at this: http://vuex.vuejs.org/en/forms.html, as this problem is solved in here. Basically, this checkbox should trigger a vuex action on input or change. Taken from the docs:
<input :value="message" #input="updateMessage">
Where updateMessage is:
vuex: {
getters: {
message: state => state.obj.message
},
actions: {
updateMessage: ({ dispatch }, e) => {
dispatch('UPDATE_MESSAGE', e.target.value)
}
}
}
If you do not wish to track the mutations, you can move the state of this component away from vuex, to be able to use v-model in all its glory.
You just have to make a custom getter and setter:
<template>
<ui-checkbox :value.sync="thisCustomer"></ui-checkbox>
</template>
<script>
//this is using vuex 2.0 syntax
export default {
thisCustomer: {
get() {
return this.$store.state.customer;
},
set(val) {
this.$store.commit('SET_CUSTOMER', val);
// instead of performing the mutation here,
// you could also use an action:
// this.$store.disptach('updateCustomer')
}
},
}
</script>
In your store:
import {
SET_CUSTOMER,
} from '../mutation-types';
const state = {
customer: null,
};
const mutations = {
[SET_CUSTOMER](state, value) {
state.customer = value;
},
}
I'm not exactly sure what your store looks like, but hopefully this gives you the idea :)
if your customers are in the root state, you can try this:
watch: {
'$store.state.customers'{
handler() {
// ...
},
deep: true
}
}
try using mapState in your component and watch the customers like you have done above.worked for me