v-model на компонентах

v-model является удобным способом связывать данные между родительским компонентом и дочерним компонентом. В Vue 3 синтаксис немного отличается от Vue 2, особенно при работе с кастомными компонентами, где v-model можно настраивать с помощью имени пропса и события. Тестирование таких компонентов требует понимания того, как v-model транслируется в пропсы и события, а также как это отражается на состоянии компонента.


Основы работы v-model на кастомных компонентах

Когда родительский компонент использует v-model для дочернего компонента, фактически происходит следующее:

  1. Пропс передается в дочерний компонент с именем modelValue (по умолчанию).
  2. Событие update:modelValue испускается дочерним компонентом для уведомления родителя об изменении значения.

Пример базового компонента:

// InputComponent.vue
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  props: {
    modelValue: String
  }
}
</script>

Использование в родительском компоненте:

<InputComponent v-model="text" />

Эквивалентно:

<InputComponent :modelValue="text" @update:modelValue="val => text = val" />

Тестирование v-model через Vue Test Utils

Vue Test Utils предоставляет методы mount и shallowMount, которые позволяют создавать экземпляры компонентов и взаимодействовать с ними. Для проверки работы v-model важно учитывать два аспекта:

  1. Передача начального значения через проп modelValue.
  2. Проверка эмита события update:modelValue при изменении внутреннего состояния компонента.

Пример теста для InputComponent:

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

test('v-model правильно передает и обновляет значение', async () => {
  const wrapper = mount(InputComponent, {
    props: {
      modelValue: 'initial'
    }
  })

  // Проверка передачи начального значения
  const input = wrapper.find('input')
  expect(input.element.value).toBe('initial')

  // Симуляция ввода нового значения
  await input.setValue('updated')

  // Проверка эмита события
  expect(wrapper.emitted('update:modelValue')).toBeTruthy()
  expect(wrapper.emitted('update:modelValue')[0]).toEqual(['updated'])
})

Ключевой момент: setValue автоматически триггерит событие input, которое в компоненте связано с $emit('update:modelValue').


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

Vue 3 позволяет переопределять имя пропса и события, используя аргументы v-model:

<CustomComponent v-model:title="pageTitle" />

Внутри CustomComponent:

props: {
  title: String
},
emits: ['update:title']

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

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

test('v-model с кастомным именем работает корректно', async () => {
  const wrapper = mount(CustomComponent, {
    props: { title: 'Hello' }
  })

  await wrapper.setProps({ title: 'World' })
  expect(wrapper.props('title')).toBe('World')

  wrapper.vm.$emit('update:title', 'Updated')
  expect(wrapper.emitted('update:title')[0]).toEqual(['Updated'])
})

Важно понимать, что при кастомном v-model проверка всегда должна учитывать именно правильное событие, а не универсальный update:modelValue.


Связывание v-model с составными компонентами

Для более сложных компонентов, например, с несколькими внутренними состояниями, тестирование v-model требует:

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

Пример с компонентом формы:

<template>
  <input :value="modelValue.firstName" @input="$emit('update:modelValue', { ...modelValue, firstName: $event.target.value })" />
  <input :value="modelValue.lastName" @input="$emit('update:modelValue', { ...modelValue, lastName: $event.target.value })" />
</template>

<script>
export default {
  props: {
    modelValue: Object
  }
}
</script>

Тест:

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

test('v-model с объектом обновляет свойства корректно', async () => {
  const wrapper = mount(NameForm, {
    props: { modelValue: { firstName: 'John', lastName: 'Doe' } }
  })

  const inputs = wrapper.findAll('input')
  await inputs[0].setValue('Jane')
  await inputs[1].setValue('Smith')

  expect(wrapper.emitted('update:modelValue')[0]).toEqual([{ firstName: 'Jane', lastName: 'Doe' }])
  expect(wrapper.emitted('update:modelValue')[1]).toEqual([{ firstName: 'Jane', lastName: 'Smith' }])
})

Такой подход позволяет тестировать v-model не только с примитивными значениями, но и с объектами и массивами.


Взаимодействие v-model с shallowMount

shallowMount полезен для изоляции компонента и игнорирования дочерних компонентов. В случае тестирования v-model это особенно важно:

  • Можно проверить только, что событие эмитится.
  • Не нужно рендерить внутренние структуры дочерних компонентов, экономя ресурсы.

Пример:

import { shallowMount } from '@vue/test-utils'
import InputComponent from '@/components/InputComponent.vue'

test('shallowMount проверяет эмит события', async () => {
  const wrapper = shallowMount(InputComponent, {
    props: { modelValue: 'test' }
  })

  await wrapper.find('input').setValue('new')
  expect(wrapper.emitted('update:modelValue')[0]).toEqual(['new'])
})

Практические советы

  • Для тестов не требуется полностью рендерить DOM, достаточно найти нужный элемент через find или findComponent.
  • Всегда использовать await при setValue или trigger('input'), чтобы обновления Vue успели примениться.
  • Проверка props полезна для подтверждения корректной передачи начального значения.
  • Эмит событий нужно проверять через wrapper.emitted(), чтобы убедиться, что компонент правильно уведомляет родителя об изменениях.

v-model на компонентах обеспечивает двустороннее связывание данных, и тестирование через Vue Test Utils строится на проверке правильной передачи пропсов и корректной эмиссии событий обновления. Для сложных компонентов важно симулировать пользовательские действия и проверять, что обновления данных происходят именно так, как это ожидается в реальном приложении.