Based on the title, is this possible?
I'm trying to test a simple component of a Vue application that contains a header and a button that redirects the user to the next page. I want to test if when the button is clicked an event/message is sent to the router and check if the destination route is correct.
The application is structured as follows:
App.Vue - Entry component
<script setup lang="ts"> import { RouterView } from "vue-router"; import Wrapper from "@/components/Wrapper.vue"; import Container from "@/components/Container.vue"; import HomeView from "@/views/HomeView.vue"; </script> <template> <Wrapper> <Container> <RouterView/> </Container> </Wrapper> </template> <style> body{ @apply bg-slate-50 text-slate-800 dark:bg-slate-800 dark:text-slate-50; } </style>
router/index.ts - Vue Router configuration file
import {createRouter, createWebHistory} from "vue-router"; import HomeView from "@/views/HomeView.vue"; export enum RouteNames { HOME = "home", GAME = "game", } const routes = [ { path: "/", name: RouteNames.HOME, component: HomeView, alias: "/home" }, { path: "/game", name: RouteNames.GAME, component: () => import("@/views/GameView.vue"), }, ]; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }); export default router;
HomeView.Vue - Entrance view of RouterView in App component
<template> <Header title="My App"/> <ButtonRouterLink :to="RouteNames.GAME" text="Start" class="btn-game"/> </template> <script setup> import Header from "@/components/Header.vue"; import ButtonRouterLink from "@/components/ButtonRouterLink.vue"; import {RouteNames} from "@/router/"; </script>
ButtonRouterLink.vue
<template> <RouterLink v-bind:to="{name: to}" class="btn"> {{ text }} </RouterLink> </template> <script setup> import { RouterLink } from "vue-router"; const props = defineProps({ to: String, text: String }) </script>
Considering that it uses CompositionAPI and TypeScript, is there a way to mock the router in Vitest tests?
This is an example of a test file structure (HomeView.spec.ts):
import {shallowMount} from "@vue/test-utils"; import { describe, test, vi, expect } from "vitest"; import { RouteNames } from "@/router"; import HomeView from "../../views/HomeView.vue"; describe("Home", () => { test ('navigates to game route', async () => { // Check if the button navigates to the game route const wrapper = shallowMount(HomeView); await wrapper.find('.btn-game').trigger('click'); expect(MOCK_ROUTER.push).toHaveBeenCalledWith({ name: RouteNames.GAME }); }); });
I tried multiple methods: vi.mock('vue-router'), vi.spyOn(useRoute,'push'), vue-router-mock library, but I can't get the test to run, it seems that the router is unavailable.
Based on @tao's suggestion, I realized that testing click events in HomeView was not a good test (testing the library not my app), so I added a test ButtonRouterLink, to see if it renders the Vue RouterLink correctly, using the to parameter:
import {mount, shallowMount} from "@vue/test-utils"; import { describe, test, vi, expect } from "vitest"; import { RouteNames } from "@/router"; import ButtonRouterLink from "../ButtonRouterLink.vue"; vi.mock('vue-router'); describe("ButtonRouterLink", () => { test (`correctly transforms 'to' param into a router-link prop`, async () => { const wrapper = mount(ButtonRouterLink, { props: { to: RouteNames.GAME } }); expect(wrapper.html()).toMatchSnapshot(); }); });
It renders an empty HTML string""
exports[`ButtonRouterLink > correctly transforms 'to' param into a router-link prop 1`] = `""`;
Accompanied by a not-so-helpful Vue warning (no expected type specified):
[Vue warn]: Invalid prop: type check failed for prop "to". Expected , got Object at <RouterLink to= { name: 'game' } class="btn btn-blue" > at <ButtonRouterLink to="game" ref="VTU_COMPONENT" > at <VTUROOT> [Vue warn]: Invalid prop: type check failed for prop "ariaCurrentValue". Expected , got String with value "page". at <RouterLink to= { name: 'game' } class="btn btn-blue" > at <ButtonRouterLink to="game" ref="VTU_COMPONENT" > at <VTUROOT> [Vue warn]: Component is missing template or render function. at <RouterLink to= { name: 'game' } class="btn btn-blue" > at <ButtonRouterLink to="game" ref="VTU_COMPONENT" > at <VTUROOT>
P粉7594512552024-03-28 00:46:02
this is possible.
There is an important difference between your example and the official example: in the component under test, you placed it in an intermediate component instead of usingRouterLink (or using
router.push button).
The first problem is that you use
shallowMount, which replaces the child component with an empty container. In your case this means that ButtonRouterLink
no longer contains
RouterLink and does nothing with click events, it just receives click events.
instead of
shallowMount, turning this into an integration test instead of a unit test
. Once you've tested that any
:to of
ButtonRouterLink is correctly converted to
{ name: to } and passed to
RouterLink, you just need to test Are these
:to passed correctly to the
<ButtonRouterLink /> in any component that uses them.
import { RouterLinkStub } from '@vue/test-utils'
const wrapper = shallowMount(YourComp, {
stubs: {
RouterLink: RouterLinkStub
}
})
expect(wrapper.findComponent(RouterLinkStub).props('to')).toEqual({
name: RouterNames.GAME
})
To my surprise, this test fails, claiming that the value of the :to attribute received in the RouterLinkStub is not
{ name: RouterNames.GAME }, but
'game', this is the value I pass to
ButtonRouterLink. It's like our component never converted
value to
{ name: value }.
It turns out that the problem is that
<RouterLink /> happens to be the root element of our component. This means that the component (
<BottomRouterLink>) is replaced in the DOM by
<RouterLinkStub>, but the value of
:to is from the former and not the latter read. In short, the test will succeed if the template looks like this:
<span>
<RouterLink ... />
</span>
To solve this problem, we need to import the actual RouterLink component (from
vue-router) and find
it, Instead of finding RouterLinkStub. Why?
@vue/test-utils is smart enough to find the stub component instead of the actual component, but this way,
.findComponent() will only match the replaced element, not the one it still has When it is
ButtonRouterLink (unprocessed). At this time, the value of
:to has been parsed into the value actually received by
RouterLink.
import { shallowMount } from '@vue/test-utils' import { describe, it, expect } from 'vitest' import ButtonRouterLink from '../src/ButtonRouterLink.vue' import { RouterLinkStub } from '@vue/test-utils' import { RouterLink } from 'vue-router' const TEST_STRING = 'this is a test string' describe('ButtonRouterLink', () => { it(`should transform 'to' into '{ name: to }'`, () => { const wrapper = shallowMount(ButtonRouterLink, { props: { to: TEST_STRING }, stubs: { RouterLink: RouterLinkStub } }) expect(wrapper.findComponent(RouterLink).props('to')).toEqual({ name: TEST_STRING }) }) })A bit tricky.