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>
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
How will this change unit testing?(or using
router.pushbutton).
shallowMount
The options to solve this problem are:, which replaces the child component with an empty container. In your case this means that ButtonRouterLink
no longer contains
RouterLinkand does nothing with click events, it just receives click events.
a) Use- mount
b) Move the test of this function to the test of - ButtonRouterLink
instead of
shallowMount, turning this into an integration test instead of a unit test
. Once you've tested that any
:toof
ButtonRouterLinkis correctly converted to
{ name: to }and passed to
RouterLink, you just need to test Are these
:topassed correctly to the
<ButtonRouterLink />in any component that uses them.
The second problem created by moving this functionality into its own component is slightly more subtle. Generally, I would expect the following tests for ButtonRouterLink to work:
:to
Quite strange.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
valueto
{ name: value }.
It turns out that the problem is that
To solve this problem, we need to import the actual<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
:tois from the former and not the latter read. In short, the test will succeed if the template looks like this:
RouterLink
The complete test is as follows:component (from
vue-router) and find
it, Instead of finding RouterLinkStub. Why?
@vue/test-utilsis 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
:tohas been parsed into the value actually received by
RouterLink.