Интерактивные интерфейсы с перетаскиванием содержат большое количество событий, состояний и DOM-манипуляций. Ошибки в такой логике проявляются не сразу: некорректное положение элемента, пропущенное событие, неправильный порядок вызовов обработчиков.
Unit-тестирование позволяет изолированно проверять:
В библиотеке Shopify Draggable тестирование обычно выполняется в среде Jest либо Mocha с использованием виртуального DOM через JSDOM.
Unit-тесты сосредоточены на проверке отдельных компонентов: классов, функций и событийных обработчиков.
Экземпляр Draggable создаётся на основе контейнера и конфигурации. Базовый тест проверяет корректность создания объекта и привязки элементов.
import {Draggable} from '@shopify/draggable';
export function createDraggable(container) {
return new Draggable(container, {
draggable: '.item'
});
}
import {createDraggable} from './draggable';
test('Draggable instance should be created', () => {
document.body.innerHTML = `
<div id="container">
<div class="item"></div>
<div class="item"></div>
</div>
`;
const container = document.getElementById('container');
const draggable = createDraggable(container);
expect(draggable).toBeDefined();
});
Проверяется:
Draggable принимает множество параметров конфигурации. Unit-тест должен удостовериться, что параметры передаются корректно.
export function initSortable(container) {
return new Draggable.Sortable(container, {
draggable: '.card',
delay: 100
});
}
test('Sortable should receive configuration', () => {
document.body.innerHTML = `
<div id="list">
<div class="card"></div>
</div>
`;
const container = document.getElementById('list');
const sortable = initSortable(container);
expect(sortable.options.draggable).toBe('.card');
expect(sortable.options.delay).toBe(100);
});
Проверяется:
optionsОсновная логика библиотеки основана на системе событий. Проверка событий является ключевой частью unit-тестирования.
Основные события:
| Событие | Описание |
|---|---|
drag:start |
начало перетаскивания |
drag:move |
перемещение |
drag:stop |
завершение |
sortable:sorted |
изменение порядка |
export function attachStartHandler(draggable, callback) {
draggable.on('drag:start', callback);
}
test('drag:start event should trigger callback', () => {
const mockCallback = jest.fn();
document.body.innerHTML = `
<div id="container">
<div class="item"></div>
</div>
`;
const container = document.getElementById('container');
const draggable = new Draggable(container, {
draggable: '.item'
});
attachStartHandler(draggable, mockCallback);
draggable.trigger('drag:start');
expect(mockCallback).toHaveBeenCalled();
});
Проверяется:
Обычно библиотека используется внутри бизнес-логики приложения. Unit-тест должен проверять эту интеграцию.
export function trackOrder(draggable, store) {
draggable.on('sortable:sorted', (event) => {
store.update(event.oldIndex, event.newIndex);
});
}
test('store should update order', () => {
const store = {
update: jest.fn()
};
const draggable = {
on: (event, cb) => {
cb({
oldIndex: 0,
newIndex: 2
});
}
};
trackOrder(draggable, store);
expect(store.update).toHaveBeenCalledWith(0, 2);
});
Проверяется:
Draggable активно изменяет DOM: перемещает элементы, создаёт placeholder, добавляет классы.
Unit-тест должен фиксировать эти изменения.
export function markDragging(element) {
element.classList.add('dragging');
}
test('element should receive dragging class', () => {
const element = document.createElement('div');
markDragging(element);
expect(element.classList.contains('dragging')).toBe(true);
});
Draggable поддерживает расширения через систему плагинов.
export default class HighlightPlugin {
attach(draggable) {
draggable.on('drag:start', (event) => {
event.source.classList.add('highlight');
});
}
}
test('plugin should highlight element', () => {
const element = document.createElement('div');
const draggable = {
on: (event, cb) => {
cb({ source: element });
}
};
const plugin = new HighlightPlugin();
plugin.attach(draggable);
expect(element.classList.contains('highlight')).toBe(true);
});
Проверяется:
Многие компоненты Draggable сложно протестировать напрямую из-за зависимости от DOM-событий. Используются mock-объекты.
const draggableMock = {
on: jest.fn(),
off: jest.fn(),
trigger: jest.fn()
};
Mock позволяет:
Некоторые обработчики могут содержать асинхронный код.
export function saveOrder(draggable, api) {
draggable.on('sortable:sorted', async (event) => {
await api.save(event.newIndex);
});
}
test('API should be called', async () => {
const api = {
save: jest.fn().mockResolvedValue(true)
};
const draggable = {
on: (event, cb) => {
cb({ newIndex: 3 });
}
};
await saveOrder(draggable, api);
expect(api.save).toHaveBeenCalledWith(3);
});
Перетаскивание часто используется для изменения интерфейса.
export function changeColorOnDrag(draggable) {
draggable.on('drag:start', (event) => {
event.source.style.background = 'red';
});
}
test('background should change', () => {
const element = document.createElement('div');
const draggable = {
on: (event, cb) => {
cb({ source: element });
}
};
changeColorOnDrag(draggable);
expect(element.style.background).toBe('red');
});
Типичная структура проекта с unit-тестами:
src/
draggable/
init.js
plugins.js
tests/
draggable.test.js
plugins.test.js
jest.config.js
Основные принципы организации:
1. Один модуль — один файл тестов
drag-handler.js
drag-handler.test.js
2. Изолированные тесты
Каждый тест:
3. Чёткие названия
should trigger drag:start event
should update store on sort
should apply CSS class
При работе с DOM необходимо очищать окружение после каждого теста.
afterEach(() => {
document.body.innerHTML = '';
});
Это предотвращает:
Jest предоставляет множество инструментов для проверки поведения.
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(arg);
Пример:
expect(store.update).toHaveBeenCalledWith(1, 4);
Важным показателем является coverage.
Команда:
jest --coverage
Проверяются:
Для логики drag-and-drop желательно покрытие:
Особенно важно покрывать:
Нативные drag-события браузера плохо работают в JSDOM.
Лучше:
draggable.trigger('drag:start');
Некоторые тесты пытаются проверить сложное перемещение элементов.
Рекомендуемый подход:
Без mock-объектов тесты становятся:
1. Тестировать только собственный код
Draggable уже протестирован разработчиками библиотеки.
Unit-тесты должны проверять:
2. Минимизировать DOM
Чем меньше DOM-структура, тем стабильнее тест.
3. Использовать mock-события
const event = {
source: element,
oldIndex: 0,
newIndex: 1
};
4. Проверять побочные эффекты
Наиболее часто используемые инструменты:
Эти инструменты позволяют:
import {Draggable} from '@shopify/draggable';
import {trackOrder} from '../trackOrder';
describe('trackOrder', () => {
test('should update store after sorting', () => {
const store = {
update: jest.fn()
};
const draggable = new Draggable(document.createElement('div'), {
draggable: '.item'
});
trackOrder(draggable, store);
draggable.trigger('sortable:sorted', {
oldIndex: 1,
newIndex: 3
});
expect(store.update).toHaveBeenCalledWith(1, 3);
});
});
Такой тест проверяет полный цикл: