vue test utils doesn't fire trigger - vue.js

I am trying to simulate a trigger event and test that it has actually fired, but I keep having an empty response.
this is the code:
<v-btn small class="elevation-0"
#click="$emit('close')">
cancel
</v-btn>
it('Should emit close when the CANCEL button is clicked', () => {
const wrapper = shallowMount(TaskForm, {
propsData: {
teamMembers: project.teamMembers,
task: new Task({ project, name: '' })
}
});
wrapper.findComponent(VBtn).trigger('click');
expect(wrapper.emitted()).toHaveProperty('close');
});
Ok I have found the solution: basically trigger cannot be called on a component, therefore I have opted for component.vm.$emit('click') that does the job the right way

Related

How to wait for click button to continue function

I have a function with a popup dialog window with button. I want to wait with executing the rest of the function until the button is clicked. I tried it with Promise and AddEventListener, but cannot find out why is it not working. Could someone help me? (I use Vue3 and Quasar)
I have an error for "const confirm" - Object is possibly 'null'.ts(2531)
Thank you for any advices.
Here is a part of my template:
<q-dialog persistent v-model="phoneDialogBank">
<q-card>
<q-card-section>
<div class="items" v-for="formField in dynamicDialogFieldsVisible" :key="formField?.Key">
<dynamic-form-field :form-field="formField"></dynamic-form-field>
</div>
<div class="row items-center justify-center">
<q-btn color="primary" class="confirm-phone-dialog" v-close-popup>
{{ t('signing-table.phoneDialogBank.ok') }}
</q-btn>
</div>
</q-card-section>
</q-card>
</q-dialog>
Here is my function:
async function dynamicDialogBeforeSubmit() {
const params = getParamsFromAdvisorDeviceBoolean();
if (params && params.hasOwnProperty('dialogBeforeSubmit') && params.dialogBeforeSubmit) {
phoneDialogBank.value = true;
const confirm = document.querySelector('.confirm-phone-dialog');
const waitForButtonClick = new Promise((resolve) => { confirm.addEventListener('click', resolve); });
await waitForButtonClick.then(() => {
dynamicDialogSave();
});
return;
}
dynamicDialogSave();
}
That error is because your button is within a dialog that renders conditionally. So if there is no dialog, that DOM element does not exist. I'm curious why you are not using some of the great features of Vue? Like putting the click handler on the element and using refs to target the DOM instead of using query selectors and event listeners.
<q-btn color="primary" class="confirm-phone-dialog" #click.prevent="dynamicDialogBeforeSubmit" v-close-popup>
{{ t('signing-table.phoneDialogBank.ok') }}
</q-btn>
Your function is a bit mixed up between async and Promises. async / await is just syntactic sugar for a Promise. The way you have it written now, you are wrapping a Promise within another Promise.
async function dynamicDialogBeforeSubmit() {
const params = getParamsFromAdvisorDeviceBoolean();
if (params && params.hasOwnProperty('dialogBeforeSubmit') && params.dialogBeforeSubmit) {
phoneDialogBank.value = true;
await dynamicDialogSave();
} else {
console.log('There was a problem')
}
}
I should note that you will likely need to pass an object to dynamicDialogSave(someObject) for it to actually save something. I assume this, not knowing what that function actually looks like.
Event listeners are not asynchronous and you wouldn't want to wrap them in a promise anyways. If you wish to write it as you have, it would be best to declare the click listener as a separate function that fires onMounted, then call it from any async function. You should also detach native event listeners onUnmounted to avoid memory leaks with Vue.

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

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

Vuex State not Reactive without Page reload

The problem experiences entails the following setup:
Simple Nuxt Intallation v2^
Using Vuetify and Axios
I have already built an advanced app with a lot of extras using a real back-end, but decided to replicate the error on a simple new installation of Nuxt just to be able to create a repo for you guys. So the functionality of this demo might not be clear to you - but in real life the app is using tasks and projects. The user must be able to change the status of each task.
I cannot figure out how to go further with this app if I can't implement this feature. My first thoughts are that it might be a bug with Nuxt / Vue - but I'm hoping someone can tell me I'm wrong!
Seems simple?! Well, 7 Days later and I'm no closer to solving this little bug I am experiencing...
Well for your ease-of-access I have loaded a simple github demo showing just what the problem is with only 3 steps to reproduce the error.
https://github.com/BarryJamez/not-so-reactive-app.git
Steps: Once you have run 'npm run dev' - as follows:
Step 1: Start on the index page (http://localhost:3000)
Step 2: Click on 'View Tasks'
Step 3: Try to change the status of a task by pressing the buttons
Step 4: Now refresh the page and all of a sudden it works.
To repeat this problem, just navigate back to the home page, refresh (reload) the page and start again from step 1
I have tried a lot of things, but none of them worked:
**1. I have moved my entire child component housing the list of comments into the parent component to eliminate the passing of props being the problem.
I have used Vue.set and array.splice instead of just replacing the array item with an index
I have enabled watchers on these computed arrays and set 'deep' to true
I have played around with async and await by removing them and adding them on certain places.
I have used the $nextTick function to see if it was a render issue
I have placed the array in its own parent level state property (although the postId field in a child element in the array) - yet how else can I contain an object in a state - an object must have 'child' elements.
I have referenced the parent component computed properties instead of the props in the child component.**
My Page with the tasks:
<template>
<v-container fluid>
<v-row v-for="task in myTasks" :key="task.id">
<v-col>
<v-card class="ma-4">
<v-card-title>{{task.title}}</v-card-title>
<v-card-text>
<p>{{task.description}}</p>
<p class="blue--text">{{task.status}}</p>
</v-card-text>
<v-card-actions>
<v-btn
v-if="task.status == 'Active'"
#click="onChangeStatus(task.id, 'Completed')"
>
Complete Task
</v-btn>
<v-btn
v-if="task.status == 'Completed'"
#click="onChangeStatus(task.id, 'Active')"
>
Incomplete Task
</v-btn>
<v-btn
v-if="task.status == 'Active'"
#click="onChangeStatus(task.id, 'Postponed')"
>
Postpone Task
</v-btn>
<v-btn
v-if="task.status == 'Postponed'"
#click="onChangeStatus(task.id, 'Active')"
>
Activate Task
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script>
import { mapState } from 'vuex'
export default {
async fetch({store, route}) {
await store.dispatch('tasks/getMyTasks', {
sid: route.params.sid
})
},
methods: {
onChangeStatus(taskId, status) {
this.$store.commit('tasks/changeTaskStatus', {
taskId: taskId,
status: status,
})
}
},
computed: {
...mapState({
myTasks: state => state.tasks.myTasks
})
}
}
My store namespaced 'tasks':
export const store = () => ({
myTasks: []
})
export const mutations = {
setMyTasks: (state, tasks) => {
state.myTasks = tasks
},
changeTaskStatus: (state, params) => {
let index = state.myTasks.findIndex(el => {
return el.id == params.taskId
})
state.myTasks[index].status = params.status
}
}
export const actions = {
async getMyTasks (vuexContext, params) {
try {
let {data} = await this.$axios.$get('/tasks.json')
vuexContext.commit('setMyTasks', data)
} catch (e) {
console.log(e)
}
}
}
I don't really want to have to reload the page every time a user lands on it as this defeats the purpose of SSR and SPAs.
IF YOU CAN PICK YOUR BRAIN please checkout the repo on Github and follow the steps above:
https://github.com/BarryJamez/not-so-reactive-app.git
You are not defining state in your task store.
In store/tasks.js
export const store = () => ({
myTasks: []
})
while it should be
export const state = () => ({
myTasks: []
})

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