При разработке интерфейсов с кастомными элементами формы значительная
часть логики переносится из стандартных HTML-компонентов в
JavaScript-слой. В случае использования Choices.js поведение
<select> заменяется сложным DOM-компонентом с
поиском, мультивыбором, тегами, асинхронной подгрузкой данных и
кастомной отрисовкой. Это делает E2E тестирование ключевым уровнем
проверки корректности работы интерфейса.
Choices.js не работает как нативный <select> после
инициализации. Вместо него создаётся структура DOM:
.choices<select>.choices__input.choices__list--dropdown.choices__itemТакое преобразование влияет на стратегию тестирования:
Критический аспект — синхронизация состояния между скрытым
<select> и визуальным слоем.
Типовой набор сценариев включает:
Проверяется:
.choices<option>Пример логики проверки:
cy.get('.choices').should('exist');
cy.get('.choices__list--single').should('exist');
cy.get('select').find('option').should('have.length.greaterThan', 0);
Поведение раскрытия списка опций является одним из ключевых состояний:
.choices.choices__list--dropdowncy.get('.choices').click();
cy.get('.choices__list--dropdown').should('be.visible');
После клика по опции:
cy.get('.choices').click();
cy.get('.choices__item--choice').contains('Option 1').click();
cy.get('select').should('have.value', '1');
cy.get('.choices__item--selectable').should('contain', 'Option 1');
Фреймворк Cypress часто применяется благодаря удобной работе с DOM и автоматической синхронизации состояния.
При включённом поиске появляется input:
cy.get('.choices').click();
cy.get('.choices__input').type('app');
cy.get('.choices__list--dropdown')
.should('contain', 'Apple');
Особенность заключается в том, что фильтрация происходит на стороне JavaScript, поэтому тест проверяет не только DOM, но и реакцию библиотеки на ввод.
Choices.js поддерживает режим multiple, где значения превращаются в теги:
cy.get('.choices').click();
cy.get('.choices__item--choice').contains('Red').click();
cy.get('.choices__item--choice').contains('Blue').click();
cy.get('.choices__item--selectable').should('have.length', 2);
cy.get('select').then($select => {
const values = [...$select.val()];
expect(values).to.include.members(['red', 'blue']);
});
Альтернативный подход реализуется через Playwright, который предоставляет более строгую изоляцию браузерных контекстов и поддержку нескольких браузеров.
import { test, expect } fr om '@playwright/test';
test('select option in Choices.js', async ({ page }) => {
await page.goto('/form');
await page.click('.choices');
await page.click('.choices__item--choice:text("Option 1")');
const value = await page.locator('select').inputValue();
expect(value).toBe('1');
});
await page.click('.choices');
await page.fill('.choices__input', 'ban');
await expect(page.locator('.choices__list--dropdown'))
.toContainText('Banana');
Playwright позволяет более точно управлять ожиданиями:
waitForSelectorexpect(locator).toBeVisible()Choices.js часто используется с удалёнными источниками данных через
fetch.
cy.intercept('GET', '/api/options', {
body: [
{ value: '1', label: 'Alpha' },
{ value: '2', label: 'Beta' }
]
}).as('getOptions');
cy.visit('/form');
cy.wait('@getOptions');
cy.get('.choices__item--choice').should('contain', 'Alpha');
await page.route('/api/options', route =>
route.fulfill({
body: JSON.stringify([
{ value: '1', label: 'Alpha' }
])
})
);
Асинхронная подгрузка требует обязательной синхронизации тестов с сетевыми запросами, иначе возникают флаки-тесты.
Ключевая особенность Choices.js — скрытый <select>
остаётся источником истины.
Проверки включают:
cy.get('select').should('have.value', '2');
cy.get('select option:selected').should('have.text', 'Beta');
E2E тестирование должно включать негативные сценарии:
cy.intercept('/api/options', []).as('empty');
cy.visit('/form');
cy.get('.choices__list--dropdown')
.should('not.exist');
cy.intercept('/api/options', {
statusCode: 500
}).as('error');
cy.visit('/form');
cy.get('.choices__list--dropdown')
.should('not.exist');
Choices.js не всегда автоматически обеспечивает полную доступность, поэтому проверяются:
cy.get('.choices').focus().type('{downarrow}');
cy.get('.choices__list--dropdown').should('be.visible');
Клавиатурная навигация:
Основные источники нестабильности:
Рекомендуемые стратегии:
.choices перед действиямиcy.get('.choices', { timeout: 10000 }).should('be.visible');
E2E покрытие обычно делится на уровни:
В CI средах E2E тесты выполняются в headless режиме. Важно учитывать:
Пример pipeline:
- run: npm install
- run: npm run build
- run: npm run test:e2e
Распараллеливание тестов критично для больших форм с Choices.js в нескольких местах.
При наличии нескольких селектов важно избегать глобальных селекторов:
cy.get('.form-group').eq(0).within(() => {
cy.get('.choices').click();
cy.get('.choices__item--choice').first().click();
});
Такой подход исключает конфликты между компонентами.
select.value до синхронизации UI.choices__list--dropdownКорректная стратегия всегда опирается на DOM, который управляется библиотекой, а не на исходный HTML.
cy.get('.add-option').click();
cy.get('.choices__item--choice')
.should('contain', 'New option');
cy.get('.choices__button').click();
cy.get('select').should('have.value', '');
При re-render SPA важно проверять повторную инициализацию:
.choices