Интеграционное тестирование

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

Интеграционные тесты для Tom Select обычно выполняются в среде, имитирующей браузер. Чаще всего используются Jest с jsdom, Vitest или браузерные раннеры вроде Playwright.

Ключевой момент — корректная эмуляция DOM, поскольку Tom Select активно манипулирует элементами:

/**
 * Базовая HTML-структура для теста
 */
document.body.innerHTML = `
  <select id="select">
    <option value="1">One</option>
    <option value="2">Two</option>
  </select>
`;

Инициализация компонента:

import TomSelect fr om "tom-select";

const el = document.querySelector("#select");

const instance = new TomSelect(el, {
  create: false,
  persist: false
});

Важно учитывать, что Tom Sel ect создаёт собственную DOM-структуру поверх исходного <select>, поэтому тесты должны опираться на новые элементы, а не на оригинальный тег.

Проверка инициализации и DOM-структуры

Интеграционный тест должен подтверждать, что компонент корректно преобразует исходный элемент:

test("создаёт контейнер Tom Select", () => {
  const wrapper = document.querySelector(".tom-select");

  expect(wrapper).not.toBeNull();
});

Типичные проверки:

  • наличие контейнера .tom-select
  • скрытие исходного <select>
  • создание input-поля
  • корректная привязка значений
expect(el.style.display).toBe("none");

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

Основной сценарий — выбор опции через интерфейс:

const control = document.querySelector(".ts-control");

control.click();

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

const items = document.querySelectorAll(".option");
expect(items.length).toBeGreaterThan(0);

Выбор элемента:

items[0].click();

expect(instance.getValue()).toBe("1");

Проверка синхронизации состояния:

  • значение в скрытом <select>
  • значение внутри Tom Select API
  • отображение выбранного item в UI

Работа с асинхронными данными

Одной из ключевых особенностей интеграционного тестирования Tom Select является проверка remote-загрузки данных через load:

const instance = new TomSelect(el, {
  load: (query, callback) => {
    setTimeout(() => {
      callback([
        { value: "10", text: "Ten" },
        { value: "20", text: "Twenty" }
      ]);
    }, 50);
  }
});

Тест должен учитывать задержку:

control.focus();
control.value = "te";
control.dispatchEvent(new Event("input"));

await new Promise(r => setTimeout(r, 100));

const options = document.querySelectorAll(".option");
expect(options.length).toBe(2);

Ключевой аспект — контроль таймингов и предотвращение флаки-тестов.

Мокирование fetch и XHR

При интеграции с API часто используется fetch. В тестах необходимо подменять сетевой слой:

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () =>
      Promise.resolve([
        { value: "1", text: "Alpha" },
        { value: "2", text: "Beta" }
      ])
  })
);

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

const instance = new TomSelect(el, {
  load: async (query, callback) => {
    const res = await fetch("/api/items?q=" + query);
    const data = await res.json();
    callback(data);
  }
});

Проверка результата:

control.focus();
control.value = "a";
control.dispatchEvent(new Event("input"));

await flushPromises();

expect(fetch).toHaveBeenCalled();

flushPromises используется для ожидания всех микротасков:

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

Тестирование событийной модели

Tom Select активно использует события: onChange, onDropdownOpen, onItemAdd.

Пример интеграционного теста:

const onCha nge = jest.fn();

const instance = new TomSelect(el, {
  onChange
});

Имитация выбора:

instance.addItem("1");

expect(onChange).toHaveBeenCalledWith("1");

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

Поведение при множественном выборе

Для maxItems > 1 тестируется добавление нескольких элементов:

const instance = new TomSelect(el, {
  maxItems: 3
});

instance.addItem("1");
instance.addItem("2");
instance.addItem("3");

Проверки:

expect(instance.items.length).toBe(3);
expect(instance.getValue()).toBe("1,2,3");

Также важно тестировать ограничение:

instance.addItem("4");

expect(instance.items.length).toBe(3);

Очистка состояния и повторная инициализация

Интеграционные тесты должны учитывать жизненный цикл компонента.

Очистка:

instance.destroy();

Проверки после уничтожения:

  • удаление DOM-обёртки
  • восстановление <select>
  • отсутствие обработчиков событий
expect(document.querySelector(".tom-select")).toBeNull();

Повторная инициализация:

const instance2 = new TomSelect(el, {});
expect(instance2).toBeDefined();

Тестирование плагинов

Tom Select поддерживает плагины, влияющие на поведение интерфейса. Интеграционные тесты обязаны учитывать их влияние на DOM.

Пример с remove_button:

const instance = new TomSelect(el, {
  plugins: ["remove_button"]
});

Проверка кнопки удаления:

instance.addItem("1");

const removeBtn = document.querySelector(".remove");
expect(removeBtn).not.toBeNull();

Имитация удаления:

removeBtn.click();
expect(instance.items.length).toBe(0);

Проверка фильтрации и поиска

Поиск внутри Tom Select — один из наиболее чувствительных сценариев:

control.focus();
control.value = "one";
control.dispatchEvent(new Event("input"));

Проверка отфильтрованных элементов:

const visibleOptions = Array.fr om(document.querySelectorAll(".option"))
  .filter(o => o.style.display !== "none");

expect(visibleOptions.length).toBe(1);

Важно учитывать debounce-логику, если она включена в конфигурации.

Обработка гонок и нестабильных состояний

Асинхронные операции внутри Tom Sel ect могут приводить к race conditions. Для стабилизации тестов используется контроль времени:

jest.useFakeTimers();

control.value = "a";
control.dispatchEvent(new Event("input"));

jest.runAllTimers();

Это позволяет детерминированно проверять результат загрузки и фильтрации.

Проверка синхронизации с внешним <select>

Критически важный аспект интеграции — корректное отражение состояния:

instance.addItem("2");

expect(el.value).toBe("2");

При множественном выборе:

expect(Array.fr om(el.selectedOptions).map(o => o.value))
  .toEqual(["1", "2"]);

Проверка клавиатурной навигации

Интеграционные тесты часто включают эмуляцию клавиш:

control.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown" }));
control.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));

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

  • изменение активного элемента
  • выбор значения через Enter
  • корректное обновление UI

Стратегии минимизации флаки-тестов

Поведение Tom Select чувствительно к таймингам и DOM-рефлоу, поэтому стабильность тестов достигается через:

  • использование fake timers
  • изоляцию DOM между тестами
  • очистку document.body после каждого теста
  • мокирование всех сетевых запросов
  • отказ от реальных задержек setTimeout в логике тестов
afterEach(() => {
  document.body.innerHTML = "";
  jest.clearAllMocks();
});

Проверка сложных сценариев комбинирования

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

  • ввод текста
  • загрузка данных
  • выбор элемента
  • добавление нового значения
  • удаление
  • повторный поиск
control.value = "a";
control.dispatchEvent(new Event("input"));

await flushPromises();

document.querySelectorAll(".option")[0].click();
instance.addItem("2");

instance.removeItem("1");

Проверка итогового состояния:

expect(instance.getValue()).toBe("2");

Такие сценарии выявляют скрытые проблемы взаимодействия внутренних модулей Tom Select, особенно при комбинировании плагинов, асинхронных загрузок и пользовательских обработчиков событий.