Vue3 with Jest stub functionality does not stub - unit-testing

Component:
<template>
<div id="fileviewer" class="min-h-full">
<section class="gap-4 mt-4">
<div class="bg-medium-50 w-1/3 p-4">
<FileUpload ></FileUpload>
</div>
<div class="bg-medium-50 w-2/3 p-4">
<FileViewer></FileViewer>
</div>
</section>
</div>
</template>
<script>
import FileUpload from "#/components/FileUpload";
import FileViewer from "#/components/FileViewer";
export default {
name: "FileManager",
components: { FileUpload, FileViewer },
};
</script>
Test:
import { mount } from "#vue/test-utils";
import FileManager from '#/views/FileManager';
describe('FileManager.vue', () =>{
it('should mount', () => {
const wrapper = mount(FileManager, {
global: {
stubs: {
FileUpload: true,
FileViewer: true
}
}
})
expect(wrapper).toBeDefined()
})
})
Does not work for me as per the docs. No special installations. Instead, The framework wants to do the 'import' statements for the child components and then fails because I do not want to mock out 'fetch' for this one component. Any Ideas?
"vue-jest": "^5.0.0-alpha.9"
"#vue/test-utils": "^2.0.0-rc.6"
"vue": "^3.0.0",
Thanks for help.

I. If you want to stub all child components automatically you just can use shallowMount instead of mount.
II. If you want so use mount anyway try to fix your stubs like that:
global: {
stubs: {
FileUpload: {
template: '<div class="file-upload-or-any-class-you-want">You can put there anything you want</div>'
},
FileViewer: {
template: '<div class="file-viewer-or-any-class-you-want">You can put there anything you want</div>'
}
}
}
Or you can define your stubs before tests as I always do. For example:
const FileUploadStub = {
template: '<div class="file-upload-or-any-class-you-want">You can put there anything you want</div>'
}
const FileViewerStub: {
template: '<div class="file-viewer-or-any-class-you-want">You can put there anything you want</div>'
}
And then use stubs in mount or shallowMount:
global: {
stubs: {
FileUpload: FileUploadStub,
FileViewer: FileViewerStub
}
}

Related

Best way to mock/stub vue-i18n translations in a vue3 component when using Vitest

I have started to replace Jest with Vitest for my unit test library in my Vue 3 App.
I am trying to write unit test for a component that uses the vue-i18n library to translate text within it but when I try to mount this component in my test file, it fails with the error:
ReferenceError: t is not defined
What is the proper way to stub/mock t from import { useI18n } from 'vue-i18n' when writing tests using the vitest library?
Note since upgrading from Vue2 to Vue3 this does not work:
const wrapper = shallowMount(MyComponent, {
global: {
mocks: {
$t: () => {}
}
}
})
Here is a list of some notable package versions:
"vue": "^3.2.31",
"vue-i18n": "^9.2.0-beta.14",
"vite": "^2.9.0",
"vitest": "^0.10.2"
Thanks!
import { createI18n } from 'vue-i18n';
describe('xxx', () => {
it('yyy', () => {
const i18n = createI18n({
messages: {
gb: {},
nl: {},
...
}
});
const wrapper = mount(YourComponent, {
global: {
plugins: [i18n]
}
});
}
})
I suppose you want to mock this globally, no need to put same code in every test suite.
// vitest.config.ts
import { mergeConfig } from 'vite';
import { defineConfig } from 'vitest/config';
import viteConfig from './vite.config';
export default defineConfig(
mergeConfig(viteConfig, { // extending app vite config
test: {
setupFiles: ['tests/unit.setup.ts'],
environment: 'jsdom',
}
})
);
// tests/unit.setup.ts
import { config } from "#vue/test-utils"
config.global.mocks = {
$t: tKey => tKey; // just return translation key
};
Panos Vakalopoulos’s answer worked for me.
And the code could be run globally.
See https://test-utils.vuejs.org/migration/#no-more-createlocalvue
// vite.config.ts
export default defineConfig(
// add config for test
test: {
environment: 'jsdom',
setupFiles: 'vitest.setup.ts',
}
);
// vitest.setup.ts'
import { config } from '#vue/test-utils'
import { createI18n } from 'vue-i18n'
const i18n = createI18n()
config.global.plugins = [i18n]
// YourComponent.vue
<div id="app">
<p>{{ t("message.hello") }}</p>
</div>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
// component_test.ts
describe('xxx', () => {
it('yyy', () => {
const wrapper = mount(YourComponent);
}
})
Note that if you use global config as $t, Luckylooke's answer would work.
// YourComponent.vue
<div id="app">
<p>{{ $t("message.hello") }}</p>
</div>
// tests/unit.setup.ts
import { config } from "#vue/test-utils"
config.global.mocks = {
$t: tKey => tKey; // just return translation key
};
I read this tutorial that teaches to mock vue-router, then I made a similar solution for vue-i18n and it worked.
Component (HelloWorld.vue)
<script setup>
import { useI18n } from "vue-i18n";
const { t } = useI18n();
</script>
<template>
<div class="greetings">
<h1>{{ t("commonsmessagehello") }}</h1>
<h2>{{ t("localhello") }}</h2>
<h2>{{ $t("message.success") }}</h2>
</div>
</template>
<i18n src="../commons/locales.json"></i18n>
<i18n>
{
"enUS": {
"localhello": "local helloooooo"
}
}
</i18n>
Test
import { describe, it, expect, vi } from "vitest";
import { mount, config } from "#vue/test-utils";
import { useI18n } from "vue-i18n";
import HelloWorld from "../HelloWorld.vue";
vi.mock("vue-i18n");
useI18n.mockReturnValue({
t: (tKey) => tKey,
});
config.global.mocks = {
$t: (tKey) => tKey,
};
describe("HelloWorld", () => {
it("renders properly", () => {
const wrapper = mount(HelloWorld, { });
expect(wrapper.text()).toContain("message.success");
});
});
How you can see, it worked for t and $t.
That's not the ideal way. Someday I'll try to figure out how to do it globally for every test.

NuxtJS , Unit Test language picker with Jest and nuxt-i18n

I have a component that switch Language of a nuxtjs application using nuxt-i18n as follows
<template>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link langpicker">{{ $t("language_picker") }} </a>
<div class="navbar-dropdown is-hidden-mobile">
<div>
<nuxt-link
v-if="currentLanguage != 'en'"
class="navbar-item"
:to="switchLocalePath('en')"
>
<img src="~/static/flags/us.svg" class="flagIcon" /> English
</nuxt-link>
<nuxt-link
v-if="currentLanguage != 'el'"
class="navbar-item"
:to="switchLocalePath('el')"
>
<img src="~/static/flags/el.svg" class="flagIcon" /> Ελληνικά
</nuxt-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LangPicker",
computed: {
currentLanguage() {
return this.$i18n.locale || "en";
}
}
};
</script>
I want to write a Unit Test that test the correct language switch on 'nuxt-link' click.
So far I have the following
import { mount, RouterLinkStub } from "#vue/test-utils";
import LangPicker from "#/components/layout/LangPicker";
describe("LangPicker with locale en", () => {
let cmp;
beforeEach(() => {
cmp = mount(LangPicker, {
mocks: {
$t: msg => msg,
$i18n: { locale: "en" },
switchLocalePath: msg => msg
},
stubs: {
NuxtLink: RouterLinkStub
}
});
});
it("Trigger language", () => {
const el = cmp.findAll(".navbar-item")
});
});
cmp.find(".navbar-item") return an empty object.
I don't know how I must set up to "trigger" the click event.
const el = cmp.findAll(".navbar-item")[1].trigger("click");
make sure your find selector is correct.
const comp = cmp.find(".navbar-item");
comp.trigger('click');
you can use chrome dev tools selector utility.
Refer this link for detailed information.

Testing VueJS component containing other components written by myself

I work on a website with multiple components that contain other components. Now I would like to test if the save button of a form is deactivated correctly if no data is set. I am using vuetify for the UI and Jest for testing.
Here is my parent component, containing the edit-user-details component:
<template>
<v-container>
<v-form v-model="valid">
<v-card>
<v-card-text>
<edit-user-details :user="user"></edit-user-details>
</v-card-text>
<v-card-actions>
<v-btn :disabled="!valid" #click="save()">Save</v-btn>
<v-btn #click="cancel()">Cancel</v-btn>
</v-card-actions>
</v-card>
</v-form>
</v-container>
</template>
<script>
export default {
name: "edit-user",
components: {},
data: () => ({
user: {},
valid: false
}),
methods: {
save() {
...
},
cancel() {
...}
}
}
}
</script>
This is a part of the edit-user-details component:
<template>
<v-container>
<v-text-field
v-model="user.userName"
label="Username*"
required
:rules="[v => !!v || 'Please, enter a user name.']"
></v-text-field>
...
</v-container>
</template>
<script>
export default {
name: "edit-user-details",
props: {
user: {
type: Object,
default: () => ({})
}
}
}
</script>
And here we have my test:
import { mount, createLocalVue } from '#vue/test-utils'
import EditUser from '../../src/views/EditUser'
import Vuetify from 'vuetify';
import EditUserDetails from '../../src/components/EditUserDetails'
describe('Edited user data ', () => {
it('can be saved if valid', () => {
const localVue = createLocalVue();
localVue.use(Vuetify)
localVue.use(EditUserDetails)
const wrapper = mount(EditUser, {
localVue: localVue
});
})
})
The test is green because it has no assert. The main issue is, that I get this error: [Vue warn]: Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the "name" option.
So my question is: How can I test a component containing other components written by me?
Thank you in advance for your help.
instead of mount, use shallowMount.
Like mount, it creates a Wrapper that contains the mounted and
rendered Vue component, but with stubbed child components.
https://vue-test-utils.vuejs.org/api/#shallowmount
I haven’t tried this together with createLocalVue(), but I hope it'll help:
import Vuetify from 'vuetify'
const vuetify = new Vuetify()
const wrapper = mount(Component, { ..., vuetify })

Vue.js Vuex unit test failing , [vuex] unknown getter:

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

Vue component testing using Karma: 'undefined is not an object'

I am working on an app which was created with the Vue loader's webpack template.
I included testing with Karma as an option when creating the project, so it was all set up and I haven't changed any of the config.
The app is a Github user lookup which currently consists of three components; App.vue, Stats.vue and UserForm.vue. The stats and form components are children of the containing app component.
Here is App.vue:
<template>
<div id="app">
<user-form
v-model="inputValue"
#go="submit"
:input-value="inputValue"
></user-form>
<stats
:username="username"
:avatar="avatar"
:fave-lang="faveLang"
:followers="followers"
></stats>
</div>
</template>
<script>
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
import _ from 'lodash'
import UserForm from './components/UserForm'
import Stats from './components/Stats'
Vue.use(VueAxios, axios)
export default {
name: 'app',
components: {
UserForm,
Stats
},
data () {
return {
inputValue: '',
username: '',
avatar: '',
followers: [],
faveLang: '',
urlBase: 'https://api.github.com/users'
}
},
methods: {
submit () {
if (this.inputValue) {
const api = `${this.urlBase}/${this.inputValue}`
this.fetchUser(api)
}
},
fetchUser (api) {
Vue.axios.get(api).then((response) => {
const { data } = response
this.inputValue = ''
this.username = data.login
this.avatar = data.avatar_url
this.fetchFollowers()
this.fetchFaveLang()
}).catch(error => {
console.warn('ERROR:', error)
})
},
fetchFollowers () {
Vue.axios.get(`${this.urlBase}/${this.username}/followers`).then(followersResponse => {
this.followers = followersResponse.data.map(follower => {
return follower.login
})
})
},
fetchFaveLang () {
Vue.axios.get(`${this.urlBase}/${this.username}/repos`).then(reposResponse => {
const langs = reposResponse.data.map(repo => {
return repo.language
})
// Get most commonly occurring string from array
const faveLang = _.chain(langs).countBy().toPairs().maxBy(_.last).head().value()
if (faveLang !== 'null') {
this.faveLang = faveLang
} else {
this.faveLang = ''
}
})
}
}
}
</script>
<style lang="stylus">
body
background-color goldenrod
</style>
Here is Stats.vue:
<template>
<div class="container">
<h1 class="username" v-if="username">{{username}}</h1>
<img v-if="avatar" :src="avatar" class="avatar">
<h2 v-if="faveLang">Favourite Language: {{faveLang}}</h2>
<h3 v-if="followers.length > 0">Followers ({{followers.length}}):</h3>
<ul v-if="followers.length > 0">
<li v-for="follower in followers">
{{follower}}
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'stats',
props: [
'username',
'avatar',
'faveLang',
'followers'
]
}
</script>
<style lang="stylus" scoped>
h1
font-size 44px
.avatar
height 200px
width 200px
border-radius 10%
.container
display flex
align-items center
flex-flow column
font-family Comic Sans MS
</style>
And here is UserForm.vue:
<template>
<form #submit.prevent="handleSubmit">
<input
class="input"
:value="inputValue"
#input="updateValue($event.target.value)"
type="text"
placeholder="Enter a GitHub username..."
>
<button class="button">Go!</button>
</form>
</template>
<script>
export default {
props: ['inputValue'],
name: 'user-form',
methods: {
updateValue (value) {
this.$emit('input', value)
},
handleSubmit () {
this.$emit('go')
}
}
}
</script>
<style lang="stylus" scoped>
input
width 320px
input,
button
font-size 25px
form
display flex
justify-content center
</style>
I wrote a trivial test for UserForm.vue which test's the outerHTML of the <button>:
import Vue from 'vue'
import UserForm from 'src/components/UserForm'
describe('UserForm.vue', () => {
it('should have a data-attribute in the button outerHTML', () => {
const vm = new Vue({
el: document.createElement('div'),
render: (h) => h(UserForm)
})
expect(vm.$el.querySelector('.button').outerHTML)
.to.include('data-v')
})
})
This works fine; the output when running npm run unit is:
UserForm.vue
✓ should have a data-attribute in the button outerHTML
However, when I tried to write a similarly simple test for Stats.vue based on the documentation, I ran into a problem.
Here is the test:
import Vue from 'vue'
import Stats from 'src/components/Stats'
// Inspect the generated HTML after a state update
it('updates the rendered message when vm.message updates', done => {
const vm = new Vue(Stats).$mount()
vm.username = 'foo'
// wait a "tick" after state change before asserting DOM updates
Vue.nextTick(() => {
expect(vm.$el.querySelector('.username').textContent).toBe('foo')
done()
})
})
and here is the respective error when running npm run unit:
ERROR LOG: '[Vue warn]: Error when rendering root instance: '
✗ updates the rendered message when vm.message updates
undefined is not an object (evaluating '_vm.followers.length')
I have tried the following in an attempt to get the test working:
Change how the vm is created in the Stats test to be the same as the UserForm test - same error is returned
Test individual parts of the component, for example the textContent of a div in the component - same error is returned
Why is the error referring to _vm.followers.length? What is _vm with an underscore in front? How can I get around this issue to be able to successfully test my component?
(Repo with all code: https://github.com/alanbuchanan/vue-github-lookup-2)
Why is the error referring to _vm.followers.length? What is _vm with an underscore in front?
This piece of code is from the render function that Vue compiled your template into. _vm is a placeholder that gets inserted automatically into all Javascript expressions when vue-loader converts the template into a render function during build - it does that to provide access to the component.
When you do this in your template:
{{followers.length}}
The compiled result in the render function for this piece of code will be:
_vm.followers.length
Now, why does the error happen in the first place? Because you have defined a prop followers on your component, but don't provide any data for it - therefore, the prop's value is undefined
Solution: either you provide a default value for the prop:
// Stats.vue
props: {
followers: { default: () => [] }, // function required to return fresh object
// ... other props
}
Or you propvide acual values for the prop:
// in the test:
const vm = new Vue({
...Stats,
propsData: {
followers: [/* ... actual data*/]
}
}).$mount()