Тестирование библиотеки Choices.js опирается на понимание её архитектуры: работа с DOM-элементами, внутренним состоянием экземпляра, обработчиками событий и возможностью асинхронной подгрузки данных. Автоматизация тестов в таком контексте строится на нескольких уровнях: модульные проверки логики, интеграционные сценарии взаимодействия с DOM и end-to-end тестирование пользовательского поведения.
Базовая структура автоматизации обычно разделяется на три слоя:
Ключевая сложность заключается в том, что Choices.js активно манипулирует DOM, поэтому тестовая среда должна поддерживать jsdom или полноценный браузер.
Для unit и integration тестов чаще всего используется Jest с jsdom:
npm install --save-dev jest jest-environment-jsdom
Конфигурация:
{
"testEnvironment": "jsdom"
}
Инициализация базового HTML:
<sel ect id="test-select">
<option value="1">One</option>
<option value="2">Two</option>
</select>
Проверка корректного создания экземпляра и преобразования select-элемента:
import Choices fr om 'choices.js';
test('инициализация Choices преобразует sel ect', () => {
document.body.innerHTML = `
<select id="test-select">
<option value="1">One</option>
<option value="2">Two</option>
</select>
`;
const el = document.getElementById('test-select');
const instance = new Choices(el);
expect(instance.passedElement.element).toBe(el);
expect(document.querySelector('.choices')).not.toBeNull();
});
Основные операции состояния — добавление и удаление элементов:
test('добавление элемента через API', () => {
const el = document.createElement('select');
el.multiple = true;
document.body.appendChild(el);
const instance = new Choices(el);
instance.setChoiceByValue('1');
instance.setValue([{ value: '1', label: 'One', selected: true }]);
const items = instance.getValue(true);
expect(items).toContainEqual(
expect.objectContaining({ value: '1' })
);
});
Удаление:
test('удаление выбранного элемента', () => {
const el = document.createElement('select');
el.multiple = true;
document.body.appendChild(el);
const instance = new Choices(el);
instance.setChoiceByValue('1');
instance.removeActiveItems();
expect(instance.getValue(true)).toHaveLength(0);
});
Choices.js генерирует события (addItem,
removeItem, change). Их проверка критична для
интеграции.
test('событие addItem вызывается при добавлении', () => {
const el = document.createElement('select');
document.body.appendChild(el);
const instance = new Choices(el);
const handler = jest.fn();
el.addEventListener('addItem', handler);
instance.setChoiceByValue('1');
expect(handler).toHaveBeenCalled();
});
Поисковый механизм требует проверки фильтрации списка:
test('фильтрация списка по строке поиска', () => {
const el = document.createElement('select');
document.body.innerHTML = `
<select id="s">
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</select>
`;
const instance = new Choices(el);
instance.input.value = 'app';
instance._onSearch();
const items = document.querySelectorAll('.choices__item--choice');
expect(items.length).toBe(1);
});
При использовании choices.ajax или пользовательских
fetch-функций требуется мокирование сети:
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([
{ value: '1', label: 'Remote One' }
])
})
);
Тест:
test('загрузка удалённых данных', async () => {
const el = document.createElement('select');
document.body.appendChild(el);
const instance = new Choices(el, {
searchEnabled: true
});
await instance.setChoices(async () => {
const res = await fetch('/api/data');
const data = await res.json();
return data;
});
expect(fetch).toHaveBeenCalled();
});
Навигация через стрелки и Enter является частью UX-логики:
test('навигация клавишами', () => {
const el = document.createElement('select');
document.body.appendChild(el);
new Choices(el);
const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
document.dispatchEvent(event);
expect(document.querySelector('.is-highlighted')).not.toBeNull();
});
Проверяются ARIA-атрибуты и корректность роли элементов:
test('aria-атрибуты присутствуют', () => {
const el = document.createElement('select');
document.body.appendChild(el);
new Choices(el);
const container = document.querySelector('.choices');
expect(container.getAttribute('role')).toBe('combobox');
});
Дополнительно применяются инструменты вроде
axe-core:
import axe fr om 'axe-core';
test('доступность компонента', async () => {
document.body.innerHTML = `<select id="s"></select>`;
new Choices(document.getElementById('s'));
const results = await axe.run(document.body);
expect(results.violations.length).toBe(0);
});
Проверяется целостность UI после последовательности действий:
test('полный сценарий взаимодействия', () => {
document.body.innerHTML = `
<select id="s" multiple>
<option value="1">One</option>
</select>
`;
const instance = new Choices('#s');
instance.setChoiceByValue('1');
instance.removeActiveItems();
const chips = document.querySelectorAll('.choices__item--selectable');
expect(chips.length).toBe(0);
});
Для E2E используется Cypress или Playwright.
Пример Cypress:
describe('Choices UI', () => {
it('добавляет и удаляет элемент', () => {
cy.visit('/page');
cy.get('.choices__input').type('One');
cy.get('.choices__item--choice').first().click();
cy.get('.choices__item--selectable').should('have.length', 1);
cy.get('.choices__button').click();
cy.get('.choices__item--selectable').should('have.length', 0);
});
});
Playwright:
test('выбор элемента', async ({ page }) => {
await page.goto('/');
await page.fill('.choices__input', 'Banana');
await page.click('.choices__item--choice');
const items = await page.locator('.choices__item--selectable').count();
expect(items).toBe(1);
});
При интенсивной работе с DOM важно проверять отсутствие утечек:
test('cleanup после destroy', () => {
const el = document.createElement('select');
document.body.appendChild(el);
const instance = new Choices(el);
instance.destroy();
expect(document.querySelector('.choices')).toBeNull();
});
Для профилирования используется performance.memory (в
Chromium):
test('память не растёт при повторной инициализации', () => {
for (let i = 0; i < 50; i++) {
const el = document.createElement('select');
document.body.appendChild(el);
const instance = new Choices(el);
instance.destroy();
}
expect(true).toBe(true);
});
Типовая конфигурация GitHub Actions:
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm install
- run: npm test
Для упрощения тестирования создаётся фабрика:
export function createSelect(options = []) {
const el = document.createElement('select');
options.forEach(opt => {
const o = document.createElement('option');
o.value = opt.value;
o.textContent = opt.label;
el.appendChild(o);
});
document.body.appendChild(el);
return el;
}
Использование:
const el = createSelect([
{ value: '1', label: 'One' },
{ value: '2', label: 'Two' }
]);
Choices.js поддерживает множество опций, которые также подлежат проверке:
test('disabled search', () => {
const el = document.createElement('select');
document.body.appendChild(el);
const instance = new Choices(el, {
searchEnabled: false
});
expect(instance.config.searchEnabled).toBe(false);
});
test('повторная инициализация не ломает DOM', () => {
const el = document.createElement('select');
document.body.appendChild(el);
const first = new Choices(el);
first.destroy();
const second = new Choices(el);
expect(second).toBeDefined();
});