Vue JS JEST test - vue.js

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

Related

Jest: How I should change the mock data of Vuex in each test?

I've been working in a test where I need the data from Vuex. However, I'm having some problems, I need to change that data in each test in order to test the functionality of the component.
Here is my component:
<template>
<div id="cb-items-displayer" #click="textClick">
<span>(</span>
<p>{{ text }}</p>
<span>)</span>
</div>
</template>
<script lang="ts" setup>
import { capitalize } from '#/utils/capitalize'
import { ItemsDisplayer } from '#/models/ItemsDisplayer'
import { computed, PropType } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const props = defineProps({
type: {
type: String,
default: '',
},
menuType: {
type: String,
default: '',
},
items: {
type: Array as PropType<ItemsDisplayer[]>,
default: () => [],
}
})
const emit = defineEmits<{
(event: 'textClicked'): void
}>()
const text = computed(() => {
const param = props.menuType === 'radio' ? 'One' : 'Many'
console.log( "TYPEEE ", props.type, " ", param )
const itemsIds = store.getters['filters/get' + capitalize(props.type) + param]
console.log("ITEMSSS", JSON.stringify(itemsIds))
return getTextToShow(itemsIds)
})
const getTextToShow = (itemsIds: string) => {
//TODO - improve it
if (itemsIds === 'all') {
return 'all'
} else if (itemsIds.length === 0) {
return '-'
} else if (itemsIds.length === 1) {
return getName(itemsIds[0], props.items)
} else {
return itemsIds.length
}
}
const textClick = () => {
emit('textClicked')
}
const getName = (id: string, items: ItemsDisplayer[]) => {
const found: ItemsDisplayer = items.find((x) => x.id! === id) as ItemsDisplayer
console.log("GETNAME ", found.name)
return found?.name
}
</script>
And this is the test:
import { render, screen, click, waitFor } from '#tests/app-test-utils'
import ItemsDisplayer from './ItemsDisplayer.vue'
import { capitalize } from '#/utils/capitalize'
let mockStoreCommit: jest.Mock
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: [],
},
commit: mockStoreCommit,
}),
}))
describe('ItemsDisplayer', () => {
beforeEach(() => {
mockStoreCommit = jest.fn()
render(
ItemsDisplayer,
{},
{
props: {
type: 'categories',
menuType: 'checkbox',
items: [
{
box_templates:"",
id:"1",
name:"Culture"
},
{
box_templates:"",
id:"2",
name:"Economy"
},
{
box_templates:"",
id:"3",
name:"Education"
}
]},
}
)
})
it('renders the component', async() => {
await screen.getByText('-')
})
it('renders the component with one item', async() => {
//DON'T WORK HERE THERE SHOULD BE A CHANGE OF DATA IN THE MOCKED STORE IN ORDER TO WORK
await screen.getByText('Culture')
})
})
My problem is that I need to change the value of [filters/get${capitalize('categories')}Many] to ["1"] in the second test.
I tried several things in order to change the mocked data but they don't work. How can I change the mocked store data in each test?
Thanks!
You can achieve this by lazy loading your vue component:
Add jest.resetModules(); in the beforeEach to reset all of the imported modules before each test so they can be re-evaluated and re-mocked:
beforeEach(() => {
jest.resetModules();
In each unit test, you will first need to import the vue component using the require syntax as follows:
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
Then add the mock directly after the import with the [`filters/get${capitalize('categories')}Many`] value being set to whatever you want it to be:
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
I have noticed that you do your rendering in the beforeEach. Unfortunately because you import and mock your modules during the test, the rendering will need to be done after these have taken place - hence you will need to either move that logic into your unit test or extract it into another function which can be called from within the unit test.
Each unit test should look something like this:
it('renders the component', async() => {
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
// beforeEach logic here or a call to a function that contains it
await screen.getByText('-')
})

Vue Test Utils - Unable to correctly mount/shallow mount component, wrapper undefined

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 };
}
};

Vue composition API computed property test

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

Vue Test Mock Promise-Based Action within Module

I unfortunately can't attach all code or create a gist because the project I'm working on is related to work but I can give enough detail that I think it will work.
I'm trying to mock a call to an action that is stored in a different module but for the life of me I can't figure out how to. I'm able to create a Jest spy on the store.dispatch method but I want to be able to resolve the promise and make sure that the subsequent steps are taken.
The method in the SFC is
doSomething(data) {
this.$store.dispatch('moduleA/moduleDoSomething',{data: data})
.then(() => {
this.$router.push({name: 'RouteName'})
})
.catch(err => {
console.log(err)
alert('There was an error. Please try again.')
})
},
This is what my test looks like:
import { mount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import Vuetify from 'vuetify'
import Component from '#/components/Component'
import moduleA from '#/store/modules/moduleA'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Vuetify)
describe('Component.vue', () => {
let actions
let store
const $router = []
beforeEach(() => {
actions = {
moduleDoSomething: jest.fn((payload) => {
return Promise.resolve({
status: 200,
})
})
}
store = new Vuex.Store({
state: {},
modules: {
moduleA: {
actions
}
},
})
})
it('does something', () => {
const wrapper = mount(Component, {
store,
localVue,
mocks: {
$router,
},
})
let button = wrapper.find('button that calls doSomething')
button.trigger('click')
expect(actions.moduleDoSomething).toHaveBeenCalled()
expect(wrapper.vm.$router[0].name).toBe('RouteName')
})
})
The following test passes, but I don't want to just test that the action was dispatched; I also want to test things in the "then" block.
it('does something', () => {
const dispatchSpy = jest.spyOn(store, 'dispatch')
const wrapper = mount(Component, {
store,
localVue,
mocks: {
$router,
},
})
let button = wrapper.find('button that calls doSomething')
button.trigger('click')
expect(dispatchSpy).toHaveBeenCalledWith('moduleA/moduleDoSomething',{data: data})
})
})
I managed to solve this problem by simply making the module namespaced in the mocked store.
store = new Vuex.Store({
state: {},
modules: {
moduleA: {
actions,
namespaced: true
}
},
})
I'll delete the question in a little bit

key.charAt is not a function error in createLocalVue()

Using createLocalVue function throws a TypeError: key.charAt is not a function. The interesting thing is that it is thrown only in the case of some components while I use localVue in every components tests. Here is a sample test code:
import assign from 'lodash/assign'
import { mount, createLocalVue } from '#vue/test-utils'
import filters from '#/filters'
import { DateUtils } from '#/filters/date-utils'
import TaskCard from '#/components/molecules/TaskCard'
import Icon from '#/components/atoms/Icon'
const localVue = createLocalVue()
describe('TaskCard.vue', () => {
let wrapper
const task = {
Status: 3,
Name: 'Sample task',
Deadline: Date.now(),
Owner: {
DisplayName: 'Admin'
},
Title: 'Sample title'
}
const position = 2
beforeEach(() => {
wrapper = mount(TaskCard, {
localVue,
propsData: {
task,
position
}
})
})
it('TaskCard should be a Vue instance and should have a div root element with the class "ca-task-card"', () => {
expect(wrapper.isVueInstance()).toBe(true)
expect(wrapper.is('div')).toBe(true)
expect(wrapper.classes()).toContain('ca-task-card')
})
it('TaskCard\'s ca-task-order element should have the mocked status prop', () => {
expect(wrapper.find('.ca-task-order').find('span').text()).toBe(position.toString())
})
it('TaskCard\'s first "p" element should contain the modified task name as text', () => {
const newTask = assign(task, { Name: 'Sample task name' })
wrapper.setProps({task: newTask})
expect(wrapper.findAll('p').at(0).text()).toBe('Sample task name')
})
it('TaskCard should have 4 Icon component children', () => {
expect(wrapper.findAll(Icon).length).toBe(4)
})
it('TaskCard should have the formatted deadline of the task in the element with the class "ca-task-card__deadline"', () => {
expect(wrapper.find('.ca-task-card__deadline').text()).toBe(DateUtils.formatDate(wrapper.vm.task.Deadline))
})
it('TaskCard with completed status should have completed icon and not any others', () => {
expect(wrapper.find('.ca-task-card__icon--completed').exists()).toBe(true)
expect(wrapper.find('.ca-task-card__icon--delegated').exists()).toBe(false)
})
it('TaskCard with delegated status should have delegated icon and not any others', () => {
const newTask = assign(task, {
Status: 2,
Name: 'Sample delegated task'
})
const newWrapper = mount(TaskCard, {
localVue,
propsData: {
task: newTask,
position
}
})
expect(newWrapper.find('.ca-task-card__icon--completed').exists()).toBe(false)
expect(newWrapper.find('.ca-task-card__icon--delegated').exists()).toBe(true)
})
})
Here is the full stacktrace:
FAIL test\unit\specs\molecules\TaskCard.spec.js
● Test suite failed to run
TypeError: key.charAt is not a function
6 | import Icon from '#/components/atoms/Icon'
7 |
> 8 | const localVue = createLocalVue()
9 |
10 | describe('TaskCard.vue', () => {
11 | let wrapper
at Object.has (node_modules/vue/dist/vue.common.js:1932:50)
at baseGetTag (node_modules/#vue/test-utils/dist/vue-test-utils.js:448:48)
at baseClone (node_modules/#vue/test-utils/dist/vue-test-utils.js:5047:15)
at node_modules/#vue/test-utils/dist/vue-test-utils.js:5086:31
at arrayEach (node_modules/#vue/test-utils/dist/vue-test-utils.js:4304:9)
at baseClone (node_modules/#vue/test-utils/dist/vue-test-utils.js:5080:3)
at node_modules/#vue/test-utils/dist/vue-test-utils.js:5086:31
at arrayEach (node_modules/#vue/test-utils/dist/vue-test-utils.js:4304:9)
at baseClone (node_modules/#vue/test-utils/dist/vue-test-utils.js:5080:3)
at node_modules/#vue/test-utils/dist/vue-test-utils.js:5086:31
at arrayEach (node_modules/#vue/test-utils/dist/vue-test-utils.js:4304:9)
at baseClone (node_modules/#vue/test-utils/dist/vue-test-utils.js:5080:3)
at cloneDeep (node_modules/#vue/test-utils/dist/vue-test-utils.js:5116:10)
at node_modules/#vue/test-utils/dist/vue-test-utils.js:5141:11
at Array.forEach (<anonymous>)
at createLocalVue (node_modules/#vue/test-utils/dist/vue-test-utils.js:5137:20)
at Object.<anonymous> (test/unit/specs/molecules/TaskCard.spec.js:8:46)