Имитация Vue RouterView/RouterLink в Vitest (API композиции)

Судя по заголовку, это вообще возможно?

Я пытаюсь протестировать простой компонент приложения Vue, который содержит заголовок и кнопку, которая направляет пользователя на следующую страницу. Я хочу проверить, что после нажатия кнопок событие/сообщение отправляется на маршрутизатор, и проверьте правильность маршрута назначения.

Структура приложения:

App.Vue — компонент входа

<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

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 — представление входа для RouterView в компоненте приложения.

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

Учитывая, что он использует CompositionAPI, TypeScript, есть ли способ издеваться над маршрутизатором в тестах Vitest?

Вот пример структуры тестового файла (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 });
    });
});

Я пробовал несколько способов: vi.mock('vue-router'), vi.spyOn(useRoute,'push'), библиотеку vue-router-mock, и мне всегда не удавалось запустить тесты вроде роутера просто нет.

Обновление (15.04.23)

Следуя совету @tao, я понял, что тестирование щелчка в HomeView не очень хороший тест (тестирование библиотек, а не моего приложения), поэтому я добавил тест для ButtonRouterLink, чтобы увидеть, правильно ли он отображает Vue RouterLink, используя to параметр:

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();
    });
});

Что отображает пустую строку HTML ""

exports[`ButtonRouterLink > correctly transforms 'to' param into a router-link prop 1`] = `""`;

сопровождается довольно бесполезным предупреждением Vue (ожидаемые типы не указаны):

[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>
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
202
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это возможно.

Между вашим примером и официальным есть одно важное отличие: вместо использования RouterLink (или кнопки, которая использует router.push) внутри тестируемого компонента вы поместили его в промежуточный компонент.

Как это меняет модульный тест?

Первая проблема заключается в том, что вы используете smallMount, который заменяет дочерние компоненты пустыми контейнерами. В вашем случае это означает, что ButtonRouterLink больше не содержит RouterLink и ничего не делает с кликом. Только получает.

Варианты обойти это:

  • а) используйте mount вместо shallowMount, чтобы сделать это интеграционным тестом, а не единичным
  • б) переместите тест этой функциональности внутрь теста на ButtonRouterLink. После того, как вы проверите, что любой :to в ButtonRouterLink правильно преобразован в { name: to } и передан в RouterLink, все, что вам нужно проверить, это правильность передачи :to в <ButtonRouterLink /> в любом компоненте, использующем их.

Вторая проблема, которую создает перенос этой функциональности в отдельный компонент, немного более тонкая. Обычно я ожидал, что следующий тест ButtonRouterLink сработает:

import { RouterLinkStub } from '@vue/test-utils'

const wrapper = shallowMount(YourComp, {
  stubs: {
    RouterLink: RouterLinkStub
  }
})

expect(wrapper.findComponent(RouterLinkStub).props('to')).toEqual({
  name: RouterNames.GAME
})

К моему удивлению, тест провалился, утверждая, что полученное значение свойства :to в RouterLinkStub было не { name: RouterNames.GAME }, а 'game', что я и передал ButtonRouterLink. Как будто наш компонент никогда не выполнял преобразование из value в { name: value }.

Довольно странно.
Оказывается, проблема в том, что <RouterLink /> является корневым элементом нашего компонента. Это означает, что компонент (<BottomRouterLink>) заменялся в DOM на <RouterLinkStub>, но значение :to читалось из первого, а не из второго. Короче говоря, тест сработал бы, если бы шаблон был:

  <span>
    <RouterLink ... />
  </span>

Чтобы обойти это, нам нужно импортировать фактический компонент RouterLink (из vue-router) и найти его вместо поиска RouterLinkStub. Почему? @vue/test-utils достаточно умен, чтобы найти компонент-заглушку вместо фактического, но таким образом .findComponent() будет соответствовать элементу только после замены, а не тогда, когда он все еще был ButtonRouterLink (необработанным). В этот момент значение :to было преобразовано в то, что на самом деле получает 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
    })
  })
})

Сложный.

Спасибо за такой исчерпывающий ответ, особенно совет со снимками против console.info(), полезно понять, что на самом деле отображается. Кроме того, я вижу, что нет особого смысла тестировать триггер кнопки HomeView, так как я не тестирую там никаких пользовательских поведений. Я попытался протестировать ButtonRouterLink, как вы предложили, я использовал mount для компонента, передал имя тестового маршрута в качестве параметра и сделал снимок, чтобы увидеть, что отображается, но он отобразил пустую строку «», сопровождаемую довольно бесполезным предупреждением Vue (обновленный вопрос с тестом и предупреждающим сообщением).

h0nter 15.04.2023 18:07

@h0nter, мне пришлось раскручивать проект пару часов, пока я не докопался до сути, просмотрев документы и заставив консоль истекать кровью. Смотрите обновление к ответу.

tao 16.04.2023 00:03

@Pipetus, ты считаешь, что я был бы лучше, если бы не давал тебе совет? Потому что я верю в обратное. Основное различие между нашими определениями приятности заключается в том, что вы рассматриваете чувства человека в очень коротком временном масштабе, тогда как я имею в виду более длительный период времени. Забавно то, что вас запоминают по тому, что люди чувствовали в данный момент, а не по тому, как вы заставляете их чувствовать себя в будущем, если вам удастся изменить их к лучшему. Но я думаю, это моя проблема. Спасибо за то, что был добр и честен со мной. Удачного кодирования!

tao 16.04.2023 13:29

Это действительно впечатляющая работа по отладке, большое спасибо за вашу помощь в этом. Теперь все понятно, почему он не будет правильно соответствовать объекту, поэтому он отвечает на мой вопрос. В вашем ответе есть две вещи, которые я хотел проверить: во-первых, упаковка ButtonRouterLink в <span>, похоже, не имеет никакого эффекта, поскольку ошибки теста: Error: Cannot call props on an empty VueWrapper. поскольку wrapper.findComponent(RouterLinkStub) является нулевым объектом. Во-вторых, в вашем окончательном ответе, я думаю, нам не нужно указывать заглушку RouterLink, так как smallMount обработает ее автоматически.

h0nter 16.04.2023 15:22

Я удивлен, что это не работает, когда это не корневой элемент, это не имеет смысла. RouterLinkStub никогда не должен иметь :to равное строковому значению, он всегда должен быть объектом, за исключением случаев, когда он заменяет элемент строкой :to. Возможно, я посмотрю на это позже, но точно не сегодня.

tao 17.04.2023 12:09

Спасибо и не волнуйтесь, это больше не проблема, так как вы ответили на мой вопрос, и у меня есть рабочий тест. Теперь мне просто любопытно, почему это не работает последовательно.

h0nter 17.04.2023 14:49

Другие вопросы по теме

В чем разница между распараллеливанием через шардинг и воркерами в Jest?
React + Jest: тест зависает навсегда всякий раз, когда я пытаюсь получить доступ к свойствам DOM (например, toHaveStyle)
Инспектор макетов не работает при запуске приложения Android в режиме отладки
Тестирование Laravel assertJsonMissing не работает только для ключа. почему?
Bash: итеративно монтировать первый диск, том или раздел, соответствующие схеме именования, игнорируя второе и последующие совпадения
Как проверить один столбец, значения которого зависят от другого для DBT
Swift XCTest - Как проверить свойства tabItems в TabView?
Не удалось инициализировать встроенный макет Byte Buddy. Проблема при использовании встроенного mockito
Jest-тестирование службы, использующей Neo4J и MongoDb, возвращает ошибку: «TypeError: невозможно прочитать свойства неопределенного (чтение« inspectModules »)»
Как написать фиктивный тест для метода с регистратором?