Unit тесты

Интерактивные интерфейсы с перетаскиванием содержат большое количество событий, состояний и DOM-манипуляций. Ошибки в такой логике проявляются не сразу: некорректное положение элемента, пропущенное событие, неправильный порядок вызовов обработчиков.

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

  • корректность инициализации экземпляра Draggable
  • работу событийной системы
  • поведение кастомных плагинов
  • изменение DOM при перетаскивании
  • интеграцию с пользовательской логикой

В библиотеке Shopify Draggable тестирование обычно выполняется в среде Jest либо Mocha с использованием виртуального DOM через JSDOM.

Unit-тесты сосредоточены на проверке отдельных компонентов: классов, функций и событийных обработчиков.


Тестирование инициализации Draggable

Экземпляр Draggable создаётся на основе контейнера и конфигурации. Базовый тест проверяет корректность создания объекта и привязки элементов.

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

import {Draggable} from '@shopify/draggable';

export function createDraggable(container) {
  return new Draggable(container, {
    draggable: '.item'
  });
}

Unit-тест

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();
});

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

  • создание экземпляра
  • отсутствие ошибок при инициализации
  • доступность методов API

Тестирование конфигурации

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

Тестирование событий Draggable

Основная логика библиотеки основана на системе событий. Проверка событий является ключевой частью unit-тестирования.

Основные события:

Событие Описание
drag:start начало перетаскивания
drag:move перемещение
drag:stop завершение
sortable:sorted изменение порядка

Проверка события drag:start

Тестируемый код

export function attachStartHandler(draggable, callback) {
  draggable.on('drag:start', callback);
}

Unit-тест

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);
  });
}

Unit-тест

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);
});

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

  • корректная передача данных
  • правильный вызов методов бизнес-логики

Тестирование DOM-изменений

Draggable активно изменяет DOM: перемещает элементы, создаёт placeholder, добавляет классы.

Unit-тест должен фиксировать эти изменения.

Тестируемая функция

export function markDragging(element) {
  element.classList.add('dragging');
}

Unit-тест

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');
    });
  }
}

Unit-тест

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);
});

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

  • корректная регистрация событий
  • изменение DOM

Mock-объекты при тестировании

Многие компоненты Draggable сложно протестировать напрямую из-за зависимости от DOM-событий. Используются mock-объекты.

Mock Draggable

const draggableMock = {
  on: jest.fn(),
  off: jest.fn(),
  trigger: jest.fn()
};

Mock позволяет:

  • изолировать тестируемую функцию
  • контролировать поведение библиотеки
  • проверять вызовы API

Тестирование асинхронных обработчиков

Некоторые обработчики могут содержать асинхронный код.

Пример

export function saveOrder(draggable, api) {
  draggable.on('sortable:sorted', async (event) => {
    await api.save(event.newIndex);
  });
}

Unit-тест

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);
});

Тестирование пользовательских обработчиков drag

Перетаскивание часто используется для изменения интерфейса.

Пример

export function changeColorOnDrag(draggable) {
  draggable.on('drag:start', (event) => {
    event.source.style.background = 'red';
  });
}

Unit-тест

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. Изолированные тесты

Каждый тест:

  • создаёт свой DOM
  • не зависит от других тестов

3. Чёткие названия

should trigger drag:start event
should update store on sort
should apply CSS class

Очистка DOM после тестов

При работе с DOM необходимо очищать окружение после каждого теста.

afterEach(() => {
  document.body.innerHTML = '';
});

Это предотвращает:

  • конфликт элементов
  • накопление DOM-узлов
  • случайные зависимости между тестами

Проверка вызовов функций

Jest предоставляет множество инструментов для проверки поведения.

Основные проверки

expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(arg);

Пример:

expect(store.update).toHaveBeenCalledWith(1, 4);

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

Важным показателем является coverage.

Команда:

jest --coverage

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

  • функции
  • ветвления
  • строки
  • условия

Для логики drag-and-drop желательно покрытие:

  • не менее 80–90%

Особенно важно покрывать:

  • обработчики событий
  • пользовательские плагины
  • функции обработки DOM

Типичные ошибки при тестировании Draggable

Тестирование реальных drag-событий

Нативные drag-события браузера плохо работают в JSDOM.

Лучше:

  • вызывать события вручную
  • тестировать обработчики

Пример

draggable.trigger('drag:start');

Сильная зависимость от DOM

Некоторые тесты пытаются проверить сложное перемещение элементов.

Рекомендуемый подход:

  • тестировать логику
  • не тестировать саму библиотеку

Отсутствие моков

Без mock-объектов тесты становятся:

  • медленными
  • нестабильными
  • сложными для поддержки

Практические рекомендации

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

Draggable уже протестирован разработчиками библиотеки.

Unit-тесты должны проверять:

  • обработчики
  • плагины
  • интеграцию.

2. Минимизировать DOM

Чем меньше DOM-структура, тем стабильнее тест.

3. Использовать mock-события

const event = {
  source: element,
  oldIndex: 0,
  newIndex: 1
};

4. Проверять побочные эффекты

  • изменения классов
  • вызовы API
  • обновление состояния приложения

Инструменты тестирования

Наиболее часто используемые инструменты:

  • Jest — основной тестовый фреймворк
  • JSDOM — виртуальный DOM
  • Testing Library — удобные DOM-утилиты
  • Mocha + Chai — альтернативный стек

Эти инструменты позволяют:

  • моделировать DOM
  • отслеживать события
  • контролировать вызовы функций
  • измерять покрытие кода

Пример полного unit-теста модуля

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);
  });

});

Такой тест проверяет полный цикл:

  • создание экземпляра Draggable
  • регистрацию обработчика
  • генерацию события
  • реакцию бизнес-логики приложения.