Unit тесты

Библиотека Interact.js широко применяется для реализации интерактивных интерфейсов: перетаскивания элементов, изменения размеров, жестов, ограничений перемещения и инерции. Большая часть логики в таких приложениях находится в обработчиках событий — dragmove, resizemove, gesturemove и других.

Unit-тестирование позволяет изолированно проверять:

  • корректность вычислений координат
  • логику изменения состояния интерфейса
  • работу ограничений перемещения
  • реакцию на события Interact.js
  • корректность обновления DOM-состояния

Главная сложность заключается в том, что Interact.js активно использует DOM-события и пользовательские взаимодействия, поэтому тесты должны уметь имитировать эти события.


Инструменты для unit-тестирования

Чаще всего для тестирования JavaScript-кода используются следующие инструменты:

Инструмент Назначение
Jest фреймворк тестирования
Mocha среда выполнения тестов
Chai библиотека утверждений
Sinon создание моков и шпионов
jsdom эмуляция DOM в Node.js

В большинстве проектов применяется связка:

Jest + jsdom

Поскольку Jest уже включает jsdom по умолчанию.


Структура тестируемого кода

Для удобного тестирования логика Interact.js обычно выносится из обработчиков событий в отдельные функции.

Неправильный подход

interact('.box').draggable({
  listeners: {
    move(event) {
      const target = event.target

      const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
      const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy

      target.style.transform = `translate(${x}px, ${y}px)`
      target.setAttribute('data-x', x)
      target.setAttribute('data-y', y)
    }
  }
})

Такая логика практически не тестируется.


Правильный подход

Выделение чистой функции.

export function calculatePosition(x, y, dx, dy) {
  return {
    x: x + dx,
    y: y + dy
  }
}

Обработчик Interact.js использует эту функцию:

import { calculatePosition } fr om './position'

function dragMoveListener(event) {
  const target = event.target

  const x = parseFloat(target.getAttribute('data-x')) || 0
  const y = parseFloat(target.getAttribute('data-y')) || 0

  const result = calculatePosition(x, y, event.dx, event.dy)

  target.style.transform =
    `translate(${result.x}px, ${result.y}px)`

  target.setAttribute('data-x', result.x)
  target.setAttribute('data-y', result.y)
}

Теперь логика перемещения тестируется отдельно.


Тестирование вычислений координат

Пример unit-теста с использованием Jest.

import { calculatePosition } fr om '../position'

test('корректно рассчитывает новую позицию', () => {
  const result = calculatePosition(10, 20, 5, -10)

  expect(result).toEqual({
    x: 15,
    y: 10
  })
})

Проверяется:

  • сложение координат
  • корректность отрицательных значений
  • отсутствие побочных эффектов

Тестирование обработчиков Interact.js

Обработчик dragMoveListener работает с DOM-элементом. Для тестирования используется jsdom.

Подготовка DOM

document.body.innerHTML = `
  <div class="box" data-x="0" data-y="0"></div>
`

const element = document.querySelector('.box')

Имитация события drag

Interact.js передаёт объект события со свойствами:

  • target
  • dx
  • dy

В тесте создаётся аналогичный объект.

import { dragMoveListener } from '../drag'

test('обновляет позицию элемента', () => {

  const event = {
    target: element,
    dx: 10,
    dy: 5
  }

  dragMoveListener(event)

  expect(element.getAttribute('data-x')).toBe('10')
  expect(element.getAttribute('data-y')).toBe('5')
})

Проверка обновления transform

Важно проверять, что стиль элемента изменяется корректно.

test('обновляет CSS transform', () => {

  const event = {
    target: element,
    dx: 20,
    dy: 15
  }

  dragMoveListener(event)

  expect(element.style.transform)
    .toBe('translate(20px, 15px)')
})

Тестирование ограничений перемещения

Interact.js часто используется с ограничениями (restrict).

Пример функции ограничения:

export function restrictPosition(x, y, maxX, maxY) {
  return {
    x: Math.max(0, Math.min(x, maxX)),
    y: Math.max(0, Math.min(y, maxY))
  }
}

Unit-тесты ограничений

import { restrictPosition } from '../restrict'

test('не позволяет выйти за левую границу', () => {
  const result = restrictPosition(-10, 20, 500, 500)

  expect(result.x).toBe(0)
})

test('не позволяет выйти за правую границу', () => {
  const result = restrictPosition(600, 20, 500, 500)

  expect(result.x).toBe(500)
})

Тестирование resize-логики

При изменении размеров элементы обычно используют координаты:

  • width
  • height

Пример функции:

export function resizeBox(width, height, deltaWidth, deltaHeight) {
  return {
    width: width + deltaWidth,
    height: height + deltaHeight
  }
}

Тест resize

import { resizeBox } from '../resize'

test('увеличивает размеры элемента', () => {
  const result = resizeBox(100, 200, 20, 10)

  expect(result).toEqual({
    width: 120,
    height: 210
  })
})

Тестирование логики snap-сеток

Interact.js поддерживает привязку к сетке (snap). Часто используется собственная функция округления.

export function snapToGrid(value, gridSize) {
  return Math.round(value / gridSize) * gridSize
}

Unit-тест snap

import { snapToGrid } from '../grid'

test('привязывает координату к сетке', () => {
  const result = snapToGrid(23, 10)

  expect(result).toBe(20)
})

Тестирование состояния drag-операции

Во многих интерфейсах хранится состояние:

isDragging
startX
startY
currentX
currentY

Пример reducer-подобной функции:

export function startDrag(state, x, y) {
  return {
    ...state,
    isDragging: true,
    startX: x,
    startY: y
  }
}

Тест состояния

import { startDrag } from '../dragState'

test('начинает операцию перетаскивания', () => {

  const state = {
    isDragging: false
  }

  const result = startDrag(state, 100, 200)

  expect(result.isDragging).toBe(true)
  expect(result.startX).toBe(100)
  expect(result.startY).toBe(200)
})

Использование моков

Если код вызывает сторонние функции, используется mock.

Пример:

export function updateUI(element, x, y, logger) {
  element.style.transform = `translate(${x}px, ${y}px)`
  logger('position updated')
}

Тест с моком

test('вызывает логгер', () => {

  const logger = jest.fn()

  updateUI(element, 10, 20, logger)

  expect(logger).toHaveBeenCalled()
})

Проверяется:

  • факт вызова
  • количество вызовов
  • аргументы

Тестирование последовательности событий

Интерактивные элементы часто проходят цепочку событий:

dragstart
dragmove
dragend

Пример теста:

test('полный цикл drag', () => {

  dragStart(eventStart)
  dragMove(eventMove)
  dragEnd(eventEnd)

  expect(state.isDragging).toBe(false)
})

Тестирование debounce и throttle

Иногда обработчики Interact.js оптимизируются.

Пример throttle:

export function throttle(fn, lim it) {
  let waiting = false

  return function (...args) {
    if (!waiting) {
      fn.apply(this, args)
      waiting = true

      setTimeout(() => {
        waiting = false
      }, lim it)
    }
  }
}

Тест throttle

jest.useFakeTimers()

test('ограничивает частоту вызовов', () => {

  const fn = jest.fn()
  const throttled = throttle(fn, 100)

  throttled()
  throttled()

  expect(fn).toHaveBeenCalledTimes(1)
})

Покрытие тестами

Важный показатель качества тестов — test coverage.

Проверяются:

  • строки кода
  • ветвления
  • функции
  • выражения

Запуск покрытия в Jest:

npm test -- --coverage

Отчёт показывает:

Statements   92%
Branches     85%
Functions    90%
Lines        91%

Организация структуры тестов

Типичная структура проекта:

src/
  interact/
    drag.js
    resize.js
    grid.js

tests/
  drag.test.js
  resize.test.js
  grid.test.js

Иногда используется совместное размещение:

drag.js
drag.test.js

Практики написания устойчивых тестов

1. Изоляция логики

Вся математика и вычисления должны находиться в отдельных функциях.


2. Минимизация DOM-зависимостей

DOM-операции тестируются отдельно от бизнес-логики.


3. Предсказуемость тестов

Тесты не должны зависеть от:

  • времени
  • сети
  • внешнего состояния

4. Малые независимые тесты

Каждый тест проверяет одну конкретную функцию или сценарий.


5. Чистые функции

Функции без побочных эффектов тестируются значительно проще:

input → function → output

Интеграция тестов в CI

Unit-тесты запускаются автоматически:

  • при каждом коммите
  • при pull request
  • перед деплоем

Пример для GitHub Actions:

name: tests

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - run: npm install
      - run: npm test

Проверка стабильности интерактивных компонентов

В проектах с Interact.js unit-тесты позволяют гарантировать корректность:

  • перемещения элементов
  • ограничений и привязок
  • вычислений координат
  • resize-операций
  • управления состоянием drag-процессов

Даже при изменении интерфейсной части математическая и логическая основа поведения элементов остаётся надёжно защищённой тестами.