Покрытие кода тестами в библиотеке Choices.js строится вокруг проверки поведения DOM-компонента, управляемого состоянием, событиями и динамическими списками. Основная сложность тестирования заключается в том, что библиотека одновременно работает с пользовательским вводом, виртуализированным состоянием выбора и изменением структуры DOM, что требует комбинирования unit-, integration- и end-to-end подходов.
Структура тестов обычно делится на три уровня, каждый из которых закрывает отдельный слой логики:
1. Модульные тесты (Unit tests) Проверяют изолированные функции: парсинг опций, фильтрацию, нормализацию данных, генерацию внутренних структур.
2. Интеграционные тесты (Integration tests) Оценивают взаимодействие компонентов внутри библиотеки: связку состояния, событий и DOM-рендеринга.
3. End-to-End тесты (E2E) Проверяют поведение библиотеки в реальном браузере: ввод пользователя, выбор элементов, клавиатурную навигацию, доступность.
Такое разделение позволяет удерживать баланс между скоростью выполнения тестов и полнотой покрытия.
Для тестирования DOM-библиотек используется JSDOM или полноценный браузерный раннер.
Часто применяются:
Дополнительно подключается инструмент покрытия кода:
Пример базовой конфигурации покрытия:
{
"collectCoverage": true,
"coverageDirectory": "coverage",
"coverageReporters": ["text", "lcov", "html"],
"collectCoverageFrom": [
"src/**/*.js",
"!src/**/*.spec.js"
]
}
Внутренние функции Choices.js обычно не зависят от DOM и поддаются чистому тестированию.
import { normalizeOptions } from '../src/utils/normalize';
test('преобразует строковые опции в объекты', () => {
const input = ['Apple', 'Banana'];
const result = normalizeOptions(input);
expect(result).toEqual([
{ label: 'Apple', value: 'Apple' },
{ label: 'Banana', value: 'Banana' }
]);
});
Особое внимание уделяется:
test('обрабатывает пустой массив', () => {
expect(normalizeOptions([])).toEqual([]);
});
Ключевая часть логики связана с управлением состоянием выбора.
Основные события:
test('добавляет элемент в выбранные', () => {
const choices = new Choices(selectElement);
choices.setChoiceByValue('apple');
expect(choices.getValue(true)).toContain('apple');
});
test('удаляет элемент из выбранных', () => {
const choices = new Choices(selectElement);
choices.setChoiceByValue('apple');
choices.removeActiveItems();
expect(choices.getValue(true)).not.toContain('apple');
});
На этом уровне проверяется связка логики и DOM.
import { screen } from '@testing-library/dom';
test('рендерит список опций', () => {
new Choices(selectElement, {
choices: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' }
]
});
expect(screen.getByText('Apple')).toBeTruthy();
expect(screen.getByText('Banana')).toBeTruthy();
});
test('фильтрует список при вводе', () => {
const instance = new Choices(inputElement);
instance.setChoices([
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' }
]);
instance.input.value = 'App';
instance.input.dispatchEvent(new Event('input'));
expect(screen.getByText('Apple')).toBeTruthy();
expect(screen.queryByText('Banana')).toBeNull();
});
При тестировании важно контролировать поведение событий:
test('обрабатывает стрелки вниз', () => {
const instance = new Choices(selectElement);
const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
instance.container.dispatchEvent(event);
expect(instance.highlightedIndex).toBeGreaterThanOrEqual(0);
});
E2E сценарии проверяют реальное взаимодействие в браузере.
Пример сценария в Playwright:
test('выбор элемента через интерфейс', async ({ page }) => {
await page.goto('http://localhost:3000');
await page.click('.choices__input');
await page.fill('.choices__input', 'App');
await page.click('text=Apple');
const value = await page.$eval('select', el => el.value);
expect(value).toBe('apple');
});
Инструменты покрытия фиксируют:
Особенно важно покрывать:
Пример сложной ветки:
function isValidChoice(choice) {
if (!choice) return false;
if (choice.disabled) return false;
if (typeof choice.value === 'undefined') return false;
return true;
}
Тесты должны покрывать каждую ветвь отдельно.
При использовании remote data:
test('обрабатывает асинхронный поиск', async () => {
const instance = new Choices(selectElement, {
searchEnabled: true,
callbackOnSearch: async () => {
return [
{ label: 'Apple', value: 'apple' }
];
}
});
await instance.search('App');
expect(instance.resultsCount).toBe(1);
});
Критично тестировать:
test('имеет корректные aria-атрибуты', () => {
new Choices(selectElement);
expect(selectElement.getAttribute('aria-expanded')).toBe('false');
});
Флаки часто возникают из-за:
Подходы:
jest.useFakeTimers();
test('debounce работает корректно', () => {
instance.search('a');
jest.advanceTimersByTime(300);
expect(instance.resultsCount).toBeGreaterThanOrEqual(0);
});
Высокое покрытие не гарантирует качество, поэтому дополнительно анализируются:
Особое внимание уделяется путям:
В CI-процессе проверяются пороги:
Пример GitHub Actions:
- name: Run tests
run: npm test -- --coverage
- name: Check coverage
run: npx nyc check-coverage --lines 90 --branches 85