E2E тестирование

При разработке интерфейсов с кастомными элементами формы значительная часть логики переносится из стандартных HTML-компонентов в JavaScript-слой. В случае использования Choices.js поведение <select> заменяется сложным DOM-компонентом с поиском, мультивыбором, тегами, асинхронной подгрузкой данных и кастомной отрисовкой. Это делает E2E тестирование ключевым уровнем проверки корректности работы интерфейса.

Особенности DOM-модели Choices.js с точки зрения E2E

Choices.js не работает как нативный <select> после инициализации. Вместо него создаётся структура DOM:

  • контейнер .choices
  • скрытый оригинальный <select>
  • инпут поиска .choices__input
  • список опций .choices__list--dropdown
  • выбранные элементы .choices__item

Такое преобразование влияет на стратегию тестирования:

  • взаимодействие происходит через кастомные элементы
  • события нативного sel ect не всегда применимы
  • проверка состояния требует анализа DOM, а не value у select напрямую

Критический аспект — синхронизация состояния между скрытым <select> и визуальным слоем.

Базовые сценарии E2E для Choices.js

Типовой набор сценариев включает:

Инициализация компонента

Проверяется:

  • наличие контейнера .choices
  • корректная отрисовка списка опций
  • соответствие исходным <option>

Пример логики проверки:

cy.get('.choices').should('exist');
cy.get('.choices__list--single').should('exist');
cy.get('select').find('option').should('have.length.greaterThan', 0);

Открытие dropdown

Поведение раскрытия списка опций является одним из ключевых состояний:

  • клик по контейнеру .choices
  • появление .choices__list--dropdown
  • корректная визуализация опций
cy.get('.choices').click();
cy.get('.choices__list--dropdown').should('be.visible');

Выбор значения

После клика по опции:

  • обновляется UI
  • изменяется состояние hidden select
  • появляется выбранный элемент
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

Фреймворк Cypress часто применяется благодаря удобной работе с DOM и автоматической синхронизации состояния.

Проверка поиска внутри Choices.js

При включённом поиске появляется 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']);
});

E2E тестирование с Playwright

Альтернативный подход реализуется через 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 позволяет более точно управлять ожиданиями:

  • waitForSelector
  • expect(locator).toBeVisible()
  • автоматическое ожидание состояния DOM

Асинхронные данные и API-интеграция

Choices.js часто используется с удалёнными источниками данных через fetch.

Подмена API в Cypress

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');

Подмена API в Playwright

await page.route('/api/options', route =>
  route.fulfill({
    body: JSON.stringify([
      { value: '1', label: 'Alpha' }
    ])
  })
);

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

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

Ключевая особенность Choices.js — скрытый <select> остаётся источником истины.

Проверки включают:

  • value синхронизирован с UI
  • наличие selected attributes
  • корректная сериализация формы
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');

Ошибка API

cy.intercept('/api/options', {
  statusCode: 500
}).as('error');

cy.visit('/form');

cy.get('.choices__list--dropdown')
  .should('not.exist');

Тестирование доступности (a11y)

Choices.js не всегда автоматически обеспечивает полную доступность, поэтому проверяются:

  • aria-expanded
  • aria-controls
  • keyboard navigation
cy.get('.choices').focus().type('{downarrow}');
cy.get('.choices__list--dropdown').should('be.visible');

Клавиатурная навигация:

  • ArrowDown / ArrowUp — перемещение
  • Enter — выбор
  • Esc — закрытие

Стабильность E2E тестов и борьба с флаками

Основные источники нестабильности:

  • асинхронная инициализация Choices.js
  • задержки API
  • анимации dropdown
  • перерисовка DOM

Рекомендуемые стратегии:

  • ожидание .choices перед действиями
  • отключение анимаций в тестовой среде
  • использование deterministic fixtures
  • минимизация зависимости от времени
cy.get('.choices', { timeout: 10000 }).should('be.visible');

Структурирование тестового покрытия

E2E покрытие обычно делится на уровни:

UI уровень

  • открытие/закрытие dropdown
  • отображение списка
  • визуальные состояния

State уровень

  • синхронизация select
  • multiple values
  • reset формы

Integration уровень

  • API загрузка
  • отправка формы
  • обработка ошибок

CI интеграция

В CI средах E2E тесты выполняются в headless режиме. Важно учитывать:

  • фиксированное окно браузера
  • отключение анимаций
  • стабильные фикстуры данных

Пример pipeline:

- run: npm install
- run: npm run build
- run: npm run test:e2e

Распараллеливание тестов критично для больших форм с Choices.js в нескольких местах.

Множественные экземпляры Choices.js на странице

При наличии нескольких селектов важно избегать глобальных селекторов:

cy.get('.form-group').eq(0).within(() => {
  cy.get('.choices').click();
  cy.get('.choices__item--choice').first().click();
});

Такой подход исключает конфликты между компонентами.

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

  • обращение к select.value до синхронизации UI
  • попытка клика по скрытым option
  • игнорирование async загрузки
  • отсутствие ожиданий .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
  • корректная привязка событий