I am trying to test the following component w Avoriaz, but upon props change , the action in watch: {} is not triggered
ItemComponent.vue
switch checkbox
✗ calls store action updateList when item checkbox is switched
AssertionError: expected false to equal true
at Context.<anonymous> (webpack:///test/unit/specs/components/ItemComponent.spec.js:35:47 <- index.js:25510:48)
thanks for feedback
ItemComponent.vue
<template>
<li :class="{ 'removed': item.checked }">
<div class="checkbox">
<label>
<input type="checkbox" v-model="item.checked"> {{ item.text }}
</label>
</div>
</li>
</template>
<script>
import { mapActions } from 'vuex'
export default {
props: ['item', 'id'],
methods: mapActions(['updateList']),
watch: {
'item.checked': function () {
this.updateList(this.id)
}
}
}
</script>
here is my component test
ItemComponent.spec.js
import Vue from 'vue'
import ItemComponent from '#/components/ItemComponent'
import Vuex from 'vuex'
import sinon from 'sinon'
import { mount } from 'avoriaz'
Vue.use(Vuex)
describe('ItemComponent.vue', () => {
let actions
let store
beforeEach(() => {
actions = {
updateList: sinon.stub()
}
store = new Vuex.Store({
state: {},
actions
})
})
describe('switch checkbox', () => {
it('calls store action updateList when item checkbox is switched', () => {
const id = '3'
const item = { text: 'Bananas', checked: true }
const wrapper = mount(ItemComponent, { propsData: { item, id }, store })
// switch item checked to false
wrapper.setProps({ item: { text: 'Bananas', checked: false } })
expect(wrapper.vm.$props.item.checked).to.equal(false)
expect(actions.updateList.calledOnce).to.equal(true)
})
})
})
U mistaked the prop,use :checked instead
I should write my expect(actions.updateList() . within a $nextTick block
describe('switch checkbox', () => {
it('calls store action updateList when item checkbox is switched', (done) => {
const id = '3'
const item = { text: 'Bananas', checked: true }
const wrapper = mount(ItemComponent, { propsData: { item, id }, store })
// switch item.checked to false
wrapper.setProps({ item: { text: 'Bananas', checked: false } })
expect(wrapper.vm.$props.item.checked).to.equal(false)
wrapper.find('input')[0].trigger('input')
wrapper.vm.$nextTick(() => {
expect(actions.updateList.calledOnce).to.equal(true)
done()
})
})
})
then my test is OK
ItemComponent.vue
switch checkbox
✓ calls store action updateList when item checkbox is switched
Related
I have a component that I want to test that uses Vuex.
components/Main/Header.vue
<template>
<v-container fluid grid-list-xl>
<v-card flat class="header" color="transparent">
<v-layout align-center justify-start row fill-height class="content">
<v-flex xs5>
<v-img :src="avatar" class="avatar" aspect-ratio="1" contain></v-img>
</v-flex>
<v-flex xs7>
<div class="main-text font-weight-black">
WELCOME
</div>
<div class="sub-text">
{{currentLocation.description}}
</div>
<div #click="showStory" class="show-more font-weight-bold">
Explore More
</div>
</v-flex>
</v-layout>
</v-card>
</v-container>
</template>
<script>
import avatar from '../../assets/images/home/avatar.png';
export default {
name: 'Header',
computed: {
currentLocation() {
return this.$store.getters.getCurrentLocation;
},
avatar() {
return avatar;
}
},
methods: {
showStory() {
this.$router.push( { name: 'Stories', params: { name: 'Our Story' } });
}
}
}
</script>
and from /test/unit/components/Main/Header.spec.js
import Vuex from 'vuex'
import { shallowMount, createLocalVue } from "#vue/test-utils"
import Header from "#/components/Main/Header.vue"
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store({
state: {
currentLocation: {
name: 'this is a name',
description: "lorem ipsum",
latitude: '1.123123',
longitude: '103.123123',
radius: '5000'
}
},
getters: {
getCurrentLocation: (state) => state.currentLocation
}
})
describe('Header', () => {
const wrapper = shallowMount(Header, {
store,
localVue
})
it('should render a computed property currentLocation', () => {
expect(Header.computed.currentLocation()).toBe(store.getters.getCurrentLocation)
});
});
The error I'm getting is from the computed property currentLocation
TypeError: Cannot read property 'getters' of undefined
I have created a Vue component and a unit test to validate its behavior. I can use the component on my Vue app without any issues, but when I run my unit test using jest, I get the error:
error TS2339: Property 'extend' does not exist on type 'typ
eof import("</path/to/module>")'.
1 export default Vue.extend({
vButton.ts - Component under test
export default Vue.extend({
template: `
<div>
<button class="default-button" #click="click">
<span>{{ text }}</span>
</button>
</div>
`,
props: {
text: String,
action: Function
},
methods: {
click(): void {
this.$emit('action');
}
}
});
vButton.spec.ts - unit test
import { mount } from '#vue/test-utils'
import vButton from '../views/core/ts/components/vButton'
describe('vButton', () => {
describe(':props', () => {
it(':text - should render a button with the passed-in label text', () => {
const msg = 'new message';
const wrapper = mount(vButton, {
propsData: { text: msg },
});
expect(wrapper.text()).toMatch(msg);
});
});
describe('#events', () => {
it('#click - should emit an "action" event when the button is clicked', () => {
const wrapper = mount(vButton);
const button = wrapper.find('button')
button.trigger('click')
expect(wrapper.emitted().action).toBeTruthy()
})
});
});
I expect Vue.extend to work fine when running the unit test.
I am testing that form data are sent after submit.
ContactForm.spec.js
import Vue from "vue";
import Vuex from "vuex";
import { mount, shallowMount } from "#vue/test-utils";
import VeeValidate from "vee-validate";
import i18n from "#/locales";
import Vuetify from "vuetify";
import ContactForm from "#/components/Home/ContactForm.vue";
Vue.use(VeeValidate, { errorBagName: "errors" });
Vue.use(Vuetify);
Vue.use(Vuex);
describe("ContactForm.vue", () => {
let store;
let wrapper;
let options;
let input;
const v = new VeeValidate.Validator();
beforeEach(() => {
const el = document.createElement("div");
el.setAttribute("data-app", true);
document.body.appendChild(el);
});
it("should sendMessage - valid form", async () => {
// given
store = new Vuex.Store({
state: {
language: "en",
loading: false
}
})
options = {
sync: false,
provide: {
$validator () {
return new VeeValidate.Validator();
}
},
i18n,
store
};
wrapper = mount(ContactForm, options);
// when
const radioInput = wrapper.findAll('input[type="radio"]');
radioInput.at(1).setChecked(); // input element value is changed, v-model is not
radioInput.at(1).trigger("change"); // v-model updated
input = wrapper.find('input[name="givenName"]');
input.element.value = "John"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('input[name="familyName"]');
input.element.value = "Doe"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('input[name="email"]');
input.element.value = "john.doe#example.com"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('textarea[name="messageContent"]');
input.element.value = "Hello World!"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
const contactForm = wrapper.find("form");
contactForm.trigger("submit");
await wrapper.vm.$nextTick();
// then
console.log("DATA: ", wrapper.vm.$data.contactLang);
expect(wrapper.vm.validForm).toEqual(true);
});
});
Validation is successful ( so validForm is set to true in the component )
BUT the test does not pass
console.log
✕ should sendMessage - valid form (476ms)
● ContactForm.vue › should sendMessage - valid form
expect(received).toEqual(expected)
Expected value to equal:
true
Received:
false
The component vue is
ContactForm.vue
<template>
<form id="contactForm" #submit="sendMessage()">
<input v-model="contactLang" type='hidden' data-vv-name="contactLang" v-validate="'required'" name='contactLang'>
<v-layout row wrap align-center>
<v-flex xs12 sm3 md3 lg3>
<v-radio-group row :mandatory="false" v-model="gender" name="gender">
<v-radio :label='genderLabel("f")' value="f" name="female"></v-radio>
<v-radio :label='genderLabel("m")' value="m" name="male"></v-radio>
</v-radio-group>
</v-flex>
<v-flex xs12 sm4 md4 lg4>
<v-text-field
v-model="givenName"
browser-autocomplete="off"
:label="$t('lang.views.home.contactForm.givenName')"
data-vv-name="givenName"
:error-messages="errors.collect('givenName')"
v-validate="'required'"
name="givenName">
</v-text-field>
</v-flex>
<v-flex xs12 sm5 md5 lg5>
<v-text-field
v-model="familyName"
browser-autocomplete="off"
:label="$t('lang.views.home.contactForm.familyName')"
data-vv-name="familyName"
:error-messages="errors.collect('familyName')"
v-validate="'required'"
name="familyName">
</v-text-field>
</v-flex>
</v-layout>
<v-text-field
browser-autocomplete="off"
v-model="email"
:label="$t('lang.views.home.contactForm.email')"
data-vv-name="email"
:error-messages="errors.collect('email')"
v-validate="'required|email'"
name="email">
</v-text-field>
<v-textarea v-model="messageContent" :label="$t('lang.views.home.contactForm.message')" :error-messages="errors.collect('messageContent')" :rules="[(v) => v.length <= 200 || 'Max 200 characters']" :counter="200" v-validate="'required'" data-vv-name="messageContent" name="messageContent"></v-textarea>
<v-btn id="btnClear" round #click.native="clear">{{ $t('lang.views.global.clear') }}</v-btn>
<v-btn round large color="primary" type="submit">{{ $t('lang.views.global.send') }}
<v-icon right>email</v-icon><span slot="loader" class="custom-loader"><v-icon light>cached</v-icon></span>
</v-btn>
</form>
</template>
<script>
import swal from "sweetalert2";
import { mapState } from "vuex";
import appValidationDictionarySetup from "#/locales/appValidationDictionary";
export default {
name: "contactForm",
$_veeValidate: { validator: "new" },
data() {
return {
contactLang: "",
gender: "f",
givenName: "",
familyName: "",
email: "",
messageContent: "",
validForm: false
};
},
...
methods: {
...
sendMessage: function() {
console.log("sendMessage()...");
this.$validator
.validateAll()
.then(isValid => {
console.log("VALIDATION RESULT: ", isValid);
this.validForm = isValid;
if (!isValid) {
console.log("VALIDATION ERROR");
// console.log("Errors: ", this.$validator.errors.items.length);
const alertTitle = this.$validator.dictionary.container[
this.language
].custom.error;
const textMsg = this.$validator.dictionary.container[this.language]
.custom.correct_all;
swal({
title: alertTitle,
text: textMsg,
type: "error",
confirmButtonText: "OK"
});
return;
}
console.log("validation success, form submitted validForm: ", this.validForm);
return;
})
.catch(e => {
// catch error from validateAll() promise
console.log("error validation promise: ", e);
this.validForm = false;
return;
});
},
clear: function() {
this.contactLang = "";
this.gender = "f";
this.givenName = "";
this.familyName = "";
this.email = "";
this.messageContent = "";
this.validForm = false;
this.$validator.reset();
}
},
mounted() {
appValidationDictionarySetup(this.$validator);
this.$validator.localize(this.language);
this.contactLang = this.language;
}
};
</script>
</style>
and the console.log debugging is
console.log src/components/Home/ContactForm.vue:90
sendMessage()...
console.log tests/unit/ContactForm.spec.js:242
DATA: en
console.log src/components/Home/ContactForm.vue:94
VALIDATION RESULT: true
It's weird to see that the DATA ( contactLang ) value is false in the console.log from the spec ... displayed before the validation result
console.log src/components/Home/ContactForm.vue:90
sendMessage()...
console.log tests/unit/ContactForm.spec.js:242
DATA: en
console.log src/components/Home/ContactForm.vue:94
VALIDATION RESULT: true
console.log src/components/Home/ContactForm.vue:112
validation success, form submitted validForm: true
I guess there is async problem ... timout ?
thanks for feedback
SOLVED
It's actually a timeout issue
expect.assertions(1); // Otherwise jest will give a false positive
await contactForm.trigger("submit");
// then
setTimeout(() => {
expect(wrapper.vm.validForm).toEqual(true);
}, 2000);
I propose to use jest faketimers
jest.useFakeTimers()
contactForm.trigger("submit");
await wrapper.vm.$nextTick();
// then
jest.runTimersToTime(2000)
expect(wrapper.vm.validForm).toEqual(true);
I suggest to first make the test fail to avoid false positives
for more information about jest faketimers
https://jestjs.io/docs/en/timer-mocks.html
i did a simple test for my login form of component, in case it helps someone
it("submit form call method login", () => {
const login = jest.fn()
const wrapper = shallowMount(Login, {
methods: {
login
}
})
wrapper.findAll("v-form").at(0).trigger("submit")
expect(login).toBeCalled()
})
I am trying to test the following App.vue component when a click event is fired on the logout vue-router link...
App.vue
<template>
<div id="app">
<header id="header">
<nav>
<ul class="navigation">
<li id="home"><router-link :to="{ name: 'home' }">Home</router-link></li>
<li id="login" v-if="!isAuthenticated"><router-link :to="{ name: 'login' }">Login</router-link></li>
<li id="shoppinglists" v-if="isAuthenticated"><router-link :to="{ name: 'shoppinglists' }" >Shopping Lists</router-link></li>
<li id="logout" v-if="isAuthenticated">Logout</li>
</ul>
</nav>
</header><!-- /#header -->
<section id="page">
<router-view></router-view>
</section><!-- /#page -->
</div>
</template>
<script>
import store from '#/vuex/store'
import router from '#/router/index'
import { mapGetters } from 'vuex'
export default {
name: 'app',
computed: {
...mapGetters({ isAuthenticated: 'isAuthenticated' })
},
methods: {
logout () {
this. $store.dispatch('logout')
.then(() => {
window.localStorage.removeItem('vue-authenticate.vueauth_token')
this/$router.push({ name: 'home' })
})
}
},
store,
router
}
</script>
To test the logout click, I preset the isAuthenticated state to true, so the logout router link show up and I trigger the click event on it.
LOG: 'navigation: ', <ul class="navigation"><li id="home">
Home</li> <!---->
<li id="shoppinglists">Shopping Lists
</li> <li id="logout">Logout</li></ul>
I expect the action logout to have been called .. but it's not ... why ?
App.spec.js
import Vue from 'vue'
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import App from '#/App'
import router from '#/router/index'
import { mount } from 'avoriaz'
import sinon from 'sinon'
Vue.use(Vuex)
Vue.use(VueRouter)
describe('App.vue', () => {
let actions
let getters
let store
beforeEach(() => {
getters = {
isAuthenticated: (state) => {
return state.isAuthenticated
}
}
actions = {
logout: sinon.stub().returns(Promise.resolve(true))
}
store = new Vuex.Store({
getters,
actions,
state: {
isAuthenticated: true,
currentUserId: ''
}
})
router
})
it('calls logout method', () => {
const wrapper = mount(App, { router, store })
console.log('navigation: ', wrapper.find('ul.navigation')[0].element)
const logoutLink = wrapper.find('#logout a')[0]
logoutLink.trigger('click')
expect(actions.logout.calledOnce).to.equal(true)
})
})
vuex/actions.js
import { IS_AUTHENTICATED, CURRENT_USER_ID } from './mutation_types'
import getters from './getters'
export default {
logout: ({commit}) => {
commit(IS_AUTHENTICATED, { isAuthenticated: false })
commit(CURRENT_USER_ID, { currentUserId: '' })
return true
}
}
vuex/mutations.js
import * as types from './mutation_types'
import getters from './getters'
export default {
[types.IS_AUTHENTICATED] (state, payload) {
state.isAuthenticated = payload.isAuthenticated
},
[types.CURRENT_USER_ID] (state, payload) {
state.currentUserId = payload.currentUserId
}
}
vuex/getters.js
export default {
isAuthenticated: (state) => {
return state.isAuthenticated
}
}
Finally , I found a way to test it :
import Vue from 'vue'
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import App from '#/App'
import router from '#/router/index'
import { mount } from 'avoriaz'
import sinon from 'sinon'
Vue.use(Vuex)
Vue.use(VueRouter)
describe('App.vue', () => {
let actions
let getters
let store
let sandbox
let routerPush
beforeEach(() => {
sandbox = sinon.sandbox.create()
getters = {
isAuthenticated: (state) => {
return state.isAuthenticated
}
}
actions = {
logout: sandbox.stub().returns(Promise.resolve(true))
}
store = new Vuex.Store({
getters,
state: {
isAuthenticated: true,
currentUserId: ''
},
actions
})
router
})
afterEach(() => {
sandbox.restore()
})
it('calls logout method', (done) => {
const wrapper = mount(App, { store, router })
routerPush = sandbox.spy(wrapper.vm.$router, 'push')
const logoutLink = wrapper.find('#logout a')[0]
logoutLink.trigger('click')
wrapper.vm.$nextTick(() => {
expect(actions.logout.calledOnce).to.equal(true)
actions.logout().then(() => {
expect(routerPush).to.have.been.calledWith('/home')
})
done()
})
})
})
I am testing my App.vue , and I am stuck with a Vuex error on getters...
I guess it's related to a badly define getters property , but I don't see how to solve it ..
feeedback welcome
Console.log
ERROR LOG: '[vuex] unknown getter: getLists'
App.vue
✗ calls store action addShoppingList when a click event is fired from the plus-sign icon
AssertionError: expected false to equal true
at Context.<anonymous> (webpack:///test/unit/specs/App.spec.js:33:50 <- index.js:24490:51)
App.spec.js
import App from '#/App'
import Vue from 'vue'
import Vuex from 'vuex'
import sinon from 'sinon'
import { mount } from 'avoriaz'
Vue.use(Vuex)
describe('App.vue', () => {
let actions
let getters
let store
beforeEach(() => {
actions = {
addShoppingList: sinon.stub(),
populateShoppingLists: sinon.stub()
}
getters = {
shoppinglists: () => 'getLists'
}
store = new Vuex.Store({
state: {},
actions,
getters
})
})
it('calls store action addShoppingList when a click event is fired from the plus-sign icon', (done) => {
const wrapper = mount(App, { store })
wrapper.find('a')[0].trigger('click')
wrapper.vm.$nextTick(() => {
expect(actions.createShoppingList.calledOnce).to.equal(true)
done()
})
})
App.vue
<template>
<div id="app" class="container">
<ul class="nav nav-tabs" role="tablist">
<li :class="index===shoppinglists.length-1 ? 'active' : ''" v-for="(list, index) in shoppinglists" :key="list.id" role="presentation">
<shopping-list-title-component :id="list.id" :title="list.title"></shopping-list-title-component>
</li>
<li>
<a href="#" #click="addShoppingList">
<i class="glyphicon glyphicon-plus-sign"></i>
</a>
</li>
</ul>
<div class="tab-content">
<div :class="index===shoppinglists.length-1 ? 'active' : ''" v-for="(list, index) in shoppinglists" :key="list.id" class="tab-pane" role="tabpanel" :id="list.id">
<shopping-list-component :id="list.id" :title="list.title" :items="list.items"></shopping-list-component>
</div>
</div>
</div>
</template>
<script>
import ShoppingListComponent from './components/ShoppingListComponent'
import ShoppingListTitleComponent from './components/ShoppingListTitleComponent'
import store from './vuex/store'
import { mapGetters, mapActions } from 'vuex'
import _ from 'underscore'
export default {
components: {
ShoppingListComponent,
ShoppingListTitleComponent
},
computed: {
...mapGetters({ shoppinglists: 'getLists' })
},
methods: _.extend({}, mapActions(['populateShoppingLists', 'createShoppingList']), {
addShoppingList () {
let list = { title: 'New Shopping List', items: [] }
this.createShoppingList(list)
}
}),
store,
mounted: function () {
this.$nextTick(function () {
this.populateShoppingLists()
})
}
}
</script>
UPDATE
here are my getters.js anf store.js files
store.js is imported in App.vue
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters' // import getters !
import actions from './actions'
import mutations from './mutations'
Vue.use(Vuex)
const state = {
shoppinglists: []
}
export default new Vuex.Store({
state,
mutations,
getters,
actions
})
getters.js
import _ from 'underscore'
export default {
getLists: state => state.shoppinglists,
getListById: (state, id) => {
return _.findWhere(state.shoppinglists, { id: id })
}
}
You don't define a getLists getter in your store. You're defining a shoppinglists getter that returns 'getLists'.
You need to either change your mapGetters line to be:
...mapGetters(['shoppinglists'])
Or change the name of the getter to getLists:
getters = {
getLists: () => 'getLists'
}
(Although I'm not sure if you are really meaning to return a string value in that getter or not)
#thanksd put me on tracks... see my comment
so I need to define the getters in my Vur.spec.js as following
getters = {
getLists: () => {
// console.log('WE ARE S TEST')
state => state.shoppinglists
}
}