Автоматизация тестов

Тестирование библиотеки Choices.js опирается на понимание её архитектуры: работа с DOM-элементами, внутренним состоянием экземпляра, обработчиками событий и возможностью асинхронной подгрузки данных. Автоматизация тестов в таком контексте строится на нескольких уровнях: модульные проверки логики, интеграционные сценарии взаимодействия с DOM и end-to-end тестирование пользовательского поведения.

Базовая структура автоматизации обычно разделяется на три слоя:

  • Unit-тесты — проверка отдельных методов и функций без реального DOM или с минимальной его эмуляцией
  • Integration-тесты — проверка взаимодействия экземпляра Choices с DOM-деревом
  • E2E-тесты — симуляция поведения пользователя в браузере

Ключевая сложность заключается в том, что 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);
});

Асинхронные данные и remote API

При использовании 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();
});

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

Проверяются 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);
});

Интеграционное тестирование поведения DOM

Проверяется целостность 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);
});

End-to-End тестирование

Для 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);
});

CI-интеграция тестов

Типовая конфигурация 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();
});