Библиотека Interact.js широко применяется для
реализации интерактивных интерфейсов: перетаскивания элементов,
изменения размеров, жестов, ограничений перемещения и инерции. Большая
часть логики в таких приложениях находится в обработчиках событий —
dragmove, resizemove, gesturemove
и других.
Unit-тестирование позволяет изолированно проверять:
Главная сложность заключается в том, что Interact.js активно использует DOM-события и пользовательские взаимодействия, поэтому тесты должны уметь имитировать эти события.
Чаще всего для тестирования 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
})
})
Проверяется:
Обработчик dragMoveListener работает с DOM-элементом.
Для тестирования используется jsdom.
document.body.innerHTML = `
<div class="box" data-x="0" data-y="0"></div>
`
const element = document.querySelector('.box')
Interact.js передаёт объект события со свойствами:
targetdxdyВ тесте создаётся аналогичный объект.
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')
})
Важно проверять, что стиль элемента изменяется корректно.
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))
}
}
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)
})
При изменении размеров элементы обычно используют координаты:
widthheightПример функции:
export function resizeBox(width, height, deltaWidth, deltaHeight) {
return {
width: width + deltaWidth,
height: height + deltaHeight
}
}
import { resizeBox } from '../resize'
test('увеличивает размеры элемента', () => {
const result = resizeBox(100, 200, 20, 10)
expect(result).toEqual({
width: 120,
height: 210
})
})
Interact.js поддерживает привязку к сетке (snap). Часто используется собственная функция округления.
export function snapToGrid(value, gridSize) {
return Math.round(value / gridSize) * gridSize
}
import { snapToGrid } from '../grid'
test('привязывает координату к сетке', () => {
const result = snapToGrid(23, 10)
expect(result).toBe(20)
})
Во многих интерфейсах хранится состояние:
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)
})
Иногда обработчики 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)
}
}
}
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
Вся математика и вычисления должны находиться в отдельных функциях.
DOM-операции тестируются отдельно от бизнес-логики.
Тесты не должны зависеть от:
Каждый тест проверяет одну конкретную функцию или сценарий.
Функции без побочных эффектов тестируются значительно проще:
input → function → output
Unit-тесты запускаются автоматически:
Пример для 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-тесты позволяют гарантировать корректность:
Даже при изменении интерфейсной части математическая и логическая основа поведения элементов остаётся надёжно защищённой тестами.