Пользовательские директивы во Vue имеют собственный набор хуков жизненного цикла. В контексте Vue 3 и Vue Test Utils (VTU v2) ключевыми являются:
Каждый хук получает одинаковую сигнатуру аргументов и играет отдельную роль при тестировании поведения, связанного с DOM, побочными эффектами и очисткой ресурсов.
const myDirective = {
mounted(el, binding, vnode, prevVnode) {},
updated(el, binding, vnode, prevVnode) {},
unmounted(el, binding, vnode) {}
}
Ключевые параметры:
el — реальный DOM-элемент.
binding — объект с данными директивы:
value — текущее значение.oldValue — предыдущее значение (только в
updated).arg — аргумент директивы
(v-my-dir:arg).modifiers — модификаторы
(v-my-dir.mod).vnode, prevVnode — виртуальные ноды
(редко используются в тестах).
В Vue Test Utils директивы регистрируются через
global.directives:
import { mount } from '@vue/test-utils'
const wrapper = mount(Component, {
global: {
directives: {
focus: myDirective
}
}
})
Это позволяет изолированно тестировать компонент без глобальной регистрации в приложении.
Хук mounted часто используется для прямого
взаимодействия с DOM: фокус, измерения размеров, подписки на
события.
Пример директивы:
const focus = {
mounted(el) {
el.focus()
}
}
Тест:
import { mount } from '@vue/test-utils'
import { vi } from 'vitest'
test('mounted вызывает focus на элементе', () => {
const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus')
mount({
template: `<input v-focus />`
}, {
global: {
directives: { focus }
}
})
expect(focusSpy).toHaveBeenCalled()
})
Ключевые моменты:
mounted вызывается автоматически при
mount.updated срабатывает при изменении реактивных данных,
связанных с директивой.
Директива с реакцией на изменение значения:
const color = {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
}
Компонент:
const Component = {
template: `<p v-color="color">Text</p>`,
data() {
return { color: 'red' }
}
}
Тест:
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
test('updated обновляет стиль при смене значения', async () => {
const wrapper = mount(Component, {
global: {
directives: { color }
}
})
expect(wrapper.find('p').element.style.color).toBe('red')
await wrapper.setData({ color: 'blue' })
await nextTick()
expect(wrapper.find('p').element.style.color).toBe('blue')
})
Важно:
updated не вызывается при первом рендере.nextTick.oldValue доступен только в updated и
используется для сравнения состояний.
const logChange = {
updated(el, binding) {
if (binding.value !== binding.oldValue) {
el.dataset.changed = 'true'
}
}
}
Тест:
test('updated получает oldValue', async () => {
const wrapper = mount({
template: `<div v-log="val"></div>`,
data() {
return { val: 1 }
}
}, {
global: {
directives: { log: logChange }
}
})
await wrapper.setData({ val: 2 })
expect(wrapper.find('div').attributes('data-changed')).toBe('true')
})
Директива:
const padding = {
mounted(el, binding) {
if (binding.modifiers.large) {
el.style.padding = binding.arg === 'x' ? '0 20px' : '20px'
}
}
}
Шаблон:
<div v-padding:x.large></div>
Тест:
test('директива обрабатывает arg и modifiers', () => {
const wrapper = mount({
template: `<div v-padding:x.large></div>`
}, {
global: {
directives: { padding }
}
})
expect(wrapper.find('div').element.style.padding).toBe('0 20px')
})
unmounted используется для очистки ресурсов: таймеры,
обработчики событий, наблюдатели.
Директива с очисткой:
const resize = {
mounted(el) {
el._onResize = () => {}
window.addEventListener('resize', el._onResize)
},
unmounted(el) {
window.removeEventListener('resize', el._onResize)
}
}
Тест:
import { vi } from 'vitest'
test('unmounted очищает обработчики', () => {
const addSpy = vi.spyOn(window, 'addEventListener')
const removeSpy = vi.spyOn(window, 'removeEventListener')
const wrapper = mount({
template: `<div v-resize></div>`
}, {
global: {
directives: { resize }
}
})
expect(addSpy).toHaveBeenCalledWith('resize', expect.any(Function))
wrapper.unmount()
expect(removeSpy).toHaveBeenCalledWith('resize', expect.any(Function))
})
Особенности:
unmounted вызывается при
wrapper.unmount().При повторном mount компонента директива проходит полный
цикл заново. Это важно при тестировании сценариев с условным рендерингом
(v-if).
const toggle = {
mounted(el) {
el.dataset.mounted = 'true'
},
unmounted(el) {
el.dataset.unmounted = 'true'
}
}
Тесты в таких случаях строятся вокруг изменения состояния компонента и проверки DOM после каждого шага.
updated всегда требует ожидания асинхронного
обновления.unmounted проверяется через
wrapper.unmount() или условное удаление элемента.Такой подход позволяет надежно покрывать поведение директив и гарантировать корректную работу их жизненного цикла в изоляции от остальной логики приложения.