I starting to use the composition API in a new Vue project and start developing with TDD. Also, I start to use TypeScript.
I create a very simple Component Logo.vue
<template>
<div>
<div class="c-logo__fullname">{{ fullName }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '#vue/composition-api'
interface User {
firstName: string
lastName: number
}
export default defineComponent({
props: {
user: {
type: Object as () => User,
required: true,
},
},
setup({ user }) {
const fullName = computed(() => `${user.firstName} ${user.lastName}`)
return {
fullName,
}
},
})
</script>
And now I starting to test very basic stuff in Logo.spec.js file
import { mount } from '#vue/test-utils'
import Logo from '#/components/Logo.vue'
let wrapper = null
const user = {
firstName: 'Test',
lastName: 'User',
}
beforeEach(() => {
wrapper = mount(Logo, {
propsData: {
user,
},
})
})
afterEach(() => {
wrapper.destroy()
})
describe('Logo', () => {
test('is a Vue instance', () => {
expect(wrapper.vm).toBeTruthy()
})
test('User has firstname "Test"', () => {
expect(wrapper.props().user.firstName).toBe('Test')
})
test('User has lastname "User"', () => {
expect(wrapper.props().user.lastName).toBe('User')
})
test('User has fullname "Test User"', () => {
expect(wrapper.find('.c-logo__fullname').text()).toBe('Test User')
})
})
My problem now is that the test for the output of my computed property fullName fails every time.
FAIL test/Logo.spec.js
Logo
✓ is a Vue instance (11 ms)
✓ User has firstname "Test" (2 ms)
✓ User has lastname "User" (2 ms)
✕ User has fullname "Test User" (7 ms)
● Logo › User has fullname "Test User"
expect(received).toBe(expected) // Object.is equality
Expected: "Test User"
Received: ""
35 |
36 | test('User has fullname "Test User"', () => {
> 37 | expect(wrapper.find('.c-logo__fullname').text()).toBe('Test User')
| ^
38 | })
39 | })
40 |
at Object.<anonymous> (test/Logo.spec.js:37:54)
Also when I make a console.log(wrapper.html()) I get only an empty div-container. Why?
I followed some articles to start with TDD and Vue and also composition API, but I tried now a lot of scenarios without passing the test for the computed property.
I'm happy about every little help you can give me and save my day.
I had the same problem. It looks like you need to include the composition API in the test, and then attach it to the vue instance:
import CompositionApi from '#vue/composition-api'
const localVue = createLocalVue()
beforeEach(() => {
localVue.use(CompositionApi)
})
As described here Vue composition API computed property test
Related
I have a strange behaviour when trying to run a JEST unit test.
import { shallowMount } from '#vue/test-utils'
import communicationPreferences from '#/pages/account/communication-preferences'
describe('Communication Preferences Page', () => {
it('should render page', () => {
const wrapper = shallowMount(communicationPreferences)
expect(wrapper.exists()).toBe(true)
})
})
Page: communication-preferences.vue
computed: {
...mapState('account', ['communicationPreferences']),
// communicationPreferenceTypeEmail() {
// return this.communicationPreferences.filter((e) => e.type === 'EMAIL')
// },
// communicationPreferenceTypeNotEmail() {
// return this.communicationPreferences.filter((e) => e.type !== 'EMAIL')
// },
},
When I run npm run test with computed lines above uncomment I get the error below but when I comment them out I have a successful test pass.
TypeError: Cannot read property 'state' of undefined
127 | ...mapState('account', ['communicationPreferences']),
128 | communicationPreferenceTypeEmail() {
> 129 | return this.communicationPreferences.filter((e) => e.type === 'EMAIL')
| ^
130 | },
131 | communicationPreferenceTypeNotEmail() {
132 | return this.communicationPreferences.filter((e) => e.type !== 'EMAIL')
Cannot read property 'state' of undefined but I don't understand why, anything obvious I am missing here?
This happens when trying mapState (or other mapXXX Vuex utilities) without an initialized Vuex store.
Solution
One way to fix this is to pass in the store via the global.plugins mounting option:
import { shallowMount } from '#vue/test-utils'
import communicationPreferences from '#/pages/account/communication-preferences'
import store from '#/store'
describe('Communication Preferences Page', () => {
it('should render page', () => {
const wrapper = shallowMount(communicationPreferences,
{ 👇
global: {
plugins: [store],
},
}
)
expect(wrapper.exists()).toBe(true)
})
})
demo
I am working on a project built on Vue3 and composition API and writing test cases.
The component I want to test is like below.
Home.vue
<template>
<div>
<Child #onChangeValue="onChangeValue" />
</div>
</template>
<script lang="ts>
...
const onChangeValue = (value: string) => {
store.dispatch("changeValueAction", {
value: value,
});
};
</scirpt>
Now I want to test if changeValueAction has been called.
Home.spec.ts
...
import { key, store } from '#/store';
describe("Test Home component", () => {
const wrapper = mount(Home, {
global: {
plugins: [[store, key]],
},
});
it("Test onChangeValue", () => {
const child = wrapper.findComponent(Child);
child.vm.$emit("onChangeValue", "Hello, world");
// I want to check changeValueAction has been called.
expect(wrapper.vm.store.state.moduleA.value).toBe("Hello, world");
});
});
I can confirm the state has actually been updated successfully in the test case above but I am wondering how I can mock action and check if it has been called.
How can I do it?
I have sort of a similar setup.
I don't want to test the actual store just that the method within the component is calling dispatch with a certain value.
This is what I've done.
favorite.spec.ts
import {key} from '#/store';
let storeMock: any;
beforeEach(async () => {
storeMock = createStore({});
});
test(`Should remove favorite`, async () => {
const wrapper = mount(Component, {
propsData: {
item: mockItemObj
},
global: {
plugins: [[storeMock, key]],
}
});
const spyDispatch = jest.spyOn(storeMock, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
expect(spyDispatch).toHaveBeenCalledWith("favoritesState/deleteFavorite", favoriteId);
});
This is the Component method:
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", favoriteId);
}
return {
removeFavorite
}
}
Hope this will help you further :)
I was trying to cover the codes to increase the coverage
report percentage,
How to cover the if statement inside vue methods?
In my case using #vue/test-utils:"^1.1.4" and vue: "^2.6.12" package version, FYI, And below is my actual component,
<template>
<div :class="iconcls" >
<el-image
ref='cal-modal'
class="icons"
#click="handleRedirectRouter(urlname)"
:src="require(`#/assets/designsystem/home/${iconurl}`)"
fit="fill" />
<div class="desc" >{{ icondesc }}</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
#Component({
components: {}
})
class IconHolder extends Vue {
#Prop({ default: "" }) iconcls!: any;
#Prop({ default: "" }) iconurl!: any;
#Prop({ default: "" }) icondesc!: any;
#Prop({ default: "" }) urlname!: any;
handleRedirectRouter(url: string) {
if (url !== "") {
this.$router.push({ name: url });
}
}
}
export default IconHolder;
</script>
Coverage Report for Iconholder.vue component
EDIT 2 : Ater #tony updation,
i have tried with this below test suties but still getting errors,
import Vue from "vue";
import Vuex from "vuex";
import IconHolder from '#/components/designsystem/Home/IconHolder.vue';
import ElementUI, { Image } from "element-ui";
import { mount, createLocalVue } from '#vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(ElementUI, {
Image
});
Vue.component('el-image', Image);
describe("IconHolder.spec.vue", () => {
it('pushes route by name', async () => {
const push = jest.fn();
const wrapper = mount(IconHolder, {
propsData: {
iconcls:"dshomesec5_comp_icons",
icondesc:"about",
iconurl:"components_icn_15.svg",
urlname: 'about'
},
mocks: {
$router: {
push
}
}
})
await wrapper.findComponent({ name: 'el-image' }).trigger('click');
expect(push).toHaveBeenCalledWith({ name: 'about' });
})
})
ERROR REPORT:
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: {"name": "about"}
Number of calls: 0
30 | })
31 | await wrapper.findComponent({ name: 'el-image' }).trigger('click');
> 32 | expect(push).toHaveBeenCalledWith({ name: 'about' })
Create a unit test that runs that method with a non-empty string for url.
Mount the component with an initial non-empty urlname prop.
Mock the $router.push method with jest.fn(), which we'll use to verify the call later.
Find the el-image component that is bound to that method (as a click handler).
Trigger the click event on that component.
Verify $router.push was called with the specified urlname.
it('pushes route by name', () => {
/* 2 */
const push = jest.fn()
const wrapper = shallowMount(IconHolder, {
/* 1 */
propsData: {
urlname: 'about'
},
/* 2 */
mocks: {
$router: {
push
}
}
})
/* 3 👇*/ /* 4 👇*/
await wrapper.findComponent({ name: 'el-image' }).trigger('click')
/* 5 */
expect(push).toHaveBeenCalledWith({ name: 'about' })
})
I've tried almost everything I can think of but I'm unable to correctly mount/shallow mount my vue components for testing correctly. Everytime I console.log the wrapper I get the following print out:
VueWrapper {
isFunctionalComponent: undefined,
_emitted: [Object: null prototype] {},
_emittedByOrder: []
}
This question is similar to this question asked here:
Vue-test-utils wrapper undefined
I'm using Vuetify, Vuex and Vue Router. My test.spec.ts is below:
import { shallowMount, createLocalVue, mount } from "#vue/test-utils"
import Vuex from "vuex"
import Vuetify from "vuetify"
import VueRouter from "vue-router"
import TheExamAnswer from "#/components/common/TheExamAnswer.vue"
describe("TheExamAnswer.vue", () => {
const localVue = createLocalVue()
let getters: any
let store: any
let vuetify: any
let router: any
beforeEach(() => {
localVue.use(Vuex)
localVue.use(Vuetify)
localVue.use(VueRouter)
getters = {
getExam: () => true,
}
store = new Vuex.Store({
modules: {
// Need to add FlightPlanning for name spacing
FlightPlanning: {
namespaced: true,
getters,
},
},
})
vuetify = new Vuetify()
router = new VueRouter()
})
it("Renders the element if the exam has been submitted", () => {
const wrapper = mount(TheExamAnswer, { localVue, store, router })
console.log("This is the HTML", wrapper.html())
expect(wrapper.text()).toContain("Show Answer")
})
})
My view component is very simple and the code is below:
<template>
<div v-if="submitted" class="div">
<v-btn #click="answerHidden = !answerHidden" class="mb-10"
>Show Answer</v-btn
>
<div v-if="!answerHidden">
<slot name="questionAnswer"></slot>
</div>
</div>
</template>
<script>
export default {
data: () => {
return {
answerHidden: true,
}
},
computed: {
submitted() {
const exam = this.$store.getters["FlightPlanning/getExam"]
return exam.submitted
},
},
}
</script>
<style></style>
UPDATED: I've added the suggestion from the answer below however now I"m getting the following message.
TheExamAnswer.vue
✕ Renders the element if the exam has been submitted (49ms)
● TheExamAnswer.vue › Renders the element if the exam has been submitted
expect(received).toContain(expected) // indexOf
Expected substring: "Show Answer"
Received string: ""
38 | const wrapper = mount(TheExamAnswer, { localVue, store, router })
39 | console.log("This is the HTML", wrapper.html())
> 40 | expect(wrapper.text()).toContain("Show Answer")
| ^
41 | })
42 | })
43 |
at Object.it (tests/unit/test.spec.ts:40:28)
console.error node_modules/vuetify/dist/vuetify.js:43612
[Vuetify] Multiple instances of Vue detected
See https://github.com/vuetifyjs/vuetify/issues/4068
If you're seeing "$attrs is readonly", it's caused by this
console.log tests/unit/test.spec.ts:39
This is the HTML
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
As you can see the HTML is blank and therefore I'm presuming that's also the same reason it's failing this test as the received string is "".
SOLUTION -
I figured it out. The error was on my behalf by not looking at the logic of the computed property correctly.
In my test I had:
getters = {
getExam: () => true,
}
In my component I had:
computed: {
submitted() {
const exam = this.$store.getters["FlightPlanning/getExam"]
return exam.submitted
},
If you look at the logic of the computed property it going to take whats returned from the getter and assign it to the exam variable. Originally I was returning true, because that's what I wanted the submitted() function to return this means when I call exam.submitted I'm calling it on a boolean value which obviously gives me "undefined". The solution was to return exactly what the computed property was designed to deal with, an object i.e. {submitted:true}
Therefore the final test looks like this and is returning valid HTML.
import { shallowMount, createLocalVue, mount } from "#vue/test-utils"
import Vuex from "vuex"
import Vuetify from "vuetify"
import VueRouter from "vue-router"
import TheExamAnswer from "#/components/common/TheExamAnswer.vue"
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Vuetify)
localVue.use(VueRouter)
describe("test.vue", () => {
let getters: any
let store: any
let vuetify: any
let router: any
beforeEach(() => {
getters = {
getExam: () => {
return { submitted: true }
},
}
store = new Vuex.Store({
modules: {
// Need to add FlightPlanning for name spacing
FlightPlanning: {
namespaced: true,
getters,
},
},
})
vuetify = new Vuetify()
router = new VueRouter()
})
it("Renders the element if the exam has been submitted", () => {
const wrapper = mount(TheExamAnswer, { localVue, vuetify, store, router })
console.log("This is the HTML", wrapper.html())
})
})
This gives me the result of:
console.log tests/unit/test.spec.ts:44
This is the HTML <div><button type="button" class="mb-10 v-btn v-btn--contained theme--light v-size--default"><span class="v-btn__content">Show Answer</span></button>
<!---->
</div>
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 5.235s
Ran all test suites.
The console.log with that weird input for wrapper or elements is normal behaviour. I ran couple tests with your components and everything was working.
it("Renders the element if the exam has been submitted", () => {
const wrapper = mount(TheExamAnswer, { localVue, store, router });
expect(wrapper.text()).toContain("Show Answer");
});
If you want to console.log html in your wrapper:
console.log(wrapper.html())
UPDATED: the reason, why wrapper.html() return empty string is v-if="submitted" on your root component. The computed property return undefined, because getter return true, so true.submitted return undefined
Getter in test.spec.ts:
getters = {
getExam: () => {
return { submitted: true };
}
};
I'm trying to stub a tel input plugin I'm using in one of my Vue component.
This is what I've tried, I think I almost got it but I'm missing one small detail.
This is my spec file:
import { mount, createLocalVue } from '#vue/test-utils';
import ModalNewCustomer from "../../../resources/js/subviews/customers/components/ModalNewCustomer";
// Stubs
const telInput = {
template: `<input type="tel">`
}
import moment from 'moment';
describe('Modal New Customer',() => {
let wrapper,mockCheckNameDuplicate;
let districts = [
{"id":1,"name":"Aberdeen"},
{"id":2,"name":"Admiralty"},
{"id":3,"name":"Ap Lei Chau"},
{"id":4,"name":"Beacon Hill"},
{"id":5,"name":"Braemar Hill"},
{"id":6,"name":"Causeway Bay"},
{"id":8,"name":"Central"}
];
let newCustomerData ={
firstName: 'Loulou',
lastName: 'Le Loup',
emailAddress: 'email#google.com',
sex: 'M',
primaryPhoneNumber: '+85200001111',
secondaryPhoneNumber: '+85233334444',
birthday: "2020-11-30",
wayOfContact: 'email',
electroOrNot: 1,
asiaMilesMembership: '00001111'
};
beforeEach(() => {
mockCheckNameDuplicate = jest.spyOn(ModalNewCustomer.methods, 'checkNameDuplicate');
wrapper = mount(ModalNewCustomer,{
localVue,
propsData:{
districts: districts
},
data(){
return{
COMPANY_NAME: 'HK'
}
},
router,
stubs:{
"vue-tel-input": telInput
}
});
flushPromises();
jest.resetModules();
jest.clearAllMocks();
});
afterEach(() => {
flushPromises();
jest.resetModules();
jest.clearAllMocks();
})
it('can input new customer data and submit it',async() => {
const {vm} = wrapper;
let primaryPhoneNumberInput = wrapper.find('#primaryPhoneNumber');
await primaryPhoneNumberInput.setValue(newCustomerData.primaryPhoneNumber)
expect(primaryPhoneNumberInput.element.value).toBe(newCustomerData.primaryPhoneNumber);
console.log(primaryPhoneNumberInput.html())
console.log(vm.newCustomer.primaryPhoneNumber)
});
});
In my vue component, the vue tel input looks like this:
<vue-tel-input
id="primaryPhoneNumber"
:default-country="COMPANY_NAME"
validCharactersOnly
v-model="newCustomer.primaryPhoneNumber"
:inputOptions="inputOptions">
</vue-tel-input>
The problem is when I set the value of my stub component, the data of the Vue component I'm testing is not being updated, when it should be rerendered thanks to v-model.
Thanks for your help.