How to test the access of protected route guard with in Vuejs? - vue.js

I implemented a route guard to protect the /settings route with the vue-router method beforeEnter().
I try to test that the route is protected to admins only.
I am using Vuejs 2, Vue-router, Vuex and vue-test-utils.
router.js
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
export default new Router({
routes: [
..., // other routes
{
path: '/settings',
name: 'Settings',
component: () => import('./views/settings'),
beforeEnter: (to, from, next) => {
next(store.state.isAdmin);
}
}
]
});
the unit test:
test('navigates to /settings view if the user is admin', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueRouter);
const router = new VueRouter();
const wrapper = shallowMount(App, {
stubs: ['router-link', 'router-view'],
localVue,
mocks: {
$store: store
},
router
});
wrapper.vm.$route.push({ path: '/settings' });
// test if route is set correctly
});
current logs output:
wrapper.vm.$route` is undefined.
How can I mount the App correctly and access the router? How can I test the current route to verify that the admin user has been redirected succesfully?

Thank logan for the link. It seems like the best possible solution:
As of now there is no easy way to test navigation guards. If you want to simulate the event triggering by calling router.push function, you are going to have a hard time. A better easier solution is to call the guard manually in beforeEach(), but even this solution doesn't have a clean approach. See the following example:
beforeRouteEnter
// my-view.js
class MyView extends Vue {
beforeRouteEnter (to, from, next) {
next(function (vm) {
vm.entered = true;
});
}
}
// my-view.spec.js
it('should trigger beforeRouteEnter event', function () {
const view = mount(MyView);
const spy = sinon.spy(view.vm.$options.beforeRouteEnter, '0'); // you can't just call view.vm.beforeRouteEnter(). The function exists only in $options object.
const from = {}; // mock 'from' route
const to = {}; // mock 'to' route
view.vm.$options.beforeRouteEnter[0](to, from, cb => cb(view.vm));
expect(view.vm.entered).to.be.true;
expect(spy).to.have.been.called;
});

Related

Routes.js call function from composable

I am working on my routes.js and I want to check if user has access to module before redirecting them by calling the function hasAccess() from a composable but it's giving me this warning:
[Vue warn]: inject() can only be used inside setup() or functional components.
routes.js
import { createWebHistory, createRouter } from "vue-router";
import useFN from "./composables/FN"
const routes = [
// define routes here
]
const router = createRouter({
history: createWebHistory(),
routes,
})
const { hasAccess } = useFN() // use function from composable
router.beforeEach(async (to, from, next) => {
console.log(await hasAccess()); // call function from composable
next()
}
export default router;
How can I call a function from composable if I don't have setup()?
Usually I don't have any issues using composables I create for my self as they are usually very basic, but according to Vue 3 - inject() can only be used inside setup() or functional components
composable your composable appears to be using provide/inject which is intended within a setup() method
I will like to propose another approach if viable.
You could call a method(action), getter inside your store instead, if you are using one.
I often use that perform checks before navigating to a route for example if a user account is suspended or the authenticated user does not have the intended privilege
import { createRouter, createWebHistory } from 'vue-router'
import store from '#/store'
...
const account = {
'ACTIVE': 1,
'RESTRICTED': 2,
'DECLINED': 0
}
const routes = [
{
path: '/some-path',
component: () => import('../components/...'),
props: route => ({ reference: route.query.reference }),
meta: { requiresAuth: true },
beforeEnter: async (to, from) => {
if (store.getters['Auth/authUser'].account_status == account.RESTRICTED) {
return false
}
return true
},
},
...
]
```

Testing Vue Router in component navigation guard

I'm trying to unit test (with vue-test-utils) a component, which has a beforeRouteUpdate in component navigation Guard like this:
<template>
...
</template>
<script>
export default {
computed: {
...
}
...
beforeRouteUpdate(to, from, next) {
// code to be tested
this.$store.dispatch('setActiveTask')
}
}
</script>
I do render the component in the test file with shallowMount and mock stuff like the $store.
beforeEach(() => {
cmp = shallowMount(Task, {
mocks: {
$store: store,
$route: {
params: {
taskID: 12,
programID: 1
}
}
},
i18n: new VueI18N({
silentTranslationWarn: true
}),
stubs: {
'default-layout': EmptySlotComponent,
'nested-router': EmptySlotComponent,
RouterLink: RouterLinkStub
}
})
})
it('has beforeRouteUpdate hook', () => {
// how do i call beforeRouteUpdate now?
// cmp.vm.beforeRouteUpdate(...)
}
Has anyone some ideas about this?
UPDATE:
I created a minimal example with #vue/cli and Mocha + Chai as unit test tools, which can be found here: https://github.com/BerniWittmann/vue-test-navigation-guard-reproduction
Got it working, but with a kind of hacky solution.
my test now looks like this:
it('test', () => {
const beforeRouteUpdate = wrapper.vm.$options.beforeRouteUpdate
const $store = {
dispatch: () => {
}
}
spyOn($store, 'dispatch').and.returnValue({ then: (arg) => arg() })
let next = jasmine.createSpy('next')
render()
beforeRouteUpdate.call({ $store }, {
params: {
taskID: 1
}
}, {}, next)
expect($store.dispatch).toHaveBeenCalledWith('setActiveTask', 1)
expect(next).toHaveBeenCalled()
})
The navigation guard is available in wrapper.vm.$options.beforeRouteUpdate, but calling this I lost the context of this so I was not able to call this.$store.dispatch in the component navigation guard, thats why I needed to use the .call() method
Following code worked fine for me for testing route navigation guards.
const beforeRouteUpdate = wrapper.vm.$options.beforeRouteUpdate[0];
let nextFun = jest.fn();
beforeRouteUpdate.call(wrapper.vm , "toObj", "fromObj", nextFun);
testing route navigation guards git hub
how to test vue router beforeupdate navigation guard

How to setup vuex and vue-router to redirect when a store value is not set?

I'm working with the latest versions of vue-router, vuex and feathers-vuex and I have a problem with my router.
What I'm doing is to check if a route has the property "requiresAuth": true in the meta.json file. If it does then check the value of store.state.auth.user provided by feathers-vuex, if this value is not set then redirect to login.
This works fine except when I'm logged in and if I reload my protected page called /private then it gets redirected to login so it seems that the value of store.state.auth.user is not ready inside router.beforeEach.
So how can I set up my router in order to get the value of the store at the right moment?
My files are as follow:
index.js
import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'
const meta = require('./meta.json')
// Route helper function for lazy loading
function route (path, view) {
return {
path: path,
meta: meta[path],
component: () => import(`../components/${view}`)
}
}
Vue.use(Router)
export function createRouter () {
const router = new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: [
route('/login', 'Login')
route('/private', 'Private'),
{ path: '*', redirect: '/' }
]
})
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
if (!store.state.auth.user) {
next('/login')
} else {
next()
}
} else {
next()
}
})
return router
}
meta.json
{
"/private": {
"requiresAuth": true
}
}
I fixed the issue by returning a promise from vuex action and then run the validations
router.beforeEach((to, from, next) => {
store.dispatch('auth/authenticate').then(response => {
next()
}).catch(error => {
if (!error.message.includes('Could not find stored JWT')) {
console.log('Authentication error', error)
}
(to.meta.requiresAuth) ? next('/inicio-sesion') : next()
})
})

Vue Router: Keep query parameter and use same view for children

I'm rewriting an existing Angular 1 application with Vue.
The application always needs to authenticate an user by locale, id and token before entering any views. Respecting the conventions of our API, I specified the token as a query parameter within my main parent route.
Coming from the existing Angular's UI router implementation I thought this is the way to go:
// main.js
new Vue({
el: '#app',
router,
store,
template: '<router-view name="main"></router-view>'
})
// router.js
const router = new Router({
mode: 'history',
routes: [
{
name: 'start',
path : '/:locale/:id', // /:locale/:id?token didn't work
query: {
token: null
},
beforeEnter (to, from, next) {
// 1. Get data from API via locale, id and token
// 2. Update store with user data
},
components: {
main: startComponent
},
children: [{
name: 'profile',
path: 'profile',
components: {
main: profileComponent
}
}]
}
]
})
When I navigate to the profile view, I expect the view to change and the query token to stay, e.g. /en-US/123?token=abc to /en-US/123/profile?token=abc. Neither happens.
I'm using Vue 2.3.3 and Vue Router 2.3.1.
Questions:
Can I keep query parameters when navigating to child routes?
Am I using the Vue router right here? Or do I need to blame my UI router bias?
You can resolve this in the global hooks of Router
import VueRouter from 'vue-router';
import routes from './routes';
const Router = new VueRouter({
mode: 'history',
routes
});
function hasQueryParams(route) {
return !!Object.keys(route.query).length
}
Router.beforeEach((to, from, next) => {
if(!hasQueryParams(to) && hasQueryParams(from)){
next({name: to.name, query: from.query});
} else {
next()
}
})
If the new route (to) does not have its own parameters, then they will be taken from the previous route (from)
You can add in a mounted hook a router navigation guard beforeEach like this preserveQueryParams:
// helpers.js
import isEmpty from 'lodash/isEmpty';
const preserveQueryParams = (to, from, next) => {
const usePreviousQueryParams = isEmpty(to.query) && !isEmpty(from.query);
if (usePreviousQueryParams) {
next({ ...to, query: from.query });
} else {
next();
}
};
// StartComponent.vue
removeBeforeEachRouteGuard: Function;
mounted() {
this.removeBeforeEachRouteGuard = this.$router.beforeEach(preserveQueryParams);
}
// don't forget to remove created guard
destroyed() {
this.removeBeforeEachRouteGuard();
// resetting query can be useful too
this.$router.push({ query: undefined });
}

How to write test that mocks the $route object in vue components

I have a component that contains statement like this.$route.fullPath, how should I mock value of fullPathof $route object if I want to test that component?
I disagree with the top answer - you can mock $route without any issue.
On the other hand, installing vue-router multiple times on the base constructor will cause you problems. It adds $route and $router as read only properties. Which makes it impossible to overwrite them in future tests.
There are two ways to achieve this with vue-test-utils.
Mocking vue-router with the mocks option
const $route = {
fullPath: 'full/path'
}
const wrapper = mount(ComponentWithRouter, {
mocks: {
$route
}
})
wrapper.vm.$route.fullPath // 'full/path'
You can also install Vue Router safely by using createLocalVue:
Installing vue-router safely in tests with createLocalVue
const localVue = createLocalVue()
localVue.use(VueRouter)
const routes = [
{
path: '/',
component: Component
}
]
const router = new VueRouter({
routes
})
const wrapper = mount(ComponentWithRouter, { localVue, router })
expect(wrapper.vm.$route).to.be.an('object')
Best not mock vue-router but rather use it to render the component, that way you get a proper working router. Example:
import Vue from 'vue'
import VueRouter from 'vue-router'
import totest from 'src/components/totest'
describe('totest.vue', () => {
it('should totest renders stuff', done => {
Vue.use(VueRouter)
const router = new VueRouter({routes: [
{path: '/totest/:id', name: 'totest', component: totest},
{path: '/wherever', name: 'another_component', component: {render: h => '-'}},
]})
const vm = new Vue({
el: document.createElement('div'),
router: router,
render: h => h('router-view')
})
router.push({name: 'totest', params: {id: 123}})
Vue.nextTick(() => {
console.log('html:', vm.$el)
expect(vm.$el.querySelector('h2').textContent).to.equal('Fred Bloggs')
done()
})
})
})
Things to note:
I'm using the runtime-only version of vue, hence render: h => h('router-view').
I'm only testing the totest component, but others might be required if they're referenced by totest eg. another_component in this example.
You need nextTick for the HTML to have rendered before you can look at it/test it.
One of the problems is that most of the examples I found referred to the old version of vue-router, see the migrations docs, eg. some examples use router.go() which now doesn't work.
No answer was helping me out, So I dig into vue-test-utils documentation and found myself a working answer, so you need to import.
import { shallowMount,createLocalVue } from '#vue/test-utils';
import router from '#/router.ts';
const localVue = createLocalVue();
We created a sample vue instance. While testing you need to use shallowMount so you can provide vue app instance and router.
describe('Components', () => {
it('renders a comment form', () => {
const COMMENTFORM = shallowMount(CommentForm,{
localVue,
router
});
})
})
You can easily pass router and to shallow mount and it does not gives you the error. If you want to pass store you use:
import { shallowMount,createLocalVue } from '#vue/test-utils';
import router from '#/router.ts';
import store from '#/store.ts';
const localVue = createLocalVue();
And then pass store:
describe('Components', () => {
it('renders a comment form', () => {
const COMMENTFORM = shallowMount(CommentForm,{
localVue,
router,
store
});
})
})
This solution solved the following errors:
Cannot read property 'params' of undefined when using this.$route.params.id
Unknown custom element router-link
✔
Easiest method i found is to use localVue
import { createLocalVue, mount } from '#vue/test-utils';
import VueRouter from 'vue-router';
import Vuex from 'vuex';
import ComponentName from '#/components/ComponentName.vue';
// Add store file if any getters is accessed
import store from '#/store/store';
describe('File name', () => {
const localVue = createLocalVue();
localVue.use(VueRouter);
// Can also be replaced with route(router.js) file
const routes = [
{
path: '/path',
component: ComponentName,
name: 'Route name'
}
];
const router = new VueRouter({ routes });
// if needed
router.push({
name: 'Route name',
params: {}
});
const wrapper = mount(ComponentName, {
localVue,
router,
store
});
test('Method()', () => {
wrapper.vm.methodName();
expect(wrapper.vm.$route.path)
.toEqual(routes[0].path);
});
});
Hope it helps!!!
Why are all answers so complicated? You can just do:
...
wrapper = mount(HappyComponent, {
mocks: {
$route: { fullPath: '' }
},
})
...
You dont have to specifically "mock" a router. Your application can set VueRouter in the global vue scope and you can still make it do what you want in your tests without issue.
Read the localVue usage with VueRouter: https://vue-test-utils.vuejs.org/guides/#using-with-vue-router.
I am currently pulling in a complex router from our main app and am able to jest.spyOn() calls to router.push() as well as setting the path before the component is created running shallowMount() for some route handling in a created() hook.
The Workaround
// someVueComponent.vue
<template>
... something
</template>
<script>
...
data () {
return {
authenticated: false
}
},
...
created () {
if(!this.authenticated && this.$route.path !== '/'){
this.$router.push('/')
}
}
</script>
// someVueComponent.spec.js
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import { shallowMount, createLocalVue } from '#vue/test-utils'
import SomeVueComponent from 'MyApp/components/someVueComponent'
import MyAppRouter from 'MyApp/router'
import MyAppCreateStore from 'MyApp/createStore'
import merge from 'lodash.merge'
function setVueUseValues (localVue) {
localVue.use(Vuex)
localVue.use(VueRouter)
// other things here like custom directives, etc
}
beforeEach(() => {
// reset your localVue reference before each test if you need something reset like a custom directive, etc
localVue = createLocalVue()
setVueUseValues(localVue)
})
let localVue = createLocalVue()
setVueUseValues(localVue)
test('my app does not react to path because its default is "/"', () => {
const options = {
localVue,
router: MyAppRouter,
store: MyAppCreateStore()
}
const routerPushSpy = jest.spyOn(options.router, 'push')
const wrapper = shallowMount(SomeVueComponent, options)
expect(routerPushSpy).toHaveBeenCalledTimes(0)
})
test('my app reacts to path because its not "/" and were not authenticated', () => {
const options = {
localVue,
router: MyAppRouter,
store: MyAppCreateStore()
}
const routerPushSpy = jest.spyOn(options.router, 'push')
options.router.push('/nothomepath')
expect(routerPushSpy).toHaveBeenCalledWith('/nothomepath') // <- SomeVueComponent created hook will have $route === '/nothomepath' as well as fullPath
const wrapper = shallowMount(SomeVueComponent, options)
expect(routerPushSpy).toHaveBeenCalledWith('/') // <- works
})
The above is done with the idea that I need the $route state changed before SomeVueComponent.vue is created/mounted. Assuming you can create the wrapper and want to test that the component this.$router.push('/something') based on some other state or action you can always spy on the wrapper.vm instance
let routerPushSpy = jest.spyOn(wrapper.vm.$router, 'push') // or before hooks, etc
As of this writing there seems to be an open defect which keeps the following from working because vm.$route will always be undefined, making the above the only option (that I know of) as there is no other way to "mock" the $route because installing VueRouter writes read only properties to $route.
From the vue-test-utils docs https://vue-test-utils.vuejs.org/guides/#mocking-route-and-router:
import { shallowMount } from '#vue/test-utils'
const $route = {
path: '/some/path'
}
const wrapper = shallowMount(Component, {
mocks: {
$route
}
})
wrapper.vm.$route.path // /some/path
If your interested here is the github link to a reproduction of the issue: https://github.com/vuejs/vue-test-utils/issues/1136
All kudos to #SColvin for his answer; helped find an answer in my scenario wherein I had a component with a router-link that was throwing a
ERROR: '[Vue warn]: Error in render function: (found in <RouterLink>)'
during unit test because Vue hadn't been supplied with a router. Using #SColvin answer to rewrite the test originally supplied by vue-cli from
describe('Hello.vue', () =>
{
it('should render correct contents', () =>
{
const Constructor = Vue.extend(Hello);
const vm = new Constructor().$mount();
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App');
});
to
describe('Hello.vue', () =>
{
it('should render correct contents', () =>
{
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{ path: '/', name: 'Hello', component: Hello },
],
});
const vm = new Vue({
el: document.createElement('div'),
/* eslint-disable object-shorthand */
router: router,
render: h => h('router-view'),
});
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App');
});
});
Not needing to pass parameters in to the view I could simplify the component as the default render, no need to push and no need to wait nextTick. HTH someone else!
Adding to the great answer from #SColvin, here's an example of this working using Avoriaz:
import { mount } from 'avoriaz'
import Vue from 'vue'
import VueRouter from 'vue-router'
import router from '#/router'
import HappyComponent from '#/components/HappyComponent'
Vue.use(VueRouter)
describe('HappyComponent.vue', () => {
it('renders router links', () => {
wrapper = mount(HappyComponent, {router})
// Write your test
})
})
I believe this should work with vue-test-utils, too.
Take a look at this example using vue-test-utils, where I'm mocking both router and store.
import ArticleDetails from '#/components/ArticleDetails'
import { mount } from 'vue-test-utils'
import router from '#/router'
describe('ArticleDetails.vue', () => {
it('should display post details', () => {
const POST_MESSAGE = 'Header of our content!'
const EXAMPLE_POST = {
title: 'Title',
date: '6 May 2016',
content: `# ${POST_MESSAGE}`
}
const wrapper = mount(ArticleDetails, {
router,
mocks: {
$store: {
getters: {
getPostById () {
return EXAMPLE_POST
}
}
}
}
})
expect(wrapper.vm.$el.querySelector('h1.post-title').textContent.trim()).to.equal(EXAMPLE_POST.title)
expect(wrapper.vm.$el.querySelector('time').textContent.trim()).to.equal(EXAMPLE_POST.date)
expect(wrapper.vm.$el.querySelector('.post-content').innerHTML.trim()).to.equal(
`<h1>${POST_MESSAGE}</h1>`
)
})
})
This is what I've been doing as per this article:
it('renders $router.name', () => {
const scopedVue = Vue.extend();
const mockRoute = {
name: 'abc'
};
scopedVue.prototype.$route = mockRoute;
const Constructor = scopedVue.extend(Component);
const vm = new Constructor().$mount();
expect(vm.$el.textContent).to.equal('abc');
});
You can mock to vm.$router by setting vm._routerRoot._router
For example
var Constructor = Vue.extend(Your_Component)
var vm = new Constructor().$mount()
var your_mock_router = {hello:'there'}
vm.$router = your_mock_router //An error 'setting a property that has only a getter'
vm._routerRoot._router = your_mock_router //Wow, it works!
You can double check their source code here: https://github.com/vuejs/vue-router/blob/dev/dist/vue-router.js#L558
Easiest way i've found is to mock the $route.
it('renders $router.name', () => {
const $route = {
name: 'test name - avoriaz'
}
const wrapper = shallow(Component, {
mocks: {
$route
}
})
expect(wrapper.text()).to.equal($route.name)
})