Директивы во Vue предназначены для прямого воздействия на DOM и окружающую среду компонента. В отличие от компонентов, они не управляют шаблоном целиком, а внедряются в уже существующую разметку, что почти всегда приводит к побочным эффектам: добавлению обработчиков событий, изменению стилей, работе с таймерами, наблюдателями, внешними API браузера.
Побочный эффект в контексте директивы — любое действие, выходящее за рамки чистого преобразования входных данных в выходные. Проверка таких эффектов требует тестирования не только результата, но и самого факта взаимодействия с внешней средой.
Во Vue 3 пользовательская директива описывается набором хуков:
Побочные эффекты чаще всего возникают в mounted и должны
быть корректно устранены в unmounted. Тестирование строится
вокруг проверки этих двух фаз.
const focusDirective = {
mounted(el) {
el.focus()
}
}
В данном примере побочный эффект — вызов метода focus у
DOM-элемента.
Vue Test Utils предоставляет механизм глобальной регистрации директив
через опцию global.directives.
import { mount } from '@vue/test-utils'
const wrapper = mount(Component, {
global: {
directives: {
focus: focusDirective
}
}
})
Это позволяет тестировать компонент в условиях, максимально близких к реальному приложению, не модифицируя сам компонент.
Наиболее простой тип побочного эффекта — изменение состояния DOM-элемента.
const colorDirective = {
mounted(el, binding) {
el.style.color = binding.value
}
}
Тест проверяет конечное состояние DOM:
const wrapper = mount({
template: `<p v-color="'red'">text</p>`
}, {
global: {
directives: {
color: colorDirective
}
}
})
expect(wrapper.find('p').element.style.color).toBe('red')
Ключевой момент — проверка результата, а не внутренней реализации директивы.
Если директива вызывает методы DOM API или сторонние функции, используется подмена (spy).
const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus')
mount({
template: `<input v-focus />`
}, {
global: {
directives: {
focus: focusDirective
}
}
})
expect(focusSpy).toHaveBeenCalled()
Такой подход позволяет зафиксировать сам факт побочного эффекта, не полагаясь на особенности среды выполнения тестов.
Директивы часто добавляют слушатели событий, что требует проверки двух аспектов:
const clickOutside = {
mounted(el, binding) {
el.__handler__ = event => {
if (!el.contains(event.target)) {
binding.value(event)
}
}
document.addEventListener('click', el.__handler__)
},
unmounted(el) {
document.removeEventListener('click', el.__handler__)
}
}
const addSpy = vi.spyOn(document, 'addEventListener')
mount({
template: `<div v-click-outside="onClick"></div>`,
methods: {
onClick() {}
}
}, {
global: {
directives: {
clickOutside
}
}
})
expect(addSpy).toHaveBeenCalledWith('click', expect.any(Function))
const removeSpy = vi.spyOn(document, 'removeEventListener')
const wrapper = mount(...)
wrapper.unmount()
expect(removeSpy).toHaveBeenCalledWith('click', expect.any(Function))
Здесь проверяется корректное управление жизненным циклом побочного эффекта.
Если директива использует setTimeout или
setInterval, необходимо управлять временем выполнения.
vi.useFakeTimers()
const delayDirective = {
mounted(el) {
setTimeout(() => {
el.textContent = 'done'
}, 1000)
}
}
Тест:
const wrapper = mount({
template: `<div v-delay></div>`
}, {
global: {
directives: {
delay: delayDirective
}
}
})
vi.advanceTimersByTime(1000)
expect(wrapper.text()).toBe('done')
Использование поддельных таймеров позволяет изолировать тест от реального времени и сделать его детерминированным.
Директивы могут работать с IntersectionObserver,
ResizeObserver, window,
localStorage.
Для таких случаев применяется подмена глобальных объектов.
const observeMock = vi.fn()
window.IntersectionObserver = vi.fn(() => ({
observe: observeMock,
disconnect: vi.fn()
}))
После этого проверяется факт вызова observe или
disconnect в соответствующих хуках директивы.
Если директива реагирует на изменение значения, используется хук
updated.
const textDirective = {
updated(el, binding) {
el.textContent = binding.value
}
}
Тест:
const wrapper = mount({
template: `<div v-text="msg"></div>`,
data() {
return { msg: 'one' }
}
}, {
global: {
directives: {
text: textDirective
}
}
})
expect(wrapper.text()).toBe('one')
wrapper.vm.msg = 'two'
await wrapper.vm.$nextTick()
expect(wrapper.text()).toBe('two')
Здесь побочный эффект — повторное изменение DOM при обновлении данных.
При тестировании компонентов директивы могут быть источником нежелательных побочных эффектов. Vue Test Utils позволяет заменять реальные директивы заглушками.
const stubDirective = {
mounted() {}
}
Это снижает связанность тестов и позволяет проверять логику компонента независимо от поведения директивы.
Корректное тестирование побочных эффектов директив строится вокруг жизненного цикла, явных наблюдаемых изменений и строгого контроля внешней среды.