How do you open a bootstrap-vue modal with vue-test-utils? - vue.js

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>

Related

Cypress Vue Component Test Runner - testing a button click has emitted an event

I'm having trouble working out how to do a simple test of a Vue component (using the Cypress Component Test Runner) to see if a button click results in an event being emitted
// MyButton.vue component
<template>
<Button
data-testid="button"
label="Click Button"
#click="clickButton()"
/>
</template>
<script setup lang="ts">
import { defineEmits } from "vue";
const emit = defineEmits(["clickButtonEvent"]);
function clickButton() {
emit("clickButtonEvent");
}
</script>
// MyButton.spec.ts
it.only("should emit an even when clicked", () => {
const clickButtonSpy = cy.spy().as("clickButtonEvent");
mount(FulfilButton, {
global: {
components: {
Button,
},
},
})
.get('[data-testid="button"]')
.click();
cy.get("#clickButtonEvent").should("have.been.called");
});
This doesn't work - in the console I see
mount
get
-click
but then this:
expect clickButtonEvent to have been called at least once, but it was never called
So I guess I am not hooking up this cy.spy correctly - presumably because I am not doing it as part of the mount? What do I need to do? The button itself is a PrimeVue button component but I'm pretty sure that should not stop me doing this test?
Well, you are not hooking the spy at all.
The Cypress mount command has the same interface as vue-test-utils mount (it is using vue-test-utils under the hood)
In vue-test-utils v1 (for Vue 2) you can use listeners mount option to attach the spy as demonstrated in this answer
But since you are using Vue 3 and in turn vue-test-utils v2 where listeners mount option was removed, probably your best option to use recommended API of the wrapper - emitted
This example is taken from the recent talk of Jessica Sachs (Cypress team member) (repo) where she does something like this:
mount(FulfilButton, {
global: {
components: {
Button,
},
},
})
.get('[data-testid="button"]')
.click()
.vue()
.then((wrapper) => {
expect(wrapper.emitted('clickButtonEvent')).to.have.length(1)
})
Note that vue() is not a build-in Cypress command. In this demo/repo it was added by Jessica in the support file
// ./cypress/support/index.js
Cypress.Commands.add('vue', () => {
return cy.wrap(Cypress.vueWrapper)
})
You can do something very similar without introducing any helper (example)
it('emits "increment" event on click', () => {
cy.get('button')
.click()
.click()
.then(() => {
cy.wrap(Cypress.vueWrapper.emitted()).should('have.property', 'increment')
})
})

Vue Testing (JEST): button.trigger('click') not working

Been reading up a lot of stackoverflow and github discussions, about vue jest having trouble with button.trigger('click'). I've been struggling over this issue for a few hours today, have to say I'm quite frustrated, and surprised how a simple function such as trigger('click') can cause so much problems.
In short, my code has a b-button, which #click fires off fetchData function, from vuex. This works perfectly well in browser, but in testing mode, the fetchData does not get executed.
Vue Component Code
<template>
<b-button id="loadButton" variant="outline-primary" #click="fetchData">Load Data</b-button>
</template>
<script>
import { mapActions } from 'vuex';
export default {
name: "LoadAndSave",
methods: { ...mapActions(['fetchData']) }
}
</script>
Testing Code
import { shallowMount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import { BootstrapVue } from 'bootstrap-vue'
import LoadAndSave from '../../resources/components/LoadAndSave'
const localVue = createLocalVue()
localVue.use(BootstrapVue)
localVue.use(Vuex)
describe('LoadAndSave.vue', () => {
let actions
let getters
let store
beforeEach(() => {
actions = {
fetchData: jest.fn()
}
store = new Vuex.Store({
actions
})
})
it('LoadAndSave: onClick executes fetchData', async () => {
const wrapper = shallowMount(LoadAndSave, { localVue, store })
const button = wrapper.find("#loadButton")
await button.trigger('click')
expect(actions.fetchData).toHaveBeenCalled()
})
})
Result of testing
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
This is not my first day coding, I am no where near an expert in coding either, but just the idea of not being able to get a button click to trigger really sends chills down my spine, not to mention the frustration accompanied.
If anyone could give any suggestion that'd be appreciated, thank you.
Codey
I have been struggling with this as well, it seems sometimes the test component has a hard time finding the emitting/calls to functions that do not have parentheses.
it('Calls save() when pressing save button', async () => {
const savebutton = wrapper.find('#saveButton')
const spy = jest.spyOn(wrapper.vm, 'save')
savebutton.trigger('click')
await wrapper.vm.$nextTick()
expect(spy).toHaveBeenCalled()
jest.restoreAllMocks()
})
the above test will fail here:
<button class="btn btn-success" id="saveButton" #click="save">Save</button>
but not here:
<button class="btn btn-success" id="saveButton" #click="save()">Spara</button>
You can at-least check if this is your problem, make a reference to the store function in methods and call the function with parentheses on the element.
Another way of asserting the button-click has been fired is by looking at the emitted object.
it('Calls save() when pressing save button', () => {
const savebutton = wrapper.find('#saveButton')
savebutton.trigger('click')
expect(wrapper.emitted('save')).toHaveLength(1)
})

Lazy loading a specific component in Vue.js

I just make it quick:
In normal loading of a component (for example "Picker" component from emoji-mart-vue package) this syntax should be used:
import {Picker} from "./emoji-mart-vue";
Vue.component("picker", Picker);
And it works just fine.
But when I try to lazy load this component I'm not sure exactly what code to write. Note that the following syntax which is written in the documentation doesn't work in this case as expected:
let Picker = ()=>import("./emoji-mart-vue");
The problem, I'm assuming, is that you're using
let Picker = ()=>import("./emoji-mart-vue");
Vue.component("picker", Picker);
to be clear, you're defining the component directly before the promise is resolved, so the component is assigned a promise, rather than a resolved component.
The solution is not clear and depends on "what are you trying to accomplish"
One possible solution:
import("./emoji-mart-vue")
.then(Picker=> {
Vue.component("picker", Picker);
// other vue stuff
});
This will (block) wait until the component is loaded before loading rest of the application. IMHO, this defeats the purpose of code-spliting, since the application overall load time is likely worse.
Another option
is to load it on the component that needs it.
so you could put this into the .vue sfc that uses it:
export default {
components: {
Picker: () => import("./emoji-mart-vue")
}
};
But this would make it so that all components that use it need to have this added, however, this may have benefits in code-splitting, since it will load only when needed the 1st time, so if user lands on a route that doesn't require it, the load time will be faster.
A witty way to solve it
can be done by using a placeholder component while the other one loads
const Picker= () => ({
component: import("./emoji-mart-vue"),
loading: SomeLoadingComponent
});
Vue.component("picker", Picker);
or if you don't want to load another component (SomeLoadingComponent), you can pass a template like this
const Picker= () => ({
component: import("./emoji-mart-vue"),
loading: {template:`<h1>LOADING</h1>`},
});
Vue.component("picker", Picker);
In PluginPicker.vue you do this:
<template>
<picker />
</template>
<script>
import { Picker } from "./emoji-mart-vue";
export default {
components: { Picker }
}
</script>
And in comp where you like to lazy load do this:
The component will not be loaded until it is required in the DOM, which is as soon as the v-if value changes to true.
<template>
<div>
<plugin-picker v-if="compLoaded" />
</div>
</template>
<script>
const PluginPicker = () => import('./PluginPicker.vue')
export default {
data() = { return { compLoaded: false }}
components: { PluginPicker }
}
// Another syntax
export default {
components: {
PluginPicker: () => import('./PluginPicker.vue')
}
}
</script>

Let a (mocked) transition finish asap inside v-menu in a vuetify unit test

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

Testing mouseover event in vue-test-utils

I am trying to make unit test for a #mouseover and #mouseleave event who's displaying a v-card-actions depending from the mouseover. I am using vue-test-utils in my vuejs2 webpack application. Here is what i found on the web : vue-utlis mouse click exemple . Thanks in advance to anyone who's helping
Here is my code actual html template code:
<v-card class="menuCard pa-1 elevation-2 f-basis-0 my-2"
#mouseover="isBlockAct = true" #mouseleave="isBlockAct = (!checked?
false:true)">
<v-checkbox v-model="checked" id="checkbox" class="diCheckbox ma-0" hide-
details></v-checkbox>
<v-card-actions class="myUpHere pa-0">
<v-layout row :class="{'isAct': isBlockAct, 'isNotAct': !isBlockAct}">
</v-card>
Here is what i tried in my spec.js code:
describe("Over event", () => {
it("shows the icons if the card is over or not", () => {
const wrapper = mount(MenuRepasRecetteCard, {
propsData: {}
});
wrapper.find(".menuCard").trigger("mouseover");
expect(wrapper.find(".isAct").text()).contain("remove_red_eye");
});
u need to wait for the event to be processed by vue.
describe("Over event", (done) => {
it("shows the icons if the card is over or not", () => {
const wrapper = mount(MenuRepasRecetteCard, {
propsData: {}
});
wrapper.find(".menuCard").trigger("mouseover");
wrapper.vm.$nextTick( () => {
expect(wrapper.find(".isAct").text()).contain("remove_red_eye");
done();
});
});
});
The await keyword can be used to wait for the trigger to complete:
await wrapper.find(".menuCard").trigger("mouseover");
async would also need adding to the closure:
it("shows the icons if the card is over or not", async () => {
Unfortunatly I can't comment on Lars answer.
The docs say nextTick isn't needed.:
Vue Test Utils triggers event synchronously. Consequently, Vue.nextTick is not required.
-Source: https://vue-test-utils.vuejs.org/guides/dom-events.html#important

Categories