Vue Testing Library won't rerender DOM when state changes - vue.js

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")

Related

Vue js component template not updating with data

I have a weird issue in some of my Vue js components, let me explain. I only render my component template after data has been initialised like so:
<template>
<div>
<div v-if='!isLoading'>
<!-- content -->
</div>
<div v-else>...</div>
</div>
</template>
In the created method of this component, I get some data from the store and set isLoading to false like so.
data() {
return {
variable: null,
isLoading: true,
}
},
created() {
this.variable = this.$store.getters['someModule/someGetter']
this.isLoading = false
}
Here's where the weird behaviour happens. Even though I updated the isLoading variable to false in the created method, the component template is not updating.
When I log the isLoading variable to the console at the end of the created method, it logs false, like i set it. But when I check the isLoading variable in the Vue js tools, it's still set to true...
Lets say this components is rendered in '/content'. This weird behaviour happens when I change routes from '/' to '/content'. When I refresh the app on the '/content' route, this doesn't happen. When I go from '/' to '/other-content' and then to '/content' it also doesn't happen.
Any ideas on why this happens would be greatly appreciated.
Thanks is advance and have a nice day!
There are subtle differences between mounted and created in your case since you want to manipulate the DOM, you should use mounted lifecycle hook.
This answer would expound on the differences between the two lifecycle methods.
This is a working example of what you're trying to do: https://codesandbox.io/s/blissful-field-kjufc?file=/src/App.vue
The interesting part of the code is here:
async created() {
const response = await fetch("https://jsonplaceholder.typicode.com/photos");
const json = await response.json();
console.log("done loading the data");
if (json) this.isLoading = false;
},
You can go to your network tab and select "slow 3G" to have an emulated slow connection. That way, you will see that the VueJS logo is not displayed until we have fetched all the 5000 photos.
If it's not helping, we definitely need more details, like vue devtools debugging or a reproduction.

Trigger form submit on button click in Vue Unit Test

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()

How to watch on Route changes with Nuxt and asyncData

Hi everybody i'm trying to watch on route changes in my nuxt js app.
Here my middleware:
export default function ({ route }) {
return route; but i don't know what to write here
}
index.vue File
middleware: [routeReact]
i'm trying to write this:
app.context.route = route
but it says to me that app.context doesn't exist
Here's the point of my question i'm trying to update my data that gets from my api with axios on page if route changing
like this
this the page
i'm clicking link to next page :
but when i'm route to next page, nothing happens all data is the same:
here my asyncData code:
asyncData({ app }) {
return app.$axios.$get('apps/' + app.context.route.fullPath.replace(/\/categories\/?/, ''))
.then(res => {
return {
info: res.results,
nextPage: res.next,
prevPage: res.prev
};
})
}
Thanks for your help
First thing, context.route or it's alias this.$route is immutable object and should not be assigned a value.
Instead, we should use this.$router and it's methods for programmatic navigation or <nuxt-link> and <router-link>.
As I understand, you need to render the same route, but trigger asyncData hook in order to update component's data. Only route query is changed.
Correct way to navigate to the same page but with different data is to use link of such format:
<nuxt-link :to="{ name: 'index', query: { start: 420 }}"
Then you can use nuxt provided option watchQuery on page component and access that query inside asyncData as follows:
watchQuery: true,
asyncData ({ query, app }) {
const { start } = query
const queryString = start ? `?start=${start}` : ''
return app.$axios.$get(`apps/${queryString}`)
.then(res => {
return {
info: res.results,
nextPage: res.next,
prevPage: res.prev
}
})
},
This option does not require usage of middleware. If you want to stick to using middleware functions, you can add a key to layout or page view that is used. Here is an example of adding a key to default layout:
<nuxt :key="$route.fullPath" />
This will force nuxt to re-render the page, thus calling middlewares and hooks. It is also useful for triggering transitions when switching dynamic routes of the same page component.

How do I stop my vue component from rendering until a call completes

I've googled this but I can't find any specific solution. Basically I have a vue component that depends on an init call and I want it to stop rendering until the call completes, at which point I want the component to render. Seems simple but unless I'm missing something I can't find any lifecycle method that does that.
You can use v-if for that purpose
<template>
<div v-if="loaded"></div>
</template>
<script>
export default {
name: 'TestComponent',
data: function () {
return {
loaded: false
}
},
created() {
callExternalServices().then(() => {
this.loaded = true
})
}
}
</script>
It will render an empty component until loaded == true
Basically you make an init call in the created or mounted lifecycle method and you initialize a data object with the response of the call. If you don't change data during the call there is no reason for vue to render anything.
Maybe you can provide a jsfiddle that show exactly your problem.

Vue.js: how to use the afterEnter hook with an async component

I would like to use JS Hook as described here. Specially, I want to use the afterEnter hook with an async component.
This is my async component:
Vue.component('example', function(resolve, reject){
let data = {
text: 'test data',
};
$.post('http://example.com', data, function(r){
r = JSON.parse(r);
if( r.success ) {
resolve({
template: r.data,
afterEnter: function(el, done){
console.log('test');
}
});
}
});
});
This is what the ajax call gets from the server, and it's what is passed to the template in r.data.
<transition v-on:after-enter="afterEnter"></transition>
These are the two errors that I get.
[Vue warn]: Property or method "afterEnter" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
[Vue warn]: Invalid handler for event "after-enter": got undefined
Is it possible to use JS hooks with async components? And if not, how should I approach this? My objective is to run custom JS after Vue (and/or vue-router) inject the component template onto the page, so that I can initiliaze image sliders and whatnot. It is important that my custom JS fires every time the component is navigated to, and not only on the first load.
Thank you.
That warning means that Vue is looking for (but unable to find) a property or method named "afterEnter", which you reference in your template. You have defined afterEnter in your resolve function as if it is a lifecycle hook, but it needs to be one of your Vue instance's methods.
So, your resolve function should look like this:
resolve({
template: r.data,
methods: {
afterEnter: function(el, done) {
console.log('test');
}
}
});