Is there a way to actually trigger the submission of a form by clicking on a submit button in a Vue Unit Test?
Let's take this simple component:
<template>
<form #submit.prevent="$emit('submitEventTriggered')">
<button type="submit">Submit Form</button>
</form>
</template>
<script>
export default {}
</script>
You can find a similar component as an example here.
I want to test that submit.prevent gets triggered when the button is clicked and therefore the submitEventTriggered is emitted. When I run this in a browser everything works as expected, but the following test fails:
import {shallowMount} from '#vue/test-utils'
import {assert} from 'chai'
import Form from '#/components/Form.vue'
describe.only('Form', () => {
it('button click triggers submit event', () => {
const wrapper = shallowMount(Form)
wrapper.find('[type=\'submit\']').trigger('click')
assert.exists(wrapper.emitted('submitEventTriggered'), 'Form submit not triggered')
})
})
With this output:
AssertionError: Form submit not triggered: expected undefined to exist
If I change the action to trigger submit.prevent on the form directly everything works fine, but then there is actually no test coverage for the submitting via button.
wrapper.find('form').trigger('submit.prevent')
It seems like the trigger function doesn't actually click the button.
Why is this and is there a way to fix it?
Note: The previous method used attachToDocument, which has been deprecated,
The issue is that Vue Test Utils does not attach DOM nodes to the document by default. This is to avoid enforcing cleanup. You can solve this by setting attachTo to an HTML element when you mount the component:
const div = document.createElement('div')
div.id = 'root'
document.body.appendChild(div)
it('button click triggers submit event', () => {
const wrapper = shallowMount(Form, {
attachTo: '#root'
})
wrapper.find("[type='submit']").trigger('click')
assert.exists(
wrapper.emitted('submitEventTriggered'),
'Form submit not triggered'
)
})
You should remove the DOM node from the document to avoid a memory leak. You can do this by calling destroy on the wrapper:
wrapper.destroy()
Related
In my Vue3 app, I'm using the mitt eventbus library to emit and receive events between components.
I put this in onMounted of a list component that needs to refresh:
mitt.on("list_refresh", (evt) => {
refresh();
});
In another component that contains the list-component as a child (or grandchild), I do this in a method:
mitt.emit("list_refresh", {});
This works ok, but while developing with hot-reload on, the events seem to be emitted multiple times, as if they're created extra each time the app reloads, instead of overwriting the old ones.
When I reload the entire page in the browser, it works fine again.
Any idea to prevent this?
It looks like your component is missing a corresponding off() call to remove the event listener. During hot reload, the current component instances unmount, and new ones mount; so if you're not removing current event listeners, you'll just pile on new event listeners. To resolve the issue, use the onUnmounted hook to remove the event listener when the component is removed from the DOM.
Also, make sure to pass cached function references (instead of inline functions) to mitt.on() and mitt.off() to ensure the given event listener lookup succeeds in mitt.off():
// mitt.on('list_refresh', () => refresh()) ❌
mitt.on('list_refresh', refresh) ✅
mitt.off('list_refresh', refresh)
Your setup() should look similar to this:
import { onMounted, onUnmounted } from 'vue'
export default {
setup() {
const refresh = () => { /*...*/ }
onMounted(() => mitt.on('list_refresh', refresh))
onUnmounted(() => mitt.off('list_refresh', refresh)) 👈
}
}
I have a component with a strait forward Edit button. The Edit button calls a method that sets isEditing to true.
There are a few input elements with v-if="isEditing", so I'm testing that those input elements are visible after the Edit button is clicked.
When my test runs fireEvent.click(screen.getByRole('link', {name: 'Edit'})), it is updating isEditing to true (based on my console.log messages before/after the .click event), but it doesn't seem to re-render the components within the test (based on the DOM rendered in my terminal after getByRole fails).
It works as expected in the browser, but doesn't seem to update the DOM for the spec. I'm using Vue2, Vue Testing Library, and Jest.
Implementation:
<template>
<a #click.prevent="startEdit" v-if="!isEditing">Edit</a>
<input :v-if="isEditing" />
</template>
...
methods: {
startEdit: () => {
this.isEditing = true
}
}
Spec:
describe('FormComponent', () => {
beforeEach(() => {
render(FormComponent)
})
it('displays input tags' () => {
fireEvent.click(screen.getByRole('link', {name: 'Edit'}))
expect(screen.getByRole('input')).toBeInTheDocument()
})
})
The problem is your expect is running before the DOM has had a chance to update. From the testing library documentation:
Because Vue applies DOM updates asynchronously during re-renders, the fireEvent tools are re-exported as async functions. To ensure that the DOM is properly updated in response to an event in a test, it's recommended to always await fireEvent.
You should update your test to await the fireEvent promise like so:
it('displays input tags' async () => {
await fireEvent.click(screen.getByRole('link', {name: 'Edit'}))
expect(screen.getByRole('input')).toBeInTheDocument()
})
You also have a typo in your second v-if as Nicole pointed out in her answer.
It doesn't work because you wrote :v-if when it should be v-if. I guess this was simply a typo since you did it correctly the first time (v-if="!isEditing")
I pretty new to vuejs and am building a vuejs project. One of the tasks I am stuck at is, that I want to call a function written in the javascript part of vuejs from the html part of vuejs without creating any buttons or textboxes. I want to call this function as soon as my app starts. How do I achieve this? When I use mounted (vue lifecycle hook), the page it redirects to keeps refreshing. would appreciate some leads on this.
For example, I have a code:
<template>
<h1>Hello World!</h1>
<!--I WANT TO CALL THE auth0login function here. How do I do that without creating a button/text field,etc -->
</template>
<script>
export default {
data: () => ({
return : {
clientID: process.env.example
}
}),
methods:{
auth0login(){
console.log('logging in via Auth0!');
Store.dispatch('login');
}
}
}
</script>
I want to call the auth0login function defined in the script part in the html part above in order to execute the functionalities of the auth0login function. I want to do this without creating any buttons in the html file and simply just execute the auth0login function when the app launches.
How do I do this?
You need to call auth0login() only when you're not already logged I guess.
I think the reason is after user already logged in, they go back to the initial page, and the function in mounted() hook run again. Thats why they keep get redirect to login page. This not happen when you insert the button, because the button require user to click at it.
To fix this, you need to define a vuex state to store whether user has logged in or not, and in mounted() hook, just called it when they not login yet.
So. Instead of
mounted: function() { console.log('logging in via Auth0!'); this.auth0login() } }
Add a check login state
mounted() {
if(!this.$store.state.userLoggin) // here i define userLoggin and vuex state, you should update it to true whenever user successfully login
{ console.log('logging in via Auth0!'); this.auth0login() //only call it when user not loggin
}
}
And remember to update your vuex state.userLoggin when user successfully login in the auth0login() function
I have a some middleware in my Nuxt app that closes a fullscreen mobile menu when a new route is clicked. What I am experiencing is the following:
user clicks nuxt-link
menu closes
asyncData delay (still shows current page)
new page is loaded upon resolved asyncData
What I would like is the following:
user clicks nuxt-link
asyncData delay (if exists)
menu closes upon new page load
Is it possible to have an asyncData watcher in Nuxt middleware?
I know I can hack this by creating a store value that tracks asyncData load, but I'd rather not have to wire up such a messy solution to each and every page that uses asyncData.
Note - not every page uses asyncData.
I think that the best option would be to leave the menu open, and then when the new page finishes loading (probably on the mounted hook) send an event or action to close the menu.
I figured this out in a more elegant solution than having to implement a store.dispatch function on each individual asyncData function.
So what I did was use a custom loading component: https://nuxtjs.org/api/configuration-loading/#using-a-custom-loading-component
However, instead of showing a progress bar or any sort of loading animation, I just used this component to set a loading state in my vuex store. Note - it forces you to have a template, but I just permanently disabled it.
<template>
<div v-if="false"></div>
</template>
<script>
export default {
methods: {
start() {
this.$store.dispatch('updateLoadingStatus', true);
},
finish() {
this.$store.dispatch('updateLoadingStatus', false);
}
}
};
</script>
Then in my middleware, I set up an interval watcher to check when loading has stopped. When stopped, I stop the interval and close the mobile menu.
export default function({ store }) {
if (store.state.page.mobileNav) {
if (store.state.page.loading) {
var watcher = setInterval(() => {
if (!store.state.page.loading) {
store.dispatch('updateMobileNav', false);
clearInterval(watcher);
}
}, 100);
} else {
store.dispatch('updateMobileNav', false);
}
}
}
This doesn't specifically have to be for a mobile menu open/close. It can be used for anything in middleware that needs to wait for asyncData to resolve. Hope this can help anyone in the future!
I have created a global vuejs event named EvenBus in a file called app.js as follows:
window.EventBus = new Vue();
After deleting a person from a database, I redirect to the home page and I emit an event called deleted like so:
import {app} from '../app';
export default {
methods: {
deleteMe: function () {
axios.delete('/api/persons/' + this.person.id)
.then(response => {
window.location.href = '/home';
EventBus.$emit('deleted', {
notification_msg: 'This person has been successfully deleted.',
});
})
},
}
on the page home, I have inserted a tag <notify></notify> linked to a component called notify.vue.
In this component, I added the following script:
import {app} from '../app';
export default {
/* ... */
mounted() {
let that = this;
console.log(EventBus);
EventBus.$on('deleted', function(data){
alert('person deleted!!' + data.notification_msg);
that.msg_text = data.notification_msg;
});
},
}
When the delete happens, I get successfully redirected to the home page, but nothing happens there. The alert('person deleted!!...') never shows up.
The code is executed with no error.
Am I missing something to make my component listen to the emitted event?
EDIT
The line console.log(EvenBus); written in the notify.vue file shows that there is a event called 'deleted' (c.f. printscreen below)
The issue here is that when this line of code is executed,
window.location.href = '/home';
the page you are on is replaced with the /home page. So, it's possible it never even gets to the line that emits the event and if it does, the object that listens to the event is gone when the page is loaded.
You might want to look into using VueRouter so that the page isn't destroyed. Other than that, possibly tell the server when you are redirecting that it needs to show the notification when the page is loaded.