Vue test-utils how to test a router.push() - vue.js

In my component , I have a method which will execute a router.push()
import router from "#/router";
// ...
export default {
// ...
methods: {
closeAlert: function() {
if (this.msgTypeContactForm == "success") {
router.push("/home");
} else {
return;
}
},
// ....
}
}
I want to test it...
I wrote the following specs..
it("should ... go to home page", async () => {
// given
const $route = {
name: "home"
},
options = {
...
mocks: {
$route
}
};
wrapper = mount(ContactForm, options);
const closeBtn = wrapper.find(".v-alert__dismissible");
closeBtn.trigger("click");
await wrapper.vm.$nextTick();
expect(alert.attributes().style).toBe("display: none;")
// router path '/home' to be called ?
});
1 - I get an error
console.error node_modules/#vue/test-utils/dist/vue-test-utils.js:15
[vue-test-utils]: could not overwrite property $route, this is usually caused by a plugin that has added the property asa read-only value
2 - How I should write the expect() to be sure that this /home route has been called
thanks for feedback

You are doing something that happens to work, but I believe is wrong, and also is causing you problems to test the router. You're importing the router in your component:
import router from "#/router";
Then calling its push right away:
router.push("/home");
I don't know how exactly you're installing the router, but usually you do something like:
new Vue({
router,
store,
i18n,
}).$mount('#app');
To install Vue plugins. I bet you're already doing this (in fact, is this mechanism that expose $route to your component). In the example, a vuex store and a reference to vue-i18n are also being installed.
This will expose a $router member in all your components. Instead of importing the router and calling its push directly, you could call it from this as $router:
this.$router.push("/home");
Now, thise makes testing easier, because you can pass a fake router to your component, when testing, via the mocks property, just as you're doing with $route already:
const push = jest.fn();
const $router = {
push: jest.fn(),
}
...
mocks: {
$route,
$router,
}
And then, in your test, you assert against push having been called:
expect(push).toHaveBeenCalledWith('/the-desired-path');

Assuming that you have setup the pre-requisities correctly and similar to this
Just use
it("should ... go to home page", async () => {
const $route = {
name: "home"
}
...
// router path '/home' to be called ?
expect(wrapper.vm.$route.name).toBe($route.name)
});

Related

How can I use router in store for Quasar

I'm trying to redirect to dashboard after login.
I found this in app.js file that
// make router instance available in store store.$router = router
So I codede that in my login method of auth.js file like below.
store.$router.push({ name: 'dashboard' })
But there is nothing to happen.
How can I use router in vuex store file?
Router is not available in the store directly. You might have to use plugin which makes the router available to the store.
Here is the Vuex-Router plugin which helps to access router to the store.
Are you trying to call router.push in an vuex action?
If so i am doing the same in my quasar v2 project
store/auth/actions.js
import { api } from 'boot/axios'
export function login({ dispatch, commit }, data) {
return api.post('/user/login', data).then(res => {
commit('setToken', res.data.token)
this.$router.push({ name: 'dashboard' }) // <-- What you are looking for?
}).catch(err => {
let msg = err.response.data || 'Error occurred'
return Promise.reject(msg)
})
}

Cannot use Vue-Router to get the parameters in the URL

Today, when trying to use Vue-Router (in Vue-CLI) to get URL parameters, I encountered difficulties ($route.query is empty), the code is as follows.
Code purpose: Get the parameters carried after the URL (such as client_id in "http://localhost:8080/#/?client_id=00000000000077")
Project file structure:
router/index.js:
App.vue(Get part of the code for URL parameters):
The running result of this part of the code:
I'm not sure why $router.currentRoute and $route aren't matching up, but you could simply use $router.currentRoute.query.client_id if you need it in mounted().
Another workaround is to use a $watch on $route.query.client_id:
export default {
mounted() {
const unwatch = this.$watch('$route.query.client_id', clientId => {
console.log({ clientId })
// no need to continue watching
unwatch()
})
}
}
Or watch in the Composition API:
import { watch } from 'vue'
import { useRoute } from 'vue-router'
export default {
mounted() {
console.log({
route: this.$route,
router: this.$router,
})
},
setup() {
const route = useRoute()
const unwatch = watch(() => route.query.client_id, clientId => {
console.log({ clientId })
// no need to continue watching
unwatch()
})
}
}

Access Nuxt Router in plugin

I've created a Nuxt plugin that is loaded in the config file with some global functions. In one function, I'd like to access the router, and push to a new route. I am getting an error that router is undefined. Can someone help me understand how I access the router here, if its not attached to the context.
export default (context, inject) => {
const someFunction = () => {
context.router.push({ name: 'route-name' } })
}
}
Try to destruct the context then use app to access the router :
export default ({app}, inject) => {
const someFunction = () => {
app.router.push({ name: 'route-name' } })
}
}
I figured this out. When using context, you can access the router, like this:
context.app.router

Accessing app inside beforeRouteEnter

I'd like to show some loading animation in the app root while a component prepares to be rendered by vue router.
Already found this question, proposing the use of navigation guards, and another question, where the accepted answer shows how to use the beforeEach guard to set a variable in app, showing a loading animation.
The problem is that this doesn't work when deep-linking to some route (initial url includes a route path, such as 'someurl#/foo'). The beforeEach guard simply doesn't get called then.
So i switched to the loaded component's beforeRouteEnter guard, which would also allow me to show the loading animation for some components only:
app:
var app = new Vue({
el: '#app',
data: { loading: false }
router: router
});
component:
var Foo = {
template: '<div>bar</div>',
beforeRouteEnter: function(to, from, next) {
app.loading = true; // 'app' unavailable when deep-linking
// do some loading here before calling next()...
next();
}
}
But then i found that when deep-linking to the component, app isn't available in beforeRouteEnter, as it gets called very early in the initialisation process.
I don't want to set loading to true inside the app data declaration, as i might decide at some point to deep-link to another route, whose component doesn't need a loading animation.
I believe, your solution is correct. However, I would suggest using next() function instead. As written in vue-router docs.
https://router.vuejs.org/en/advanced/navigation-guards.html
The beforeRouteEnter guard does NOT have access to this, because the guard is called before the navigation is confirmed, thus the new entering component has not even been created yet.
However, you can access the instance by passing a callback to next. The callback will be called when the navigation is confirmed, and the component instance will be passed to the callback as the argument:
beforeRouteEnter (to, from, next) {
next(vm => {
vm.$root.loading = true;
})
}
Found a workaround using Vue.nextTick:
beforeRouteEnter: function(to, from, next) {
Vue.nextTick(function(){
// now app is available
app.loading = true;
// some loading to happen here...
seTimeout(function(){
app.loading = false;
next();
}, 1000);
})
}
Feels a little hacky, so would be thankful for other suggestions.
Find a demo of this solution here:
https://s.codepen.io/schellmax/debug/aYvXqx/GnrnbVPBXezr#/foo
What about using beforeRouteLeave to trigger the loading then have the component toggle it off in mounted.
For the initial load of the app you could have
app:
var app = new Vue({
el: '#app',
data() => ({ loading: true }),
mounted() { this.loading: false },
router: router
});
then for your components
component:
var Foo = {
template: '<div>bar</div>',
mounted() {
app.loading = false;
},
beforeRouteLeave(to, from , next) {
switch(to){
case COMPONENT_TO_SHOW_LOADING_ON:
case OTHER_COMPONENT:
app.loading = true;
default:
}
}
}

How to use Vue Router from Vuex state?

In my components I've been using:
this.$router.push({ name: 'home', params: { id: this.searchText }});
To change route. I've now moved a method into my Vuex actions, and of course this.$router no longer works. Nor does Vue.router. So, how do I call router methods from the Vuex state, please?
I'm assuming vuex-router-sync won't help here as you need the router instance.
Therefore although this doesn't feel ideal you could set the instance as a global within webpack, i.e.
global.router = new VueRouter({
routes
})
const app = new Vue({
router
...
now you should be able to: router.push({ name: 'home', params: { id: 1234 }}) from anywhere within your app
As an alternative if you don't like the idea of the above you could return a Promise from your action. Then if the action completes successfully I assume it calls a mutation or something and you can resolve the Promise. However if it fails and whatever condition the redirect needs is hit you reject the Promise.
This way you can move the routers redirect into a component that simply catches the rejected Promise and fires the vue-router push, i.e.
# vuex
actions: {
foo: ({ commit }, payload) =>
new Promise((resolve, reject) => {
if (payload.title) {
commit('updateTitle', payload.title)
resolve()
} else {
reject()
}
})
# component
methods: {
updateFoo () {
this.$store.dispatch('foo', {})
.then(response => { // success })
.catch(response => {
// fail
this.$router.push({ name: 'home', params: { id: 1234 }})
})
I a situation, I find myself to use .go instead of .push.
Sorry, no explanation about why, but in my case it worked. I leave this for future Googlers like me.
I believe rootState.router will be available in your actions, assuming you passed router as an option in your main Vue constructor.
As GuyC mentioned, I was also thinking you may be better off returning a promise from your action and routing after it resolves. In simple terms: dispatch(YOUR_ACTION).then(router.push()).
state: {
anyObj: {}, // Just filler
_router: null // place holder for router ref
},
mutations: {
/***
* All stores that have this mutation will run it
*
* You can call this in App mount, eg...
* mounted () {
* let vm = this
* vm.$store.commit('setRouter', vm.$router)
* }
*
setRouter (state, routerRef) {
state._router = routerRef
}
},
actions: {
/***
* You can then use the router like this
* ---
someAction ({ state }) {
if (state._router) {
state._router.push('/somewhere_else')
} else {
console.log('You forgot to set the router silly')
}
}
}
}
Update
After I published this answer I noticed that defining it the way I presented Typescript stopped detecting fields of state. I assume that's because I used any as a type. I probably could manually define the type, but it sounds like repeating yourself to me. That's way for now I ended up with a function instead of extending a class (I would be glad for letting me know some other solution if someone knows it).
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default (router: VueRouter) => {
return new Store({
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
import Vue from 'vue'
import App from './App.vue'
import createStore from './store'
// ...
new Vue({
router,
store: createStore(router),
render: createElement => createElement(App)
}).$mount('#app')
Initial answer content
I personally just made a wrapper for a typical Store class.
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default class extends Store<any> {
constructor (router: VueRouter) {
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
}
If You need $route You can just use router.currentRoute. Just remember You rather need router reactive if You want Your getters with router.currentRoute to work as expected.
And in "main.ts" (or ".js") I just use it with new Store(router).
import Vue from 'vue'
import App from './App.vue'
import Store from './store'
// ...
new Vue({
router,
store: new Store(router),
render: createElement => createElement(App)
}).$mount('#app')