vue-router-mock
Easily mock routing interactions in your Vue 3 apps
Installation
pnpm i -D vue-router-mock
yarn add -D vue-router-mock
npm install -D vue-router-mock
Requirements
This library
@vue/test-utils
>= 2.4.0- vue 3 and vue router 4
Goal
The goal of Vue Router Mock is to enable users to unit and integration test navigation scenarios. This means tests that are isolated enough to not be end to end tests (e.g. using Cypress) or are edge cases (e.g. network failures). Because of this, some scenarios are more interesting as end to end tests, using the real vue router.
Introduction
Vue Router Mock exposes a few functions to be used individually and they are all documented through TS. But most of the time you want to globally inject the router in a setupFilesAfterEnv file. Create a tests/router-mock-setup.js
file at the root of your project (it can be named differently):
import {
VueRouterMock,
createRouterMock,
injectRouterMock,
} from 'vue-router-mock'
import { config } from '@vue/test-utils'
const router = createRouterMock()
beforeEach(() => {
router.reset()
injectRouterMock(router)
})
config.plugins.VueWrapper.install(VueRouterMock)
Note: you might need to write this file in CommonJS for Jest. In Vite, you can write it in Typescript
Then add this line to your jest.config.js
:
setupFilesAfterEnv: ['<rootDir>/tests/router-mock-setup.js'],
or to your vitest.config.ts
:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'happy-dom',
setupFiles: ['./tests/setup-router-mock.ts'],
},
})
This will inject a router in all your tests. If for specific tests, you need to inject a different version of the router, you can do so:
import { createRouterMock, injectRouterMock } from 'vue-router-mock'
describe('SearchUsers', () => {
const router = createRouterMock({
})
beforeEach(() => {
injectRouterMock(router)
})
it('should paginate', async () => {
const wrapper = mount(SearchUsers)
expect(wrapper.router).toBe(router)
wrapper.find('button.next-page').click()
expect(wrapper.router.push).toHaveBeenCalledWith(
expect.objectContaining({ query: { page: 2 } })
)
expect(wrapper.router.push).toHaveBeenCalledTimes(1)
await router.getPendingNavigation()
await wrapper.vm.nextTick()
expect(wrapper.find('#user-results .user').text()).toMatchSnapshot()
})
})
If you need to create a specific version of the router for one single test (or a nested suite of them), you should call the same functions:
it('should paginate', async () => {
const router = createRouterMock()
injectRouterMock(router)
const wrapper = mount(SearchUsers)
})
Guide
Accessing the Router Mock instance
You can access the instance of the router mock in multiple ways:
-
Access wrapper.router
:
it('tests something', async () => {
const wrapper = mount(MyComponent)
await wrapper.router.push('/new-location')
})
-
Access it through wrapper.vm
:
it('tests something', async () => {
const wrapper = mount(MyComponent)
await wrapper.vm.$router.push('/new-location')
expect(wrapper.vm.$route.name).toBe('NewLocation')
})
-
Call getRouter()
inside of a test:
it('tests something', async () => {
const router = getRouter()
const wrapper = mount(MyComponent)
await router.push('/new-location')
})
Setting parameters
setParams
allows you to change route params
without triggering a navigation:
it('should display the user details', async () => {
const wrapper = mount(UserDetails)
getRouter().setParams({ userId: 12 })
})
It can be awaited if you need to wait for Vue to render again:
it('should display the user details', async () => {
const wrapper = mount(UserDetails)
await getRouter().setParams({ userId: 12 })
})
setQuery
and setHash
are very similar.
They can be used to set the route query or hash without triggering a navigation,
and can be awaited too.
Setting the initial location
By default the router mock starts on START_LOCATION
. In some scenarios this might need to be adjusted by pushing a new location and awaiting it before testing:
it('should paginate', async () => {
await router.push('/users?q=haruno')
const wrapper = mount(SearchUsers)
})
You can also set the initial location for all your tests by passing an initialLocation
:
const router = createRouterMock({
initialLocation: '/users?q=jolyne',
})
initialLocation
accepts anything that can be passed to router.push()
.
Simulating navigation failures
You can simulate the failure of the next navigation
Simulating a navigation guard
By default, all navigation guards are ignored so that you can simulate the return of the next guard by using setNextGuardReturn()
without depending on existing ones:
router.setNextGuardReturn(false)
router.setNextGuardReturn('/login')
If you want to still run existing navigation guards inside component, you can active them when creating your router mock:
const router = createRouterMock({
runInComponentGuards: true,
runPerRouteGuards: true,
})
Stubs
By default, both <router-link>
and <router-view>
are stubbed but you can override them locally. This is specially useful when you have nested <router-view>
and you rely on them for a test:
const wrapper = mount(MyComponent, {
global: {
stubs: { RouterView: MyNestedComponent },
},
})
You need to manually specify the component that is supposed to be displayed because the mock won't be able to know the level of nesting.
NOTE: this might change to become automatic if the necessary routes
are provided.
Testing libraries
Vue Router Mock automatically detects if you are using Sinon.js, Jest, or Vitest and use their spying methods. You can of course configure Vue Router Mock to use any spying library you want.
For example, if you use Vitest with globals: false
,
then you need to manually configure the spy
option and pass vi.fn()
to it:
const router = createRouterMock({
spy: {
create: fn => vi.fn(fn),
reset: spy => spy.mockClear(),
},
})
Caveats
Nested Routes
By default, the router mock comes with one single catch all route. You can add routes calling the router.addRoute()
function but if you add nested routes and you are relying on running navigation guards, you must manually set the depth of the route you are displaying. This is because the router has no way to know which level of nesting you are trying to display. e.g. Imagine the following routes
:
const routes = [
{
path: '/users',
component: UserView,
children: [
{ path: ':id', component: UserDetail },
],
},
]
router.depth.value = 1
const wrapper = mount(UserDetail)
Remember, this is not necessary if you are not adding routes or if they are not nested.
Related
License
MIT