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

Интеграционное тестирование в контексте использования Choices.js направлено на проверку корректного взаимодействия библиотеки с окружающей системой: DOM-структурой, пользовательскими событиями, серверными API, формами, валидацией и сторонними JavaScript-модулями. В отличие от модульных тестов, здесь проверяется не изолированная функция, а цепочка поведения компонента в реальной среде браузера или её эмуляции.

Choices.js представляет собой компонент для кастомизации <select> и <input> элементов с поддержкой поиска, мультиселекта, тегов и динамических источников данных. При интеграционном тестировании ключевое значение имеют следующие аспекты:

  • Манипуляции с DOM происходят динамически
  • Внутреннее состояние синхронизируется с нативными input/select
  • Используются события input, change, search, addItem, removeItem
  • Поддерживается асинхронная подгрузка данных
  • Возможна интеграция с внешними API и формами

Эти особенности требуют тестирования не только логики, но и поведения интерфейса в реальном времени.

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

Для интеграционного тестирования Choices.js чаще всего применяются:

  • Jest + jsdom для быстрых DOM-тестов
  • Testing Library (DOM Testing Library / React Testing Library при обёртках)
  • Cypress для end-to-end сценариев
  • Playwright для кросс-браузерной проверки

Базовая конфигурация с jsdom позволяет симулировать DOM:

import Choices from 'choices.js';

document.body.innerHTML = `
  <sel ect id="city">
    <option value="1">London</option>
    <option value="2">Paris</option>
  </select>
`;

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

const element = document.getElementById('city');

const choices = new Choices(element, {
  searchEnabled: true,
  removeItemButton: true
});

Проверка инициализации компонента

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

  • нативный select скрывается или заменяется кастомной разметкой
  • создаётся контейнер Choices
  • опции корректно рендерятся
test('инициализация Choices.js создаёт кастомный UI', () => {
  expect(document.querySelector('.choices')).not.toBeNull();
  expect(document.querySelectorAll('.choices__item').length).toBeGreaterThan(0);
});

Важным моментом является проверка синхронизации между DOM и внутренним состоянием экземпляра.

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

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

  • клик по элементу
  • ввод текста в поиск
  • выбор элемента из списка
  • удаление выбранного значения

Пример с Testing Library:

import { fireEvent } fr om '@testing-library/dom';

const input = document.querySelector('.choices__input--cloned');

fireEvent.focus(input);
fireEvent.input(input, { target: { value: 'Pa' } });

Ожидается фильтрация списка:

expect(document.querySelectorAll('.choices__item--choice').length).toBe(1);

Проверка мультиселекта и тегов

Choices.js активно используется для мультивыбора, что требует проверки сложных состояний.

Ключевые сценарии:

  • добавление нескольких элементов
  • предотвращение дублирования
  • удаление через кнопку
  • обновление оригинального <select>
choices.setChoiceByValue('1');
choices.setChoiceByValue('2');

expect(element.selectedOptions.length).toBe(2);

Удаление элемента:

choices.removeActiveItemsByValue('1');

expect(element.selectedOptions[0].value).toBe('2');

Особое внимание уделяется синхронизации состояния между UI и DOM.

Интеграция с формами

Одним из критичных сценариев является работа внутри <form>.

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

  • корректная отправка значений
  • сериализация выбранных элементов
  • поведение при reset формы
const form = document.querySelector('form');

form.dispatchEvent(new Event('submit'));

const data = new FormData(form);
expect(data.getAll('city')).toContain('2');

Сброс формы должен очищать внутреннее состояние Choices:

form.reset();
expect(element.selectedOptions.length).toBe(0);

Асинхронная загрузка данных

Choices.js поддерживает динамическую загрузку через fetch или пользовательские источники данных. Интеграционное тестирование в этом случае требует мокирования сетевых запросов.

Пример с Jest:

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve([
      { value: '1', label: 'London' },
      { value: '2', label: 'Paris' }
    ])
  })
);

Проверка подгрузки:

await choices.setChoices(async () => {
  const response = await fetch('/api/cities');
  return response.json();
});

Ожидается, что элементы появятся в DOM:

expect(document.querySelectorAll('.choices__item--choice').length).toBe(2);

Обработка событий библиотеки

Choices.js генерирует набор событий, которые часто используются для интеграции с бизнес-логикой:

  • addItem
  • removeItem
  • highlightItem
  • search

Проверка событий:

const handler = jest.fn();

element.addEventListener('addItem', handler);

choices.setChoiceByValue('1');

expect(handler).toHaveBeenCalled();

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

const event = handler.mock.calls[0][0];
expect(event.detail.value).toBe('1');

Интеграция с валидацией форм

Choices.js часто используется совместно с системами валидации:

  • HTML5 validation
  • custom validators
  • libraries like Yup or Joi (на уровне приложения)

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

element.setCustomValidity('Required');
expect(element.checkValidity()).toBe(false);

Интеграционный сценарий включает проверку, что UI отражает состояние ошибки.

Работа с несколькими экземплярами

В реальных приложениях на одной странице может быть несколько экземпляров Choices.js. Тестирование должно исключать перекрёстное влияние:

const first = new Choices(document.getElementById('first'));
const second = new Choices(document.getElementById('second'));

first.setChoiceByValue('1');

expect(second.getValue()).toEqual([]);

Это проверяет изоляцию состояний.

Проверка уничтожения экземпляра

Корректное освобождение ресурсов важно при SPA:

choices.destroy();

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

  • восстановление исходного DOM
  • удаление обработчиков событий
  • отсутствие утечек состояния
expect(document.querySelector('.choices')).toBeNull();

Кросс-браузерное интеграционное тестирование

При использовании Cypress или Playwright проверяются реальные браузерные сценарии:

  • поведение в Chrome, Firefox, Edge
  • работа с клавиатурной навигацией
  • фокус и accessibility

Пример Cypress:

cy.get('.choices__input').type('Pa');
cy.get('.choices__item--choice').first().click();
cy.get('select').should('have.value', '2');

Проверка accessibility-интеграции

Choices.js должен корректно взаимодействовать с ARIA-атрибутами:

  • aria-expanded
  • aria-selected
  • role="listbox"

Тестируется:

expect(document.querySelector('.choices__list')).toHaveAttribute('role', 'listbox');

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

  • стрелки вверх/вниз
  • Enter для выбора
  • Esc для закрытия

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

В процессе тестирования Choices.js часто выявляются следующие проблемы:

  • рассинхронизация DOM и внутреннего состояния
  • некорректное обновление UI при динамических изменениях
  • утечки событий при повторной инициализации
  • нестабильность тестов из-за асинхронного рендера
  • конфликт CSS при нескольких экземплярах

Эти проблемы требуют повторного запуска тестов в различных условиях и строгого контроля асинхронных операций.

Стратегии повышения стабильности тестов

Для повышения надёжности интеграционных тестов применяются:

  • ожидания через waitFor
  • контроль таймеров (jest.useFakeTimers)
  • изоляция DOM между тестами
  • полная переинициализация экземпляров
  • мокирование сетевых слоёв

Особенно важно учитывать задержки при рендере и поисковой фильтрации, так как Choices.js может обновлять UI асинхронно.