Тестирование анимаций и переходов

Тестирование анимаций и переходов в Vue-компонентах требует понимания не только API Vue Test Utils, но и внутреннего механизма работы Vue с transition и transition-group. Важной особенностью является то, что настоящие анимации в DOM зачастую не нужны для тестирования логики компонента — достаточно проверки правильного изменения классов и состояния.


Работа с компонентом <transition>

Компонент <transition> оборачивает элементы и добавляет специальные CSS-классы в зависимости от состояния анимации:

  • v-enter-from / v-enter-to
  • v-leave-from / v-leave-to
  • v-enter-active / v-leave-active

Для тестов важно проверять именно эти классы и их последовательность, а не саму анимацию.

Пример:

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

test('должен применять классы перехода при монтировании', async () => {
  const wrapper = mount(Fade, { props: { show: true } })
  
  const el = wrapper.find('div')
  
  expect(el.classes()).toContain('v-enter-from')
  
  await wrapper.vm.$nextTick()
  
  expect(el.classes()).toContain('v-enter-to')
  expect(el.classes()).toContain('v-enter-active')
})

В этом примере проверяется, что при монтировании компонент получает правильные классы для анимации появления.


Тестирование событий анимации

Vue автоматически эмитирует события before-enter, enter, after-enter, before-leave, leave, after-leave. Их можно использовать для проверки логики компонента:

test('вызывает колбэки при завершении анимации', async () => {
  const beforeEnter = jest.fn()
  const afterEnter = jest.fn()

  mount(Fade, {
    props: { show: true },
    attrs: {
      onBeforeEnter: beforeEnter,
      onAfterEnter: afterEnter
    }
  })

  expect(beforeEnter).toHaveBeenCalled()
  
  await new Promise(resolve => setTimeout(resolve, 0)) // эмуляция завершения анимации
  expect(afterEnter).toHaveBeenCalled()
})

Даже без реальных анимаций можно проверять последовательность вызова этих событий и корректность работы логики, связанной с переходами.


Игнорирование анимации с stub

Vue Test Utils позволяет заменять <transition> на простой stub, чтобы тесты выполнялись быстрее и без ожидания реальных анимаций:

const wrapper = mount(Fade, {
  global: {
    stubs: {
      transition: true
    }
  }
})

expect(wrapper.html()).toContain('<div>Контент</div>')

Такой подход полезен, когда важна проверка рендеринга и условной логики, а не визуальные эффекты.


Тестирование <transition-group>

Компонент <transition-group> используется для анимации списков. Основная задача теста — проверить правильное присвоение классов элементам при добавлении и удалении:

import ListWithTransition from '@/components/ListWithTransition.vue'

test('добавление и удаление элементов с правильными классами', async () => {
  const wrapper = mount(ListWithTransition, {
    props: { items: ['A', 'B'] }
  })

  wrapper.vm.items.push('C')
  await wrapper.vm.$nextTick()
  
  const items = wrapper.findAll('li')
  expect(items[2].classes()).toContain('v-enter-from')
  
  wrapper.vm.items.pop()
  await wrapper.vm.$nextTick()
  
  expect(items[1].classes()).toContain('v-leave-to')
})

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


Проверка условного рендеринга с анимацией

Часто <transition> используется вместе с v-if. В таких случаях тест проверяет, что элемент не появляется сразу, а проходит через правильные классы:

const wrapper = mount({
  template: `
    <transition name="fade">
      <div v-if="visible">Привет</div>
    </transition>
  `,
  data() { return { visible: false } }
})

expect(wrapper.html()).toBe('')
wrapper.setData({ visible: true })
await wrapper.vm.$nextTick()
expect(wrapper.find('div').classes()).toContain('fade-enter-from')

Это гарантирует, что анимация запускается именно при изменении состояния, а не при инициализации компонента.


Проверка логики с использованием моков

Иногда анимации влияют на выполнение колбэков после завершения. Для таких сценариев полезно мокировать requestAnimationFrame или таймеры с jest.useFakeTimers():

jest.useFakeTimers()
const wrapper = mount(Fade, { props: { show: true } })

jest.advanceTimersByTime(500) // эмуляция длительности анимации
expect(wrapper.emitted('after-enter')).toBeTruthy()

Этот подход позволяет контролировать длительность анимации и проверять вызовы событий без реального ожидания времени.


Рекомендации при тестировании анимаций

  • Основная цель теста — корректность присвоения классов и вызов событий, а не визуальное отображение.
  • Использовать stub для ускорения тестов и упрощения логики.
  • При необходимости эмулировать завершение анимации через таймеры или $nextTick.
  • Тестировать как одиночные <transition>, так и <transition-group> для списков.
  • Проверять взаимодействие анимации с условным рендерингом и колбэками.

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