Тестовые утилиты

Интерфейсы, построенные на основе библиотеки Muuri, активно используют динамическое позиционирование элементов, анимации, фильтрацию, сортировку и drag-and-drop. Такие механизмы усложняют автоматическое тестирование, поскольку:

  • изменения происходят асинхронно;
  • элементы перемещаются в DOM;
  • позиции вычисляются библиотекой;
  • поведение зависит от пользовательских событий.

Тестовые утилиты позволяют проверять:

  • корректность инициализации сетки;
  • порядок элементов после сортировки;
  • состояние фильтрации;
  • корректность drag-and-drop операций;
  • события Muuri;
  • состояние DOM после анимаций.

В крупных проектах тестирование Muuri обычно реализуется с помощью инструментов:

  • Jest
  • Mocha
  • Vitest
  • Playwright
  • Cypress

Однако для корректных тестов требуется набор вспомогательных утилит, позволяющих работать с асинхронностью, анимациями и DOM-изменениями.


Тестовая структура DOM для Muuri

Muuri работает исключительно с DOM-элементами, поэтому перед тестированием требуется подготовить тестовую разметку.

Пример минимальной структуры:

<div class="grid">
  <div class="item"><div class="item-content">1</div></div>
  <div class="item"><div class="item-content">2</div></div>
  <div class="item"><div class="item-content">3</div></div>
</div>

Инициализация сетки:

import Muuri from 'muuri';

const grid = new Muuri('.grid', {
  items: '.item'
});

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

function createTestGrid() {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item"><div class="item-content">A</div></div>
      <div class="item"><div class="item-content">B</div></div>
      <div class="item"><div class="item-content">C</div></div>
    </div>
  `;

  return new Muuri('.grid');
}

Такая функция становится базовой тестовой утилитой.


Утилита ожидания завершения layout

Muuri выполняет перерасчёт позиций асинхронно. Проверка расположения элементов должна происходить только после завершения layout.

Muuri предоставляет событие:

layoutEnd

Тестовая утилита ожидания:

function waitForLayout(grid) {
  return new Promise(resolve => {
    grid.once('layoutEnd', resolve);
  });
}

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

test('layout завершён', async () => {
  const grid = createTestGrid();

  grid.layout();
  await waitForLayout(grid);

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

Утилита отключения анимаций

Анимации усложняют тестирование, поскольку меняют время выполнения операций.

Muuri позволяет отключить их через настройки.

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

function createTestGrid() {
  document.body.innerHTML = `
    <div class="grid">
      <div class="item"><div>1</div></div>
      <div class="item"><div>2</div></div>
    </div>
  `;

  return new Muuri('.grid', {
    layoutDuration: 0,
    dragReleaseDuration: 0,
    showDuration: 0,
    hideDuration: 0
  });
}

Это превращает анимации в мгновенные операции.


Утилита получения текстового порядка элементов

После сортировки или drag-and-drop важно проверить порядок элементов.

Утилита:

function getItemOrder(grid) {
  return grid.getItems().map(item => {
    return item.getElement().textContent.trim();
  });
}

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

test('сортировка элементов', () => {
  const grid = createTestGrid();

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

  const order = getItemOrder(grid);

  expect(order).toEqual(['3', '2', '1']);
});

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

Muuri использует методы show() и hide() для управления отображением.

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

function isVisible(item) {
  return item.isVisible();
}

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

test('скрытие элемента', () => {
  const grid = createTestGrid();
  const item = grid.getItems()[0];

  grid.hide([item]);

  expect(isVisible(item)).toBe(false);
});

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

Muuri поддерживает динамическую фильтрацию элементов.

Пример:

grid.filter(item => {
  return item.getElement().textContent.includes('A');
});

Тестовая проверка:

test('фильтрация элементов', () => {
  const grid = createTestGrid();

  grid.filter(item => {
    return item.getElement().textContent === 'A';
  });

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

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

Утилита ожидания событий Muuri

Muuri активно использует событийную систему:

  • add
  • remove
  • showEnd
  • hideEnd
  • layoutStart
  • layoutEnd
  • dragStart
  • dragEnd

Тестовая утилита:

function waitForEvent(grid, eventName) {
  return new Promise(resolve => {
    grid.once(eventName, resolve);
  });
}

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

await waitForEvent(grid, 'layoutEnd');

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

Muuri поддерживает динамическое добавление элементов через add().

Создание DOM-элемента:

function createItem(content) {
  const el = document.createElement('div');
  el.className = 'item';
  el.innerHTML = `<div>${content}</div>`;
  return el;
}

Тест:

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

  const item = createItem('D');

  grid.add(item);

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

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

Метод удаления:

grid.remove(items)

Тест:

test('удаление элемента', () => {
  const grid = createTestGrid();
  const item = grid.getItems()[0];

  grid.remove(item);

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

Утилита имитации drag-and-drop

Drag-and-drop сложно тестировать напрямую, поэтому используются симулированные события.

Пример генерации события мыши:

function triggerMouseEvent(node, type, x, y) {
  const event = new MouseEvent(type, {
    clientX: x,
    clientY: y,
    bubbles: true
  });

  node.dispatchEvent(event);
}

Сценарий drag:

mousedown
mousemove
mousemove
mouseup

Пример:

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

triggerMouseEvent(element, 'mousedown', 100, 100);
triggerMouseEvent(document, 'mousemove', 200, 200);
triggerMouseEvent(document, 'mouseup', 200, 200);

Тестирование позиции элементов

Muuri управляет координатами элементов через transform.

Проверка позиции:

function getItemPosition(item) {
  return item.getPosition();
}

Пример:

const position = getItemPosition(grid.getItems()[0]);

expect(position.left).toBeGreaterThanOrEqual(0);
expect(position.top).toBeGreaterThanOrEqual(0);

Утилита сброса DOM между тестами

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

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

Это предотвращает накопление элементов и конфликт тестов.


Тестирование сортировки

Muuri поддерживает пользовательскую функцию сортировки.

Пример:

grid.sort((itemA, itemB) => {
  const a = itemA.getElement().textContent;
  const b = itemB.getElement().textContent;
  return a.localeCompare(b);
});

Тест:

test('алфавитная сортировка', () => {
  const grid = createTestGrid();

  grid.sort((a, b) => {
    return a.getElement().textContent.localeCompare(
      b.getElement().textContent
    );
  });

  const order = getItemOrder(grid);

  expect(order).toEqual(['A', 'B', 'C']);
});

Утилита тестирования размеров элементов

Muuri использует размеры элементов для расчёта layout.

Получение размеров:

function getItemSize(item) {
  const rect = item.getElement().getBoundingClientRect();
  return {
    width: rect.width,
    height: rect.height
  };
}

Пример проверки:

const size = getItemSize(grid.getItems()[0]);

expect(size.width).toBeGreaterThan(0);
expect(size.height).toBeGreaterThan(0);

Тестирование destroy()

Метод destroy() удаляет все обработчики и очищает структуру Muuri.

test('уничтожение сетки', () => {
  const grid = createTestGrid();

  grid.destroy();

  expect(grid._isDestroyed).toBe(true);
});

После destroy:

  • события больше не работают
  • drag-and-drop отключается
  • элементы возвращаются в обычный DOM

Утилита логирования событий

При сложных тестах полезно отслеживать последовательность событий.

function logEvents(grid, events) {
  const log = [];

  events.forEach(event => {
    grid.on(event, () => {
      log.push(event);
    });
  });

  return log;
}

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

const log = logEvents(grid, [
  'layoutStart',
  'layoutEnd'
]);

После выполнения операций можно проверить порядок:

expect(log).toEqual([
  'layoutStart',
  'layoutEnd'
]);

Тестирование взаимодействия нескольких сеток

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

Создание двух сеток:

const gridA = new Muuri('.grid-a');
const gridB = new Muuri('.grid-b');

Конфигурация drag:

dragEnabled: true,
dragSort: () => [gridA, gridB]

Тест проверяет:

  • перенос элемента
  • обновление количества элементов
  • корректный layout обеих сеток

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

Обычно тестовые утилиты выносятся в отдельный модуль:

test-utils/
    createGrid.js
    waitForLayout.js
    dragSimulation.js
    itemHelpers.js

Структура файла:

export {
  createTestGrid,
  waitForLayout,
  getItemOrder,
  triggerMouseEvent
};

Это делает тесты компактными и повторно используемыми.


Типичные проблемы тестирования Muuri

1. Асинхронные layout операции

Решение — ожидание событий layoutEnd.


2. Анимации

Решение — отключение длительности анимаций.


3. Отсутствие реального layout в jsdom

jsdom не рассчитывает размеры элементов.

Решение:

  • мокирование размеров
  • использование Playwright или Cypress.

4. Сложности drag-and-drop

Иногда требуется тестирование в реальном браузере.

Инструменты:

  • Playwright
  • Cypress
  • Puppeteer

Подходы к тестированию Muuri

Unit-тесты

Проверяют:

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

Используют jsdom.


Integration-тесты

Проверяют:

  • drag-and-drop
  • взаимодействие сеток
  • пользовательские сценарии

Используют браузерные среды.


End-to-End тесты

Проверяют реальные сценарии:

  • перетаскивание карточек
  • фильтрацию интерфейса
  • динамическое добавление элементов.

Инструменты:

  • Cypress
  • Playwright

Практика организации тестового окружения

Стандартная структура проекта:

src/
    grid.js

tests/
    grid.test.js

tests/utils/
    createGrid.js
    waitForLayout.js
    domHelpers.js

Пример импорта:

import {
  createTestGrid,
  waitForLayout
} from './utils/createGrid';

Такой подход обеспечивает:

  • изолированные тесты
  • повторное использование утилит
  • устойчивость тестов при изменении структуры проекта.