How to test vue router beforeRouteUpdate navigation guard? - vuejs2

First off, I'm using Vue-property-decorator.
My component has a component route, defined like this.
#Component({
components: {
ComponentA,
ComponentB
},
beforeRouteUpdate (to, _from, next) {
const { checkInDate, checkOutDate, items, currency, locale } = to.query;
const queryFullfilled = !!checkInDate && !!checkOutDate && !!items.length && !!currency && !!locale;
if ([BOOKING_PAGE, RESERVATION_PAGE].indexOf(to.name) > -1 && queryFullfilled) {
this.validateRooms();
}
next();
}
})
export default class Layout extends Vue {}
What do I need to do in my specs in order to cover what goes on in beforeRouteUpdate? I've googled this and people are pointing calling wrapper.vm.$options.beforeRouteUpdate.call()... that didn't work for me.
Has anyone done this before? Any code samples I can look at?
Thanks in advance.
John.

If anyone still looking for an answer following code will work.
const beforeRouteUpdate = wrapper.vm.$options.beforeRouteUpdate[0];
let nextFun = jest.fn();
// in place wrapper.vm you can send any object you want bcz when we call beforeRouteUpdate it looses this context
beforeRouteUpdate.call(wrapper.vm , "toObj", "fromObj", nextFun);
how to call beforeRouteUpdate github

import component from '#/components/component'
const wrapper = shallowMount()
// any additional setup
const to = {}
const from = {}
const next = jest.fn()
await component.beforeRouteLeave.call(wrapper.vm, to, from, next)
// asserts
expect(next).toHaveBeenCalledWith(false)
The route guard can be accessed through the component class. Pass in the vm of the instance so you have access to this .

Related

BeforeRouteEnter not working in production with script setup

I used the beforeRouteEnter hook in vue-router to load data from two different endpoints using axios. I used promise.all() to load the data and then passed it to the component using next(). It seems to be working in development but when it is hosted on vercel the data isn't rendered on the component.
import axios from "axios"
import NProgress from "nprogress"
export default {
name: "DetailView",
beforeRouteEnter(to, from, next) {
NProgress.start()
const getDetails = axios.get(`api/grades/${ to.params.id }/`)
const getResults =
axios.get(`api/results/`, {
params: {
'grade-id': to.params.id,
}
})
Promise.all([getDetails, getResults])
.then(([details, results]) => {
next((vm) => {
vm.details = details.data
vm.results = results.data
})
})
.finally(NProgress.done())
},
}
I used a <script setup>...</script> for the setup function with the
import { ref } from "vue"
const details = ref({})
const grades = ref({})
I'm relatively new to javascript too and still trying to understand promises and async/await very well. Thank you
Finally found a solution to the problem. Components using <script setup> in vue are closed by default, he public instance of the component, which is retrieved via template refs or $parent chains, will not expose any of the bindings declared inside <script setup>. From the vue docs.
I had to explicitly expose the properties used in the beforeRouteEnter navigation guard using the defineExpose compiler macro
defineExpose(
{
details,
results
}
)

Vue accessing Vuex store in router file

I am trying to access my store in the router file as follow, but it's not working. Giving me undefined when I console. Any advice? I am using Vue 3. Thank you in advance!
import store from '../store/index'
const preventRoutes = {
beforeEach: (to, from, next) => {
console.log(store.getters.getLoginState);
if (store.getters.getLoginState === "true") {
console.log("i reached if")
next();
} else {
console.log("i reached else")
next("/");
}
}
}
maybe is because the before route load before the imports
try to import it directly from the before route:
router.beforeEach(function (...) {
const store = require('./store')
store.blah...
})
I have similar situation.
import store from "../store"; to router
then I can use store's API (commit etc ) inside router.beforeEach

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

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

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:
}
}
}

In vue-router, how to use beforeLeave

I want to finish some information verify before one component leave.
I've scanned the vue-router document: https://router.vuejs.org
But I use vue-cli, in my file: router1.vue,
console.log(this.$router.beforeLeave) -> undefined
How can I use it?
Add this to your router1.vue:
export default {
//...
beforeRouteLeave (to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
},
//...
}
for example:
beforeRouteLeave (to, from , next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
And it will be called before this route leave.
ref: https://router.vuejs.org/en/advanced/navigation-guards.html