I am unable to mount a component during unit testing due to the the route object being undefined in the setup method during mounting. The guides seem aimed at Vue2 and the options API
References:
How to write test that mocks the $route object in vue components
How to unit testing with jest in vue composition api component?
https://vue-test-utils.vuejs.org/guides/#using-with-typescript
https://vue-test-utils.vuejs.org/guides/#using-with-vue-router
Error
● CoachItem.vue › displays alert when item is clicked
TypeError: Cannot read property 'path' of undefined
64 | fullName: computed(() => props.firstName + " " + props.lastName),
65 | coachContactLink: computed(
> 66 | () => route.path + "/" + props.id + "/contact"
// @/tests/unit/example.spec.ts
import CoachItem from "@/components/coaches/CoachItem.vue"
import router from "@/router"
describe("CoachItem.vue", () => {
it("displays alert when item is clicked", async () => {
//const route = { path: 'http://www.example-path.com' }
router.push('/')
await router.isReady()
const wrapper = mount(CoachItem); //adding this line causes failure
//await wrapper.trigger('click');
//const dialog = wrapper.find('dialog');
//(dialog.exists()).toBeTruthy()
})
})
// @/components/UserAlert.vue
<template>
<div class="backdrop" @click="closeDialog"></div>
<dialog open>
<header>
<h2>{{ title }}</h2>
</header>
<div>
<slot name="content"></slot>
</div>
<menu>
<button @click="closeDialog">Close</button>
</menu>
</dialog>
</template>
<script lang="ts>
import { defineComponent } from "vue";
export default defineComponent({
props: ['title'],
emits: ['close'],
setup(_, context) {
function closeDialog() {
context.emit('close');
}
return { closeDialog };
},
});
</script>
// @/components/coaches.CoachItem.vue
<template>
<user-alert v-if="alertIsVisible" title="Alert!" @close="hideAlert">
<template v-slot:content><p>this is a slot</p></template>
</user-alert>
<li @click="showAlert">
<h3>{{ fullName }}</h3>
<h4>${{ rate }}/hour</h4>
<div>
<base-badge
v-for="area in areas"
:key="area"
:type="area"
:title="area"
></base-badge>
</div>
<div class="actions">
<base-button mode="outline" link :to="coachContactLink"
>Contact</base-button
>
<base-button link :to="coachDetailsLink">View Details</base-button>
</div>
</li>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from "vue";
import { useRoute } from "vue-router";
import useAlert from "../../components/hooks/useAlert";
export default defineComponent({
props: {
id: {
type: String,
required: true,
},
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
rate: {
type: Number,
required: true,
},
areas: {
type: Object as PropType<Array<string>>,
required: true,
},
},
setup(props) {
const route = useRoute();
const alertTitle = ref("delete user?");
return {
fullName: computed(() => props.firstName + " " + props.lastName),
coachContactLink: computed(
() => route.path + "/" + props.id + "/contact"
),
coachDetailsLink: computed(() => route.path + "/" + props.id),
...useAlert()
};
},
});
</script>
// @/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import {store, key } from "./store";
import UserAlert from "@/components/UserAlert.vue";
createApp(App)
.component('UserAlert', UserAlert)
.use(store, key)
.use(router)
.mount("#app");
// @/router/index.ts
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;
Going via the examples in vitest issue #1918 and the vue-test-utils composition documentation
The following mock allows a component with useRouter
or useRoute
to work:
import { mount } from '@vue/test-utils'
import { expect, it, vi } from 'vitest'
import CompWithRoute from './CompWithRoute.vue'
vi.mock('vue-router', () => {
return {
useRouter: vi.fn(() => ({
push: vi.fn(),
})),
useRoute: vi.fn(()=> ({
fullPath: '',
hash: '',
matched: [],
meta: {},
name: undefined,
params: {},
path: '/guppies',
query: {
search: 'ireland'
},
redirectedFrom: undefined,
}))
}
})
it('should render the route loving component', () => {
const wrapper = mount(CompWithRoute)
})
If the call needs to be tested, mockImplementationOnce
can inject a spy (although typescript doesn't like the lax implementation of the Router
mock)
import { mount } from '@vue/test-utils'
import { afterEach, expect, it, vi } from 'vitest'
import * as routerExports from 'vue-router'
import CompWithRoute from './CompWithRoute.vue'
const useRouter = vi.spyOn(routerExports, 'useRouter')
afterEach(() => {
vi.clearAllMocks()
})
it('should push a new route on search', async () => {
const push = vi.fn()
useRouter.mockImplementationOnce(() => ({
push
}))
const wrapper = mount(CompWithRoute)
const search = wrapper.find('#search-input')
await search.setValue('ireland')
await search.trigger('keyup.enter')
expect(push).toHaveBeenCalledWith({ query: { search: 'ireland'} })
})
It's also possible to inject a global spy into the hoisted mock Router
implementation and reference that in your expectations.
const mock_push = vi.fn()
vi.mock('vue-router', () => ({
useRouter: vi.fn(() => ({
push: mock_push,
})),
}))
afterEach(() => {
vi.clearAllMocks()
})
it('should push a new route on search', async () => {
const wrapper = mount(CompWithRoute)
const search = wrapper.find('#search-input')
await search.setValue('ireland')
await search.trigger('keyup.enter')
expect(mock_push).toHaveBeenCalledWith({ query: { search: 'ireland'} })
})