Хуки директив (mounted, updated, unmounted)

Пользовательские директивы во Vue имеют собственный набор хуков жизненного цикла. В контексте Vue 3 и Vue Test Utils (VTU v2) ключевыми являются:

  • mounted — вызывается после того, как элемент вставлен в DOM.
  • updated — вызывается после обновления компонента и применения изменений к элементу.
  • unmounted — вызывается перед удалением элемента из DOM.

Каждый хук получает одинаковую сигнатуру аргументов и играет отдельную роль при тестировании поведения, связанного с 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

Хук 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()
})

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

  • Проверяется побочный эффект, а не сам вызов хука напрямую.
  • Используется spy на метод DOM-элемента.
  • mounted вызывается автоматически при mount.

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

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.
  • Проверяется состояние DOM после обновления.

Проверка binding.oldValue

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

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 после каждого шага.


Общие принципы тестирования хуков директив

  • Хуки не тестируются напрямую — проверяется их эффект.
  • Используются реальные DOM-операции, доступные в jsdom.
  • Spy и mock применяются для внешних API и глобальных объектов.
  • updated всегда требует ожидания асинхронного обновления.
  • unmounted проверяется через wrapper.unmount() или условное удаление элемента.

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