Динамическое добавление/удаление элементов

Работа с динамическим контентом — одна из ключевых задач при тестировании компонентов Vue. Динамическое добавление и удаление элементов часто связано с интерактивными списками, формами, таблицами и другими компонентами, где состояние интерфейса изменяется во время работы приложения. Vue Test Utils предоставляет мощные инструменты для имитации таких изменений и проверки корректности рендеринга и поведения компонентов.


Монтирование компонента и базовые методы

Для начала необходимо смонтировать компонент с помощью функции mount или shallowMount из Vue Test Utils:

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

const wrapper = mount(DynamicList)

Основные методы для работы с элементами внутри компонента:

  • wrapper.find(selector) — возвращает первый найденный элемент по селектору.
  • wrapper.findAll(selector) — возвращает массив всех найденных элементов.
  • wrapper.setData({ key: value }) — позволяет изменить реактивные данные компонента.
  • wrapper.vm.$emit(event, payload) — эмулирует событие.

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


Проверка добавления элементов

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

Пример компонента:

<template>
  <div>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
    <button @click="addItem">Добавить элемент</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    }
  },
  methods: {
    addItem() {
      this.items.push(`Элемент ${this.items.length + 1}`)
    }
  }
}
</script>

Тест для проверки добавления элементов:

test('добавление элемента в список', async () => {
  const wrapper = mount(DynamicList)
  
  expect(wrapper.findAll('li').length).toBe(0)
  
  await wrapper.find('button').trigger('click')
  
  expect(wrapper.findAll('li').length).toBe(1)
  expect(wrapper.find('li').text()).toBe('Элемент 1')
})

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

  • Использование await при trigger('click') важно для корректного ожидания обновления DOM.
  • wrapper.findAll('li') позволяет проверять текущее состояние списка после добавления.

Проверка удаления элементов

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

Пример расширенного компонента:

<template>
  <div>
    <ul>
      <li v-for="(item, index) in items" :key="item">
        {{ item }}
        <button class="remove" @click="removeItem(index)">Удалить</button>
      </li>
    </ul>
    <button @click="addItem">Добавить элемент</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: []
    }
  },
  methods: {
    addItem() {
      this.items.push(`Элемент ${this.items.length + 1}`)
    },
    removeItem(index) {
      this.items.splice(index, 1)
    }
  }
}
</script>

Тест для удаления элементов:

test('удаление элемента из списка', async () => {
  const wrapper = mount(DynamicList)
  
  await wrapper.find('button').trigger('click') // добавляем элемент
  await wrapper.find('button').trigger('click') // добавляем второй элемент
  
  expect(wrapper.findAll('li').length).toBe(2)
  
  await wrapper.findAll('.remove')[0].trigger('click')
  
  const items = wrapper.findAll('li')
  expect(items.length).toBe(1)
  expect(items[0].text()).toContain('Элемент 2')
})

Особенности тестирования удаления:

  • v-for с ключами позволяет Vue правильно обновлять DOM при удалении.
  • Проверка через .toContain или .text() позволяет убедиться, что удалён правильный элемент.

Асинхронные изменения и nextTick

При динамических операциях иногда используются асинхронные методы или эффекты (например, при работе с API). В таких случаях требуется использовать await Vue.nextTick() или встроенные возможности await wrapper.vm.$nextTick() для ожидания обновления DOM перед проверкой состояния элементов:

import { nextTick } from 'vue'

test('добавление элемента с nextTick', async () => {
  const wrapper = mount(DynamicList)
  
  wrapper.vm.addItem()
  await nextTick()
  
  expect(wrapper.findAll('li').length).toBe(1)
})

Проверка событий и пользовательских действий

Для динамических списков важно тестировать не только изменение состояния, но и правильность взаимодействия с пользователем:

  • Симуляция клика через trigger('click').
  • Проверка передачи параметров через @click или $emit.
  • Использование setValue для полей ввода перед добавлением элементов.

Пример добавления элемента через поле ввода:

<input v-model="newItem" />
<button @click="addItem">Добавить</button>

Тестирование:

await wrapper.find('input').setValue('Новый элемент')
await wrapper.find('button').trigger('click')

expect(wrapper.findAll('li')[0].text()).toBe('Новый элемент')

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

  1. Разделять тесты на добавление и удаление элементов.
  2. Проверять как состояние данных (wrapper.vm.items), так и отображение в DOM (wrapper.findAll('li')).
  3. Использовать v-for с уникальными ключами для избежания проблем с виртуальным DOM при динамических изменениях.
  4. При асинхронных операциях всегда применять await с nextTick или trigger.

Особенности тестирования списков с компонентами внутри

Если элементы списка — это отдельные дочерние компоненты, важно:

  • Использовать findComponent или findAllComponents для поиска дочерних компонентов.
  • Проверять правильную передачу props при динамическом добавлении.
  • Следить за удалением дочерних компонентов при изменении массива.

Пример:

expect(wrapper.findAllComponents(ItemComponent).length).toBe(3)

Такой подход гарантирует, что динамическое добавление и удаление элементов полностью контролируется как с точки зрения Vue, так и с точки зрения рендеринга в DOM.