Юнит-тестирование компонентов в Choices.js опирается на проверку поведения экземпляра библиотеки в изолированной среде DOM, где ключевым становится контроль состояния, событий и рендеринга интерфейса. Библиотека активно взаимодействует с DOM-деревом, поэтому основное внимание при тестировании уделяется не чистой логике, а корректной синхронизации состояния и визуального представления.
Choices.js представляет собой слой над стандартными HTML-элементами
<select> и <input>, расширяя их
функциональность: кастомные списки, мультивыбор, поиск, асинхронная
подгрузка данных, управление тегами. В рамках юнит-тестирования
рассматриваются следующие аспекты:
Основная сложность заключается в том, что логика распределена между внутренним состоянием и 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.
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 элемента.
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 состояния и фактическое отображение элементов.
Юнит-тестирование Choices.js эффективно только при строгой изоляции DOM и контроле всех побочных эффектов, возникающих при изменении состояния компонента.