Home  >  Q&A  >  body text

Mock Vue RouterView/RouterLink in Vitest (Combined API)

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.

Updated (15/04/23)

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粉478188786P粉478188786205 days ago400

reply all(1)I'll reply

  • P粉759451255

    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 using

    RouterLink (or using router.push button).

    How will this change unit testing?

    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.

    The options to solve this problem are:

      a) Use
    • mount instead of shallowMount, turning this into an integration test instead of a unit test
    • b) Move the test of this function to the test of
    • ButtonRouterLink. 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.

    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:

    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 }.

    Quite strange.

    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.

    The complete test is as follows:

    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.

    reply
    0
  • Cancelreply