Юнит-тестирование компонентов

Юнит-тестирование компонентов в Choices.js опирается на проверку поведения экземпляра библиотеки в изолированной среде DOM, где ключевым становится контроль состояния, событий и рендеринга интерфейса. Библиотека активно взаимодействует с DOM-деревом, поэтому основное внимание при тестировании уделяется не чистой логике, а корректной синхронизации состояния и визуального представления.

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

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

Основная сложность заключается в том, что логика распределена между внутренним состоянием и DOM, что требует использования среды JSDOM или аналогов.

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

Для тестирования Choices.js обычно применяются Jest или Vitest совместно с JSDOM. JSDOM имитирует браузерное окружение, позволяя работать с document, window и событиями DOM.

Пример базовой конфигурации Jest:

module.exports = {
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["<rootDir>/tests/setup.js"]
};

Дополнительно может подключаться полифилл TextEncoder, MutationObserver, а также мок requestAnimationFrame, так как Choices.js использует асинхронные обновления DOM.

global.MutationObserver = require("mutation-observer");
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);

Инициализация экземпляра как объект тестирования

Первый слой тестов касается создания экземпляра Choices и проверки базовой структуры DOM.

import Choices from "choices.js";

test("инициализация Choices создает обертку DOM", () => {
  document.body.innerHTML = `
    <select id="test">
      <option value="a">A</option>
    </select>
  `;

  const element = document.getElementById("test");
  const instance = new Choices(element);

  const container = document.querySelector(".choices");
  expect(container).not.toBeNull();
  expect(container.classList.contains("choices")).toBe(true);
});

Проверяется факт трансформации исходного элемента в структуру с контейнером .choices, списком и внутренним input.

Работа с элементами select и input

Choices.js по-разному обрабатывает <select multiple> и <input>. В тестах проверяется корректность загрузки начальных значений и синхронизация состояния.

test("выбранные значения из select отображаются как items", () => {
  document.body.innerHTML = `
    <select id="test" multiple>
      <option value="1" selected>One</option>
      <option value="2">Two</option>
    </select>
  `;

  new Choices("#test");

  const items = document.querySelectorAll(".choices__item");
  expect(items.length).toBe(1);
  expect(items[0].textContent).toContain("One");
});

Ключевым моментом является проверка соответствия между selected-состоянием и визуальными элементами.

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

Методы setChoiceByValue, setValue, removeItem и clearStore часто становятся объектами юнит-тестов.

test("удаление элемента обновляет состояние", () => {
  document.body.innerHTML = `
    <select id="test" multiple>
      <option value="a" selected>A</option>
      <option value="b" selected>B</option>
    </select>
  `;

  const instance = new Choices("#test");
  instance.removeActiveItemsByValue("a");

  const items = document.querySelectorAll(".choices__item");
  expect(items.length).toBe(1);
});

Проверяется синхронизация между внутренним store и DOM.

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

Choices.js активно использует события: addItem, removeItem, change, highlightItem. Юнит-тесты проверяют вызов callback-функций и корректную передачу данных.

test("событие addItem вызывается при добавлении значения", () => {
  document.body.innerHTML = `<input id="test">`;

  const callback = jest.fn();

  new Choices("#test", {
    callbackOnCreateTemplates: () => {},
  }).passedElement.element.addEventListener("addItem", callback);

  const instance = new Choices("#test");
  instance.setChoiceByValue("new");

  expect(callback).toHaveBeenCalled();
});

Особое внимание уделяется структуре event.detail, содержащей value, label и id элемента.

Проверка кастомных шаблонов (templates)

Choices.js позволяет переопределять HTML-шаблоны элементов интерфейса. Это требует тестирования корректной вставки пользовательской разметки.

test("кастомный шаблон отображается в DOM", () => {
  document.body.innerHTML = `<input id="test">`;

  new Choices("#test", {
    callbackOnCreateTemplates: () => ({
      item: (classNames, data) => {
        return `<div class="${classNames.item} custom">${data.label}</div>`;
      }
    })
  });

  const item = document.querySelector(".choices__item");
  expect(item.classList.contains("custom")).toBe(true);
});

Проверяется не только наличие кастомного HTML, но и сохранение связки с внутренними классами библиотеки.

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

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

test("поиск фильтрует список опций", () => {
  document.body.innerHTML = `
    <input id="test">
  `;

  const instance = new Choices("#test", {
    choices: [
      { value: "apple", label: "Apple" },
      { value: "banana", label: "Banana" }
    ]
  });

  instance.input.value = "app";
  instance.input.dispatchEvent(new Event("input"));

  const visibleItems = document.querySelectorAll(".choices__item--choice");
  expect(visibleItems.length).toBe(1);
});

Проверяется реакция на ввод и обновление DOM после фильтрации.

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

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

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve([
      { value: "x", label: "X" }
    ])
  })
);

Тест:

test("асинхронная загрузка данных добавляет опции", async () => {
  document.body.innerHTML = `<input id="test">`;

  const instance = new Choices("#test", {
    shouldSort: false,
  });

  await instance.setChoices(async () => {
    const res = await fetch("/data");
    return res.json();
  });

  const options = document.querySelectorAll(".choices__item--choice");
  expect(options.length).toBe(1);
});

Особое внимание уделяется ожиданию завершения промисов и обновлению DOM после resolve.

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

Для тестирования debounce, search throttling и асинхронных обновлений применяются fake timers.

jest.useFakeTimers();

test("debounce поиска работает корректно", () => {
  document.body.innerHTML = `<input id="test">`;

  new Choices("#test");

  const input = document.querySelector("input");
  input.value = "test";
  input.dispatchEvent(new Event("input"));

  jest.advanceTimersByTime(300);

  const items = document.querySelectorAll(".choices__list--dropdown");
  expect(items.length).toBeGreaterThan(0);
});

Моки позволяют контролировать временные задержки без реального ожидания.

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

Choices.js поддерживает расширения через плагины и кастомные обработчики. Тестирование таких модулей сводится к проверке влияния на lifecycle экземпляра.

test("плагин изменяет поведение добавления элементов", () => {
  const plugin = (instance) => {
    instance.passedElement.element.addEventListener("addItem", (e) => {
      e.detail.value = e.detail.value.toUpperCase();
    });
  };

  document.body.innerHTML = `<input id="test">`;

  new Choices("#test", { plugins: [plugin] });

  const instance = new Choices("#test");
  instance.setValue("abc");

  const item = document.querySelector(".choices__item");
  expect(item.textContent).toContain("ABC");
});

Проверяется вмешательство в поток событий и корректность модификации данных.

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

Choices.js хранит состояние выбранных элементов и списка опций. В юнит-тестах проверяется согласованность store и DOM.

test("внутреннее состояние синхронизировано с DOM", () => {
  document.body.innerHTML = `<input id="test">`;

  const instance = new Choices("#test");
  instance.setValue("value1");

  expect(instance.getValue(true)).toContain("value1");
});

Сравнивается API состояния и фактическое отображение элементов.

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

  • отсутствие JSDOM-совместимости для MutationObserver
  • попытки тестировать визуальные стили вместо DOM-структуры
  • игнорирование асинхронных обновлений
  • отсутствие изоляции экземпляров Choices
  • смешивание unit и integration тестов без разделения контекста

Юнит-тестирование Choices.js эффективно только при строгой изоляции DOM и контроле всех побочных эффектов, возникающих при изменении состояния компонента.