Unit-тестирование Grid

Компонент Grid в библиотеке Muuri отвечает за управление набором элементов: позиционирование, сортировку, фильтрацию, перемещение, добавление и удаление. Несмотря на то что Muuri активно работает с DOM, большая часть логики может проверяться изолированно.

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

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

При разработке интерфейсов с большим количеством интерактивных элементов отсутствие тестов часто приводит к регрессиям — изменения в одной части логики могут нарушить работу сортировки, перетаскивания или позиционирования.

Unit-тесты позволяют:

  • фиксировать ожидаемое поведение Grid
  • проверять обновления библиотеки
  • гарантировать стабильность пользовательского интерфейса
  • автоматизировать проверку сложной логики взаимодействия элементов

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

Muuri — клиентская библиотека, поэтому тестирование чаще всего выполняется в JavaScript-среде с эмуляцией DOM.

Наиболее распространённый стек:

Инструмент Назначение
Jest тестовый раннер и фреймворк
Vitest современная альтернатива Jest
jsdom виртуальная DOM-среда
Testing Library удобная работа с DOM

Пример установки среды тестирования:

npm install --save-dev jest jsdom

В конфигурации Jest необходимо указать использование jsdom:

{
  "testEnvironment": "jsdom"
}

Это позволяет Muuri работать так же, как в браузере.


Создание тестового DOM для Grid

Muuri требует наличие контейнера и элементов внутри него.

Минимальная разметка:

<div class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

В тестах DOM создаётся программно:

document.body.innerHTML = `
  <div class="grid">
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
  </div>
`;

После этого сетка инициализируется:

import Muuri from 'muuri';

const gridElement = document.querySelector('.grid');
const grid = new Muuri(gridElement);

Проверка инициализации Grid

Первый уровень unit-тестов — проверка корректного создания экземпляра.

test('Grid успешно инициализируется', () => {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item"></div>
      <div class="item"></div>
    </div>
  `;

  const grid = new Muuri('.grid');

  expect(grid).toBeDefined();
});

Проверка количества элементов:

test('Grid содержит корректное количество элементов', () => {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item"></div>
      <div class="item"></div>
      <div class="item"></div>
    </div>
  `;

  const grid = new Muuri('.grid');

  expect(grid.getItems().length).toBe(3);
});

Метод getItems() возвращает массив объектов элементов Muuri.


Тестирование добавления элементов

Muuri позволяет динамически добавлять элементы.

Добавление DOM-элемента

test('Добавление элемента в сетку', () => {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item"></div>
    </div>
  `;

  const grid = new Muuri('.grid');

  const newItem = document.createElement('div');
  newItem.classList.add('item');

  grid.add(newItem);

  expect(grid.getItems().length).toBe(2);
});

Проверка индекса элемента

test('Новый элемент добавляется в конец', () => {
  const grid = new Muuri('.grid');

  const item = document.createElement('div');
  item.classList.add('item');

  grid.add(item);

  const items = grid.getItems();
  const lastItem = items[items.length - 1];

  expect(lastItem.getElement()).toBe(item);
});

Тестирование удаления элементов

Удаление элементов выполняется методом remove().

test('Удаление элемента из сетки', () => {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item"></div>
      <div class="item"></div>
    </div>
  `;

  const grid = new Muuri('.grid');
  const items = grid.getItems();

  grid.remove(items[0]);

  expect(grid.getItems().length).toBe(1);
});

Удаление с DOM:

grid.remove(items[0], { removeElements: true });

Тест:

test('Элемент удаляется из DOM', () => {
  const grid = new Muuri('.grid');
  const items = grid.getItems();

  grid.remove(items[0], { removeElements: true });

  expect(document.querySelectorAll('.item').length).toBe(1);
});

Тестирование сортировки элементов

Muuri поддерживает сортировку.

Сортировка по индексу

grid.sort((a, b) => {
  return a.getElement().dataset.id - b.getElement().dataset.id;
});

Тест:

test('Сортировка элементов', () => {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item" data-id="3"></div>
      <div class="item" data-id="1"></div>
      <div class="item" data-id="2"></div>
    </div>
  `;

  const grid = new Muuri('.grid');

  grid.sort((a, b) => {
    return a.getElement().dataset.id - b.getElement().dataset.id;
  });

  const items = grid.getItems();

  const first = items[0].getElement().dataset.id;

  expect(first).toBe("1");
});

Тестирование фильтрации

Фильтрация используется для показа только определённых элементов.

Пример:

grid.filter(item => {
  return item.getElement().dataset.category === "news";
});

Тест:

test('Фильтрация элементов', () => {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item" data-category="news"></div>
      <div class="item" data-category="blog"></div>
    </div>
  `;

  const grid = new Muuri('.grid');

  grid.filter(item => {
    return item.getElement().dataset.category === "news";
  });

  const visibleItems = grid.getItems().filter(item => item.isVisible());

  expect(visibleItems.length).toBe(1);
});

Тестирование layout-пересчёта

Метод layout() запускает перерасчёт расположения элементов.

grid.layout();

Тест:

test('layout обновляет позиции элементов', () => {
  const grid = new Muuri('.grid');

  const layoutSpy = jest.spyOn(grid, 'layout');

  grid.layout();

  expect(layoutSpy).toHaveBeenCalled();
});

Тестирование перемещения элементов

Muuri позволяет перемещать элементы внутри сетки.

grid.move(item, newIndex);

Тест:

test('Перемещение элемента', () => {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item" id="a"></div>
      <div class="item" id="b"></div>
    </div>
  `;

  const grid = new Muuri('.grid');
  const items = grid.getItems();

  grid.move(items[1], 0);

  const newFirst = grid.getItems()[0].getElement().id;

  expect(newFirst).toBe('b');
});

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

Muuri генерирует события.

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

Событие Назначение
add добавление элемента
remove удаление
move перемещение
layoutStart начало layout
layoutEnd завершение layout

Тест события add

test('Срабатывает событие add', () => {
  const grid = new Muuri('.grid');

  const handler = jest.fn();

  grid.on('add', handler);

  const item = document.createElement('div');
  item.classList.add('item');

  grid.add(item);

  expect(handler).toHaveBeenCalled();
});

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

Некоторые операции Muuri зависят от браузерных API:

  • getBoundingClientRect
  • requestAnimationFrame
  • ResizeObserver

В тестах их часто приходится мокировать.

Mock requestAnimationFrame

global.requestAnimationFrame = (cb) => {
  return setTimeout(cb, 0);
};

Mock getBoundingClientRect

HTMLElement.prototype.getBoundingClientRect = () => ({
  width: 100,
  height: 100,
  top: 0,
  left: 0,
  bottom: 100,
  right: 100
});

Это позволяет корректно тестировать layout.


Тестирование drag-функциональности

Drag-операции сложно тестировать напрямую, потому что они зависят от пользовательских событий.

Подходы:

  1. эмуляция событий мыши
  2. тестирование логики после drag
  3. интеграционные тесты

Пример эмуляции:

element.dispatchEvent(new MouseEvent('mousedown'));
element.dispatchEvent(new MouseEvent('mousemove'));
element.dispatchEvent(new MouseEvent('mouseup'));

Однако чаще проверяется итоговое состояние:

expect(grid.getItems()[0]).toBe(expectedItem);

Изоляция тестов

Каждый тест должен работать в чистой среде.

Используется beforeEach.

beforeEach(() => {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item"></div>
      <div class="item"></div>
    </div>
  `;
});

Создание grid:

let grid;

beforeEach(() => {
  grid = new Muuri('.grid');
});

Проверка асинхронных операций

Некоторые операции Muuri выполняются асинхронно.

Например:

  • анимации
  • layout
  • drag

Используется async/await.

test('layout выполняется асинхронно', async () => {
  const grid = new Muuri('.grid');

  await new Promise(resolve => {
    grid.once('layoutEnd', resolve);
    grid.layout();
  });

  expect(grid.getItems().length).toBe(2);
});

Структура тестов проекта

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

project
│
├─ src
│  └─ grid.js
│
├─ tests
│  └─ grid.test.js
│
├─ jest.config.js
└─ package.json

Пример тестового файла:

describe('Muuri Grid', () => {

  test('инициализация', () => {});

  test('добавление элементов', () => {});

  test('удаление элементов', () => {});

  test('сортировка', () => {});

  test('фильтрация', () => {});

});

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

Минимизировать зависимость от реального DOM

Тестировать:

  • состояние элементов
  • API методов
  • события

Избегать тестирования анимаций

Анимации — визуальный эффект, они редко требуют unit-тестов.

Фиксировать публичный API

Основные методы, которые должны быть покрыты тестами:

  • add()
  • remove()
  • move()
  • sort()
  • filter()
  • layout()
  • getItems()

Использовать spy-функции

Позволяют проверять вызовы методов.

const spy = jest.spyOn(grid, 'layout');

Проверять события

Muuri активно использует event-модель.


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

Отсутствие моков браузерных API

Muuri активно использует:

  • размеры элементов
  • animation frame
  • DOM-метрики

Без mock-объектов тесты падают.


Проверка визуальных координат

Позиции элементов часто меняются из-за layout-алгоритма.

Лучше проверять:

  • порядок элементов
  • состояние visibility
  • вызовы методов

Общий экземпляр Grid между тестами

Это приводит к состоянию гонки.

Каждый тест должен создавать новый экземпляр.


Покрытие тестами ключевых сценариев

Полный набор unit-тестов для Grid должен включать проверки:

Категория Что проверяется
Инициализация создание сетки
Элементы добавление, удаление
Управление перемещение
Сортировка правильный порядок
Фильтрация видимость
Layout перерасчёт
События корректные триггеры
Асинхронность layoutEnd, drag

Такое покрытие позволяет безопасно развивать интерфейс, не опасаясь нарушить поведение сетки при изменениях кода.