Тестирование встроенных директив

Встроенные директивы Vue (v-if, v-show, v-for, v-model, v-bind, v-on, v-slot и другие) являются фундаментальной частью декларативного шаблонного синтаксиса. При тестировании компонентов через Vue Test Utils они не тестируются напрямую как отдельные сущности, а проверяются через их влияние на DOM, состояние компонента и пользовательское поведение.

Ключевой принцип: в тестах проверяется результат работы директивы, а не факт её использования.


Тестирование v-if

Директива v-if управляет условным рендерингом и физически добавляет или удаляет элементы из DOM.

Компонент:

<template>
  <div>
    <p v-if="visible">Текст</p>
  </div>
</template>

<script>
export default {
  data() {
    return { visible: false }
  }
}
</script>

Тест:

import { mount } from '@vue/test-utils'
import Component from '@/Component.vue'

test('v-if скрывает и показывает элемент', async () => {
  const wrapper = mount(Component)

  expect(wrapper.find('p').exists()).toBe(false)

  await wrapper.setData({ visible: true })

  expect(wrapper.find('p').exists()).toBe(true)
})

Ключевые моменты:

  • exists() — основной метод для проверки v-if
  • асинхронность обязательна (await)
  • отсутствие элемента в DOM — нормальное состояние

Тестирование v-show

v-show не удаляет элемент из DOM, а управляет CSS-свойством display.

Компонент:

<p v-show="visible">Текст</p>

Тест:

test('v-show управляет display', async () => {
  const wrapper = mount(Component)

  const p = wrapper.find('p')
  expect(p.exists()).toBe(true)
  expect(p.isVisible()).toBe(false)

  await wrapper.setData({ visible: true })

  expect(p.isVisible()).toBe(true)
})

Отличие от v-if:

  • элемент всегда существует
  • проверка через isVisible()
  • удобно для анимаций и частых переключений

Тестирование v-for

v-for генерирует списки элементов на основе массива или объекта.

Компонент:

<ul>
  <li v-for="item in items" :key="item.id">
    {{ item.name }}
  </li>
</ul>

Тест:

test('v-for рендерит список', () => {
  const wrapper = mount(Component, {
    data() {
      return {
        items: [
          { id: 1, name: 'A' },
          { id: 2, name: 'B' }
        ]
      }
    }
  })

  const items = wrapper.findAll('li')
  expect(items).toHaveLength(2)
  expect(items[0].text()).toBe('A')
  expect(items[1].text()).toBe('B')
})

Практика:

  • использовать findAll
  • проверять количество и содержимое
  • key не тестируется напрямую

Тестирование v-bind

v-bind динамически связывает атрибуты, классы и стили.

Компонент:

<button :disabled="isDisabled">Кнопка</button>

Тест:

test('v-bind устанавливает атрибуты', async () => {
  const wrapper = mount(Component, {
    data() {
      return { isDisabled: false }
    }
  })

  const button = wrapper.find('button')
  expect(button.attributes('disabled')).toBeUndefined()

  await wrapper.setData({ isDisabled: true })

  expect(button.attributes('disabled')).toBeDefined()
})

Для классов:

<div :class="{ active: isActive }"></div>
expect(wrapper.find('div').classes()).not.toContain('active')

Тестирование v-on

v-on связывает события с методами компонента.

Компонент:

<button @click="increment">+</button>

Тест:

test('v-on вызывает метод', async () => {
  const wrapper = mount(Component)

  await wrapper.find('button').trigger('click')

  expect(wrapper.vm.count).toBe(1)
})

Подходы:

  • проверка изменения состояния
  • проверка эмиссии событий
  • spy на метод (реже, при необходимости)

Тестирование v-model

v-model — синтаксический сахар для :value + @input (или других событий).

Компонент:

<input v-model="text" />

Тест:

test('v-model обновляет data', async () => {
  const wrapper = mount(Component)

  const input = wrapper.find('input')
  await input.setValue('hello')

  expect(wrapper.vm.text).toBe('hello')
})

Обратная проверка:

await wrapper.setData({ text: 'world' })
expect(input.element.value).toBe('world')

Важно:

  • использовать setValue, а не trigger('input')
  • Vue Test Utils корректно эмулирует v-model

Тестирование v-slot

Слоты — механизм передачи разметки в дочерние компоненты.

Компонент:

<slot name="header"></slot>

Тест:

mount(Component, {
  slots: {
    header: '<h1>Заголовок</h1>'
  }
})

Проверка:

expect(wrapper.find('h1').text()).toBe('Заголовок')

Для scoped slots:

slots: {
  default: ({ value }) => `<span>${value}</span>`
}

Пользовательские директивы и отличие от встроенных

Встроенные директивы:

  • не имеют lifecycle-хуков, доступных в тестах
  • тестируются исключительно через DOM

Пользовательские директивы:

  • могут тестироваться через global.directives
  • проверяются через side effects

Встроенные директивы никогда не мокаются и не подменяются.


Асинхронность и обновление DOM

Все директивы, влияющие на DOM, работают в рамках реактивного цикла Vue.

Обязательные правила:

  • использовать await
  • не обращаться к DOM сразу после изменения состояния
  • понимать, что setData, setProps, trigger — асинхронны

Пример ошибки:

wrapper.setData({ visible: true })
expect(wrapper.find('p').exists()).toBe(true) // ❌

Корректно:

await wrapper.setData({ visible: true })

Общие принципы тестирования директив

Правильный фокус:

  • состояние DOM
  • видимость элементов
  • атрибуты и классы
  • пользовательские эффекты

Неправильный фокус:

  • проверка наличия v-if в шаблоне
  • тестирование реализации вместо поведения
  • жёсткая привязка к разметке

Встроенные директивы Vue не являются объектами тестирования — они являются механизмами, корректность которых подтверждается через наблюдаемое поведение компонента.