How can I test the focus of a text field in a Vue component? - vuejs2

I am new to Vue and I've been asked to implement tests for some components.
This is the component:
<template>
<v-form>
<v-container>
<v-text-field
v-on:keydown.enter.prevent
v-model="filterName"
dense
clearable
style="transition: all 0.3s ease; width: 12rem;"
class="mt-6"
#focus="isFocused = true"
#blur="isFocused = false"
ref='filter'
>
</v-text-field>
</v-container>
</v-form>
</template>
<script>
export default {
name: "FilterBox",
data: () => ({
isFocused: false,
}),
computed: {
filterName: {
get() {
return this.$store.getters.filter.name
},
set(value) {
this.$store.commit('set_filter_name', value)
}
}
},
mounted() {
this.$refs.filter.focus()
},
}
</script>
This is the test that I implemented for that component:
import Vue from "vue";
import Vuex from "vuex";
import Vuetify from "vuetify";
import { shallowMount } from "#vue/test-utils";
import FilterBox from "#/components/FilterBox";
Vue.use( Vuex );
Vue.use( Vuetify );
describe( 'Filter Box', () => {
let store;
let getters;
let vuetify;
beforeEach( () => {
vuetify = new Vuetify();
} );
it( 'should return a valid wrapper of the component', function () {
getters = {};
store = new Vuex.Store( { getters } );
const wrapper = shallowMount( FilterBox, {
store,
vuetify
} );
expect( wrapper.exists() ).toBe( true );
} );
} );
But when I run it, I keep getting this error:
Cannot read properties of undefined (reading 'focus')
TypeError: Cannot read properties of undefined (reading 'focus')
I tried this approach:
getters = {
filter: () => {
return jest.fn()
}
};
store = new Vuex.Store( { getters } );
But then I got this error:
this.$refs.filter.focus is not a function
TypeError: this.$refs.filter.focus is not a function
Can anyone help me figure out what I'm doing wrong here? How can I test that the text field is focused on mount hook?

Try using mount(...) instead of shallowMount(...) for the tests that are testing the focus.
In your case #focus and #blur are events provided by v-textfield and accessed via ref.
Using shallowMount you don't actually render v-textfield but a stub instead.
This is useful in tests where you don't want to test the implementation of your sub-componentsn

Related

How to test a VueJS (Nuxt) Store in Jest

I have a Jest test that is testing my VueJs component (specifically Nuxt, but not important for this). I am attempting to mock a store that is an JSON Object. I for the life of me can not figure out how to test this. The error I keep getting when I run my test is "Cannot read propery 'easg_logo' of undefined".
My Vue component (footer.vue)
<template>
<div>
<v-img
:height="easg_logo_height"
:src="$store.state.app.easg_logo.src"
:width="easg_logo_width"
contain
/>
<v-img
:height="oma_logo_height"
:src="$store.state.app.oma_logo.src"
:width="oma_logo_width"
contain
/>
</div>
</template>
<script>
export default {
data(){
easg_logo_width: this.$store.state.app.easg_logo.top.width,
easg_logo_height: this.$store.state.app.easg_logo.top.height,
oma_logo_width: this.$store.state.app.oma_logo.top.width,
oma_logo_width: this.$store.state.app.oma_logo.top.width,
}
}
</script>
My test (footer.test.js)
import {shallowMount, createLocalVue} from '#vue/test-utils'
import Vuex from 'vuex';
import Footer from '#components/layouts/default/footer'
import Vuetify from 'vuetify';
const vuetify = new Vuetify();
const localVue = createLocalVue();
localVue.use(Vuex);
describe("Footer tests", ()=> {
let wrapper;
let store;
let state;
beforeEach(() => {
state= {
app: {
easg_logo:{
src: "~/assets/images/easg.jpg",
text: "EASG",
top:{
height: 72,
width: 82
}
},
oma_logo:{
src: "~/assets/images/oma.jpg",
text: "OMA",
top:{
height: 72,
width: 82
}
}
}
}
store = new Vuex.Store({
modules:{
state
}
})
})
test('store test', ()=> {
wrapper = shallowMount(Footer, {store, localVue, vuetify})
console.log(wrapper)
const a = 'a'
expect(a).toBe('a')
});
});
There is no state because state was mistakenly provided as a module.
It should be:
store = new Vuex.Store({
state
})

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

VueX actions/mutations error in Chrome console

I try to store drawer data in VueX to use it on external component.
My console error: [vuex] unknown action type: app/switchDrawer
My VueJS template:
pages/test.vue
<template>
<v-navigation-drawer v-model="drawer" app>
<v-list dense>
...
</v-list>
</v-navigation-drawer>
</template>
<script>
export default {
computed: {
drawer: {
get () {
return this.$store.state.app.drawer
},
set (value) {
console.log(value);
return this.$store.dispatch('app/toggleDrawer', value)
}
}
}
}
</script>
The console.log() function give me lot of lines in loop in console.
I'd like to use too the mapGetters class from VueX instead computed get/set:
computed: mapGetters({
drawer: 'app/drawer'
})
I've an error in console:
[Vue warn]: Computed property "drawer" was assigned to but it has no
setter.
My VueX store:
store/app.js
export const state = () => ({
drawer: true
})
export const getters = {
drawer: state => state.drawer
}
export const mutations = {
TOGGLE_DRAWER: (state) => {
state.drawer = !state.drawer
}
}
export const actions = {
toggleDrawer ({ commit }, value) {
commit('TOGGLE_DRAWER', value)
}
}
IN CASE YOU DON'T WANT TO MAKE A NEW MUTATION AND HANDLE LOCALLY. (which I preferred personally as my store is pretty big already)
Faced similar issue using when using a vue-ui library(vuesax)
Solved it by initializing a new data variable to a computed variable (the one from the store) in created hook
(Why in created hook)
created() {
this.localDrawer = this.drawer
},
data() {
return {
localDrawer: ''
}
},
computed: {
...mapGetters({
drawer: 'drawer'
})
},
watch: {
drawer(newValue, oldValue) {
this.localDrawer = newValue
}
}
Now use localDrawer in the you app.
NOTE: I am watching the drawer variable as well. So that in any case if its value changes it gets reflected.
Found your problem - a computed setter has to have no return statement.
drawer: {
get () {
return this.$store.state.app.drawer
},
set (value) {
this.$store.dispatch('app/toggleDrawer', value)
}
}
Please notice that your action submits a value to the mutation which dosen't take any value. So better add a new mutation that handles said value:
export const mutations = {
SET_DRAWER: (state, value) => {
state.drawer = value
}
}
export const actions = {
toggleDrawer ({ commit }, value) {
commit('SET_DRAWER', value)
}
}

Unknown custom element: - did you register the component correctly? error on with <nuxt /> component in default.vue Jest

I'm trying to write tests for default.vue file which has the following code:
default.vue
<template>
<div>
<top-nav :class="isSticky ? 'fixed-top stickyAnimate' : ''" />
<main>
<nuxt />
</main>
<footer />
</div>
</template>
<script>
import TopNav from '../components/TopNav.vue';
import Footer from '../components/Footer.vue';
import StickyNavMixin from '../mixins/stickyNavMixin';
export default {
components: {
TopNav,
Footer,
},
mixins: [StickyNavMixin],
data() {
return {
loading: true,
};
},
mounted() {
if (!window.location.hash) {
this.loading = false;
}
},
};
</script>
then my test look like this
default.spec.js
import { createLocalVue, shallowMount } from '#vue/test-utils';
import BootstrapVue from 'bootstrap-vue';
import StickyNavMixin from '../mixins/stickyNavMixin';
import Default from '../layouts/default.vue';
import TopNav from '../components/TopNav.vue';
import Footer from '../components/Footer.vue';
const localVue = createLocalVue();
localVue.use(BootstrapVue);
localVue.mixin(StickyNavMixin);
describe('Default', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Default, {
localVue,
});
});
test('is a Vue instance', () => {
expect(wrapper.isVueInstance()).toBeTruthy();
});
test('has navbar component', () => {
expect(wrapper.find(TopNav).exists()).toBe(true);
});
});
When I ran this test, I get error says:
[Vue warn]: Unknown custom element: - did you register the component correctly? For
recursive components, make sure to provide the "name" option.found in --->
Please guide me to a right direction. Thank you in advance!
I figured out how to get past that error. I had to just stub it out of the wrapper. You don't have to import Nuxt, just string 'nuxt' will replace it as a stubbed element in the wrapper:
describe('DefaultLayout', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
/** mount **/
test('is a Vue instance', () => {
wrapper = mount(DefaultLayout, {
localVue,
stubs: ['nuxt'],
});
expect(wrapper.isVueInstance()).toBeTruthy();
});
/** shallowMount **/
test('is a Vue instance', () => {
wrapper = shallowMount(DefaultLayout, {
localVue,
stubs: ['nuxt', 'top-nav', 'footer'],
});
expect(wrapper.isVueInstance()).toBeTruthy();
// expect(wrapper.html()).toBe('<div>'); => this is to debug see below for output
});
});
//DEBUG
"<div><top-nav-stub class=\"\"></top-nav-stub> <main><nuxt-stub></nuxt-stub> .
</main> <footer-stub></footer-stub></div>"

Quasar Unknown custom element error in unit test

I have a simple Vue component that uses Quasar button
<template>
<div>
<span class="count">{{ count }}</span>
<q-btn #click="increment">Increment</q-btn>
</div>
</template>
<script>
export default {
name: 'TestComponent',
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count += 1;
},
},
};
</script>
I create a unit test for it
import { mount, createLocalVue } from '#vue/test-utils';
import { Quasar, QBtn } from 'quasar';
import TestComponent from '../TestComponent';
describe('TestComponent', () => {
let wrapper;
beforeEach(() => {
const localVue = createLocalVue();
localVue.use(Quasar, { components: { QBtn } });
wrapper = mount(TestComponent, { localVue });
});
it('renders the correct markup', () => {
expect(wrapper.html()).toContain('<span class="count">0</span>');
});
// it's also easy to check for the existence of elements
it('has a button', () => {
expect(wrapper.contains('button')).toBe(true);
});
});
My problem:
If I run the test cases (it function) one by one at a time the test will pass. For example, remove the second it('has a button'...) then run the test. It'll pass. It's the same when removing the first it('renders the correct markup'...)
However, If I keep all test cases then run the test. The second test case will fail with an error
console.error node_modules/vue/dist/vue.common.dev.js:630
[Vue warn]: Unknown custom element: <q-btn> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
found in
---> <TestComponent>
<Root>
What am I doing wrong?
Try removing the before-each. I saw this problem too. Can't remember what exactly fixed it but this is how I have my describe block.
describe('Mount Quasar', () => {
const localVue = createLocalVue()
localVue.use(Quasar, { components })
const wrapper = shallowMount(Register, {
localVue,
stubs: ['router-link', 'router-view']
})
const vm = wrapper.vm
it('passes the sanity check and creates a wrapper', () => {
expect(wrapper.isVueInstance()).toBe(true)
})
})
You will need to import quasar into either webpack, babel, or jest.
In the jest.config.js file
Add
moduleNameMapper: {
quasar: "quasar-framework/dist/umd/quasar.mat.umd.min.js"
},