I have created a project in vuetify and now trying to unit test it using jest.
I have button that I have created using v-btn, which toggles the value of a boolean variable.
To check whether the button has been clicked, I tried to get hold of the button using the class name I gave to v-btn, but its not working
I have tried using shallow mounting and plain mounting. I tried to capture the tag (v-btn) itself, but it didn't work
some.vue
<template>
<div>
<v-btn class="profile-button" #click="isProfile = !isProfile">Profile</v-btn>
<v-btn icon color="#fff" class="expand" small fab absolute right #click="mini = !mini">
<v-icon color="#fcbc00">chevron_right</v-icon>
</v-btn>
</div>
</template>
<script>
export default {
data() {
return {
isProfile: true,
mini: true
}
}
}
</script>
some.spec.js
import { shallowMount } from '#vue/test-utils';
import Profile from '#/components/Profile.vue';
import Vue from 'vue'
import Vuetify from 'vuetify'
describe('Profile.vue', () => {
Vue.use(Vuetify)
it('Profile is a vue instance', () => {
const wrapper = shallowMount(Profile);
expect(wrapper.isVueInstance()).toBeTruthy()
});
it('Profile Button exists', () => {
const wrapper = shallowMount(Profile);
expect(wrapper.contains('.profile-button')).toBe(true)
});
it('Profile Button is clicked', () => {
const wrapper = shallowMount(Profile);
expect(wrapper.contains('.profile-button')).trigger('click')
});
it('Has an expand button', () => {
const wrapper = shallowMount(Profile)
wrapper.find('expand').trigger('click')
})
});
All the tests should be passed. But I receive the following error:
expect(received).toBe(expected) // Object.is equality
Expected: true
Received: false
19 |
20 | it('Profile Button exists', () => {
> 21 | expect(wrapper.contains('.profile-button')).toBe(true)
| ^
22 | })
[vue-test-utils]: find did not return expand, cannot call trigger()
on empty Wrapper
16 | it('Has an expand button', () => {
17 | const wrapper = shallowMount(SideDrawer)
> 18 | wrapper.find('expand').trigger('click')
| ^
19 | })
20 | });
What should I do? I have many such buttons that I need to test and I am stuck, as I tried everything.
Got the solution!
I was asserting the value of wrapper.find('.profile-button') with true.
It should be changed to:
(wrapper.find('.profile-button').exists()).toBe(true)
(wrapper.find('.profile-button').exists()) returns a boolean value, which is what we want to assert.
Related
I have a VueJS where I have created a component for rendering the contents from a WYSIWYG component (tiptap).
I have the following content being returned from the backend
let x = 0;
enum A {}
function Baa() {}
I'm using highlight.js to highlight this code snippet in the following manner:
import { defineComponent, h, nextTick, onMounted, onUpdated, ref, watch } from 'vue';
// No need to use a third-party component to highlight code
// since the `#tiptap/extension-code-block-lowlight` library has highlight as a dependency
import highlight from 'highlight.js'
export const WYSIWYG = defineComponent({
name: 'WYSIWYG',
props: {
content: { type: String, required: true },
},
setup(props) {
const root = ref<HTMLElement>(null);
const highlightClass = 'hljs';
const hightlightCodes = async () => {
console.log(root.value?.querySelectorAll('pre code')[0]);
setTimeout(() => {
root.value?.querySelectorAll('pre code').forEach((el: HTMLElement) => {
highlight.highlightElement(el as HTMLElement);
});
}, 2000);
}
onMounted(hightlightCodes);
watch(() => props.content, hightlightCodes);
return function render() {
return h('div', {
class: 'WYSIWYG',
ref: root,
innerHTML: props.content
});
};
},
});
Now, when I visit the page by typing the URL in the browser, it highlights the typescript code
Whenever I visit a different page and click on my browser's "Go back" button, it makes the code completely vanishes
What I have tried
I can see that the line root.value?.querySelectorAll('pre code') is returning the correct items and the correct code is present but the code vanishes after the 2 seconds passes - due to setTimeout.
How can I make highlight.js highlight the code parts whenever props.content changes?
Option 1
Use Highlight.js Vue integration (you need to setup the plugin first, check the link):
<script setup>
const props = defineProps({
content: { type: String, required: true },
})
</script>
<template>
<highlightjs :code="content" language="ts" />
</template>
Option 2
Use computed to reactively compute highlighted HTML of props.content
Use sync highlight(code, options) function to get the highlighted HTML
Use HTML as-is via innerHTML prop or v-html directive
<script setup>
import { computed } from 'vue'
import highlight from 'highlight.js'
const props = defineProps({
content: { type: String, required: true },
})
const html = computed(() => {
const { value } = highlight.highlight(props.content, { lang: 'ts' })
return value
})
</script>
<template>
<div v-html="html" />
</template>
I can't get my click-event test to work.
I'm using a Vuetify component: v-btn, but my click-event doesn't seem to dispatch. I tried using a normal button tag, but that had the same outcome. I'm new at testing, this is actually my first run, so tips and pointer are welcome.
This is my component being tested:
<template>
<div>
<div class="marked">
<h2>Clicks: {{ counter }}</h2>
</div>
<v-btn #click="addOne" :style="buttonStyle">Counter</v-btn>
</div>
</template>
<script>
export default {
name: "UnitTesting",
data() {
return {
counter: 0
};
},
computed: {
count() {
return this.counter;
},
buttonStyle() {
return {
border: "solid 2px blue",
background: "black",
padding: "5px 12px",
color: "white",
borderRadius: "8px"
};
}
},
methods: {
addOne() {
this.counter++;
}
}
};
</script>
I have a simple test here to see if the component does mount, checks the content of the title, and tries to dispatch an event for the button, failing:
// Library
import Vue from "vue"
import Vuetify from "vuetify";
// Utils
import UnitTesting from "../UnitTesting.vue";
import { mount, createLocalVue } from '#vue/test-utils'
const localVue = createLocalVue()
Vue.use(Vuetify);
describe("UnitTesting.vue", () => {
let vuetify;
beforeEach(() => {
vuetify = new Vuetify()
})
it("Testing UnitTesting Component", () => {
const wrapper = mount(UnitTesting, {
localVue,
vuetify,
});
expect(wrapper).toBeTruthy()
});
it("Testing button", () => {
const wrapper = mount(UnitTesting, {
localVue, vuetify
});
const event = jest.fn();
const button = wrapper.find(".v-btn");
expect(button.text()).toContain("Counter")
const title = wrapper.find(".marked");
expect(title.text()).toContain("Clicks: 0");
button.vm.$on("action-btn:clicked", event);
expect(event).toHaveBeenCalledTimes(0);
button.trigger("click");
expect(event).toHaveBeenCalledTimes(1);
})
})
As you can see, my test breaks when it expects the click-event to have been dispatched:
FAIL src/views/__tests__/UnitTesting.spec.js
● UnitTesting.vue › Testing button
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
37 | expect(event).toHaveBeenCalledTimes(0);
38 | button.trigger("click");
> 39 | expect(event).toHaveBeenCalledTimes(1);
| ^
40 | })
41 | })
at Object.<anonymous> (src/views/__tests__/UnitTesting.spec.js:39:19)
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 2 passed, 3 total
Snapshots: 0 total
Time: 2.249 s
Did I do something wrong?
The problem seems to be the event name you're listening to. There is no action-btn:clicked event from v-btn. However, there is a click event. Changing your event name resolves the issue:
//button.vm.$on("action-btn:clicked", event); // DON'T DO THIS
button.vm.$on("click", event);
Try mocking like addOne instead of event
You need to mock the actual event instead you are creating new one which component is not aware of
wrapper.vm.addOne = jest.fn();
expect(wrapper.vm.addOne).toHaveBeenCalledTimes(1);
v-menu uses a transition internally to show the given Element when the activator is triggered.
We simply want to test if the menu element is shown after the activator hit it.
In our setup we have a component test using Vue Test Utils and do:
const wrapper = createWrapper(...)
expect(wrapper.contains('.my-class')).toBe(false) // the Element is not there in the beginning
clickTheTriggerElement(wrapper) // trigger v-menu to show
await wrapper.vm.$nextTick()
expect(wrapper.find('.my-class').isVisible()).toBe(true) // fails
This fails because the transition used by v-menu is not finished yet. (It would after a setTimeout(...), thou but this is less acceptable).
Vue Test Utils describes this as a common issue and provides a solution that seems to work for plain vue.js Transitions. (They use a mock for Transition to render immediately.) Unfortunately this doesn't seem to work with internal transitions of vuetify.
How would you handle that?
I wrote unit-test for v-list inside v-menu and I didn't mock transitions. This is my code:
//ACTION
const button = wrapper.find('.theme-picker');
await button.trigger('click');
const topic = wrapper.find('.v-list-item');
await topic.trigger('click');
const result = wrapper.vm.$router;
//ASSERT
expect(result).toContainEqual(expected);
If you have some async behaviour which you can't 'reach' you can use:
test('description', (done) => {
wrapper.vm.$nextTick(() => {
expect(result).toBe(expected);
done();
});
});
It work for Jest and Mocha. Test will wait until async code finish.
May be it will help someone.
I don't think it's the transitions that are causing the problem here. You should be able to disable transitions completely with the following code:
import { config } from '#vue/test-utils';
config.stubs.transition = false;
I tried this, and it did not help my situation. What I ended up doing was stub out VMenu entirely. I created a component stub somewhere only accessible to tests, e.g. tests/unit/__stubs__/VMenuStub.vue
<template>
<div class="v-menu-stub">
<slot
name="activator"
:on="{
click: toggleValue,
}"
></slot>
<slot v-if="value"></slot>
</div>
</template>
<script lang="ts">
/**
* VMenuStub - used to stub out v-menu
* as a workaround for issues dealing with v-menu directly in a unit test
* example usage: use the "stubs" option for mount
* componentWrapper = mount(component, {
* ... other options, like localVue, vuetify ...
* stubs: {
* 'v-menu': VMenuStub,
* },
* };
*/
import { defineComponent } from '#vue/composition-api';
export default defineComponent({
props: {
value: {
type: Boolean,
required: true,
},
},
setup(props, context) {
function toggleValue() {
context.emit('input', !props.value);
}
return {
toggleValue,
};
},
});
</script>
I can use this stub like so:
import VMenuStub from '../../__stubs__/VMenuStub';
it('test', () => {
componentWrapper = mount(component, {
... other options, like localVue, vuetify ...
stubs: {
'v-menu': VMenuStub,
},
};
});
once the stub is added, whichever button I have set up to trigger menu open/close will work as expected.
given my component to test has a menu with activator slot:
<v-menu
v-model="menu"
>
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on">
ClickMe
</v-btn>
</template>
I can open the menu by triggering click via trigger('click') or vm.$emit('click')
it('test', () => {
componentWrapper = mount(component, {
... other options, like localVue, vuetify ...
stubs: {
'v-menu': VMenuStub,
},
};
const menuButton = componentWrapper.findAll('.v-btn').filter(i => i.text().includes('ClickMe')).at(0);
// either of these will work
await menuButton.trigger('click');
// OR
await menuButton.vm.$emit('click');
// and I can verify
expect(componentWrapper.vm.menu).toEqual(true);
});
I'm using bootstrap as my design framework and have been using bootstrap-vue. Now I would like to implement some tests to go along with my components. I'm writing a very simple test to make sure a modal is opened. What do I use in vue-test-utils to open the bootstrap-vue modal?
I'm using the basics that come with Laravel, bootstrap-vue, vue-test-utils, mocha, and mocha-webpack.
I'm trying to open the modal with wrapper.find('#modal-1').trigger('click'). I've tried using a directive <b-button v-b-modal.modal-1> I've tried using an event <b-button #click="$bvModal.show('modal-1')">. And lastly, I tried a regular button <button #click="showModal = true"> with this on the b-modal <b-modal v-model="showModal">. I've also tried adding a $nextTick in between the trigger and the assertion.
import { createLocalVue, mount } from '#vue/test-utils';
import expect from 'expect';
import BootstrapVue from 'bootstrap-vue';
import MyComponent from '#/components/MyComponent.vue';
const localVue = createLocalVue();
localVue.use(BootstrapVue);
describe ('MyComponent', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(QuotesExceptions, {
localVue
});
});
it ('opens a modal', () => {
expect(wrapper.contains('#modal-1')).toBe(false);
wrapper.find('#btnShow').trigger('click');
expect(wrapper.contains('#modal-1')).toBe(true);
});
});
I'm expecting the modal to be in the wrapper with expect(wrapper.contains('#modal-1')).toBe(true) and this is where the assertion is failing.
Use the attachToDocument: true mount option, as modal needs to be in the document in order to open.
You can see how BootstrapVue tests their modals at https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/modal/modal.spec.js
I was looking at the bootstrap-vue test on github as Troy suggested (https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/modal/modal.spec.js)
There you can see that they are using the prop static: true. Adding this to my code solved my problem.
component.vue
<b-modal
v-model="showModal"
id="myModal"
data-qa="importModal"
:static="true"
>
</b-modal>
component.spec.js
it ('opens a modal', (done) => {
const button = wrapper.find('[data-qa="button"]');
const modal = wrapper.find('#myModal');
expect(button.exists()).toBe(true);
expect(button.is('button')).toBe(true);
expect(modal.exists()).toBe(true);
expect(modal.is('div')).toBe(true);
expect(modal.isVisible()).toBe(false);
button.trigger('click');
Vue.nextTick(() => {
expect(modal.isVisible()).toBe(true);
done();
});
});
I had to select the modal by id because the inner part is getting display: none. When I put a data-qa on the modal it sticks on the outer element which is not hidden itself. Another solution for that would be to select it the following way:
const modal = wrapper.find('[data-qa="modal"] .modal');
But I still get the following warning in my console:
[BootstrapVue warn]: observeDom: Requires MutationObserver support.
it ('opens a modal', (done) => {
const button = wrapper.find('[data-qa="button"]');
expect(!!document.querySelector('myModal')).toBe(false)
button.trigger('click');
expect(!!document.querySelector('myModal')).toBe(true)
});
I'm using jest to test if a modal appears when a button is clicked adn ended up with related problem. Will leave my answer here for furture readers.
I was trying to do something like:
let openModalBtn = wrapper.find('button[id="openModal"]');
expect(openModalBtn.exists()).toBe(true); // OK
expect(openModalBtn.is("button")).toBe(true); // OK
await deleteBtn.trigger("click"); // BANG
Leading to this:
[Vue warn]: Error in v-on handler (Promise/async): "TypeError: this.$refs.confirmDialogue[0].show is not a function"
The thing was that I was using shallowMount:
// not good
describe("some test", () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(MyComponent, {...});
});
And at some point of MyComponent, it needs to do something like:
...
methods: {
openModal() {
await this.$refs.confirmDialogue[0].show();
...
},
Therefore this.$ref was comming as undefined.
Using mount instead:
// good
describe("some test", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(MyComponent, {...});
});
allowed me to click that button and find the stub within the wrapper.html().
So make sure to mount your components if you gonna need to access stubs from it.
In my case this work perfectly,
Here I have a b-modal in template with a id="modal-1" and when the button is clicked bootstrap-vue modal is opened using showModal() method.
Try this:
<template>
<b-button v-on:click="showModal()">
<b-modal id="modal-1"></b-modal>
</template>
<script>
methods: {
showModal() {
this.$root.$emit("bv::show::modal", 'modal-1', "#btnShow");
},
}
</script>
I have a vuejs modal component that shows the modal of team members (avatar, full_name & description). I need to test it.
teamMembers.js looks like :
<template lang="pug">
.col-lg-4.col-md-6.team_member_wrapper
a(href="javascript:;" data-toggle="modal" data-target="#TeamMemberModal" #click="$emit('clicked', item)" )
.team_member
.team_member_image
img(:src="item.avatar")
.team_member_name {{item.full_name}}
.team_member_status {{item.status}}
.team_member_shadow
</template>git status
<script>
export default {
name: "TeamMember",
props: {
item: {},
}
};
</script>
my test code is :
import Vue from 'vue'
import TeamMember from '#/components/TeamMember.vue'
import { mount } from '#vue/test-utils'
const wrapper = mount(TeamMember, {
context: {
props : {
item : {
avatar : 'path/to_image.png',
full_name: "Robocop"
}
}
}
});
I need to validate that the template generated the correct html
- while the Wrapper must contain a Vue instance. I did this :
wrapper.setProps({ avatar: 'path/to_image.png' }),
expect(wrapper.vm.avatar).toBe('path/to_image.png'),
wrapper.setProps({ avatar: 'Robocop' }),
expect(wrapper.vm.full_name).toBe('Robocop')
I run my test, got the following result :
FAIL tests/unit/specs/TeamMember.spec.js
● Test suite failed to run
[vue-test-utils]: mount.context can only be used when mounting a functional component
9 | item : {
10 | avatar : 'path/to_image.png',
> 11 | full_name: "Robocop"
| ^
12 | }
13 | }
14 | }
at throwError (node_modules/#vue/test-utils/dist/vue-test-utils.js:11:9)
at createInstance (node_modules/#vue/test-utils/dist/vue-test-utils.js:2847:5)
at mount (node_modules/#vue/test-utils/dist/vue-test-utils.js:5639:18)
at Object.<anonymous> (tests/unit/specs/TeamMember.spec.js:11:36)
what mistake in my code, & how can I correct it ?
Thanks
The options you are passing should be on the root level of the object, and props data is set with the propsData option:
const wrapper = mount(TeamMember, {
propsData: {
item: {
avatar: 'path/to_image.png',
full_name: 'Robocop'
}
}
})