Юнит-тестирование масок

Юнит-тестирование масок ввода в контексте Inputmask требует понимания того, что сама маска представляет собой слой трансформации между «сырыми» пользовательскими данными и нормализованным значением, которое хранится в модели данных. В отличие от классической логики функций, маска затрагивает события DOM, форматирование строки, позиционирование курсора и частично поведение браузера, поэтому тестирование должно учитывать не только результат, но и промежуточные состояния.

Архитектура тестируемого поведения Inputmask

Основная сложность тестирования Inputmask заключается в том, что маска работает на нескольких уровнях:

  • уровень преобразования строки (форматирование/очистка)
  • уровень обработки событий ввода (keydown, input, paste)
  • уровень управления кареткой (caret position)
  • уровень валидации символов по шаблону
  • уровень маскированного отображения значения

Юнит-тесты должны разделять эти уровни, иначе тесты становятся нестабильными и зависят от DOM-реализации.

Ключевой принцип: тестировать не библиотеку целиком, а поведение маски как чистую функцию, когда это возможно.


Изоляция логики маскирования

Для тестирования удобно выделять «чистую» часть логики Inputmask — преобразование входной строки в форматированную строку:

import Inputmask from "inputmask";

const mask = new Inputmask("999-999");

function format(value) {
  const el = document.createElement("input");
  mask.mask(el);
  el.value = value;
  el.dispatchEvent(new Event("input"));
  return el.value;
}

Такой подход позволяет тестировать преобразование без сложной симуляции событий.


Базовые сценарии проверки

Юнит-тестирование масок обычно покрывает следующие категории поведения:

  1. корректное форматирование валидного ввода
  2. отклонение недопустимых символов
  3. автоподстановка разделителей
  4. поведение при частичном вводе
  5. очистка значения при reset
  6. обработка вставки (paste)
  7. поведение при удалении символов

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


Тестирование простых масок

Простейший случай — числовая маска фиксированной длины:

describe("Inputmask 999-999", () => {
  it("форматирует число с дефисом", () => {
    const input = document.createElement("input");
    const mask = new Inputmask("999-999");

    mask.mask(input);

    input.value = "123456";
    input.dispatchEvent(new Event("input"));

    expect(input.value).toBe("123-456");
  });
});

В таких тестах важно учитывать, что Inputmask может реагировать только на события, а прямое присвоение value без dispatchEvent часто не вызывает обработчики.


Проверка фильтрации символов

Маска должна блокировать символы, не соответствующие шаблону:

it("игнорирует буквы в числовой маске", () => {
  const input = document.createElement("input");
  const mask = new Inputmask("999-999");

  mask.mask(input);

  input.value = "12a34b";
  input.dispatchEvent(new Event("input"));

  expect(input.value).toBe("123-4");
});

Здесь важно учитывать, что поведение может различаться в зависимости от конфигурации greedy, clearIncomplete, skipOptionalPartCharacter.


Тестирование вставки (paste event)

Вставка — один из самых нестабильных сценариев, поскольку Inputmask обрабатывает вставленные данные как отдельный поток:

it("корректно форматирует вставку", () => {
  const input = document.createElement("input");
  const mask = new Inputmask("(999) 999-9999");

  mask.mask(input);

  const pasteEvent = new Event("paste");
  Object.assign(pasteEvent, {
    clipboardData: {
      getData: () => "77011234567"
    }
  });

  input.dispatchEvent(pasteEvent);

  expect(input.value).toBe("(770) 112-3456");
});

В реальной среде часто требуется мокировать clipboardData, так как JSDOM не предоставляет полноценную реализацию.


Управление кареткой и его тестирование

Каретка является наиболее сложным аспектом тестирования, так как зависит от реализации браузера. Однако можно проверять логические ожидания:

it("ставит каретку после вставленного блока", () => {
  const input = document.createElement("input");
  const mask = new Inputmask("999-999");

  mask.mask(input);

  input.value = "123";
  input.setSelectionRange(3, 3);
  input.dispatchEvent(new Event("input"));

  const pos = input.selectionStart;

  expect(pos).toBeGreaterThanOrEqual(3);
});

Вместо точного значения часто проверяется диапазон допустимых позиций.


Использование Jest и JSDOM

Наиболее распространённая связка для тестирования Inputmask:

  • Jest как тест-раннер
  • JSDOM как эмуляция DOM

Типичная конфигурация:

module.exports = {
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["./jest.setup.js"]
};

В jest.setup.js часто добавляются polyfill-ы событий:

import "jest-canvas-mock";

global.InputEvent = window.InputEvent;

Моки событий DOM

Некоторые события требуют ручного создания:

function createInputEvent(value) {
  const event = new Event("input", { bubbles: true });
  event.simulated = true;
  return event;
}

Также часто требуется расширение Event для корректной работы старых окружений.


Проверка конфигурации маски

Inputmask поддерживает множество опций, каждая из которых должна тестироваться отдельно.

Пример тестирования clearIncomplete:

it("очищает незавершенное значение", () => {
  const input = document.createElement("input");
  const mask = new Inputmask("999-999", {
    clearIncomplete: true
  });

  mask.mask(input);

  input.value = "12";
  input.dispatchEvent(new Event("blur"));

  expect(input.value).toBe("");
});

Параметризованные тесты

Для проверки множества масок удобно использовать таблицы сценариев:

const cases = [
  ["999-999", "123456", "123-456"],
  ["(999) 999", "111222", "(111) 222"],
  ["99/99", "1224", "12/24"]
];

describe("маски форматирования", () => {
  test.each(cases)(
    "маска %s для %s",
    (maskPattern, inputValue, expected) => {
      const input = document.createElement("input");
      const mask = new Inputmask(maskPattern);

      mask.mask(input);

      input.value = inputValue;
      input.dispatchEvent(new Event("input"));

      expect(input.value).toBe(expected);
    }
  );
});

Тестирование edge cases

Особое внимание требуется следующим случаям:

  • пустая строка
  • только разделители
  • переполнение маски
  • частичная очистка
  • вставка с лишними символами
  • переключение маски на лету

Пример переполнения:

it("обрезает лишние символы", () => {
  const input = document.createElement("input");
  const mask = new Inputmask("999-999");

  mask.mask(input);

  input.value = "123456789";
  input.dispatchEvent(new Event("input"));

  expect(input.value).toBe("123-456");
});

Динамическое переключение маски

Inputmask позволяет менять маску во время выполнения, что требует отдельной проверки:

it("переключает маску без потери состояния", () => {
  const input = document.createElement("input");
  const mask = new Inputmask("999-999");

  mask.mask(input);

  input.value = "123456";
  input.dispatchEvent(new Event("input"));

  mask.option({ mask: "(999) 999" });

  expect(input.value.length).toBeGreaterThan(0);
});

Тестирование производительности в юнит-контексте

Хотя производительность чаще относится к интеграционным тестам, в юнит-тестах можно измерять базовую деградацию:

it("обрабатывает 1000 итераций без значимой задержки", () => {
  const input = document.createElement("input");
  const mask = new Inputmask("999-999");

  mask.mask(input);

  const start = performance.now();

  for (let i = 0; i < 1000; i++) {
    input.value = "123456";
    input.dispatchEvent(new Event("input"));
  }

  const end = performance.now();

  expect(end - start).toBeLessThan(200);
});

Изоляция зависимостей

Некоторые реализации Inputmask могут зависеть от глобальных объектов браузера. Для стабильных тестов требуется:

  • мок document.activeElement
  • стабилизация requestAnimationFrame
  • фиксация Date.now при необходимости
beforeAll(() => {
  jest.spyOn(Date, "now").mockReturnValue(0);
});

Типичные причины нестабильных тестов

  • отсутствие bubbles: true в событиях
  • несоответствие JSDOM реальному браузеру
  • некорректная инициализация маски до установки value
  • тестирование через прямое присваивание без событий
  • асинхронная обработка Inputmask без ожидания цикла событий

Стратегия покрытия

Оптимальная стратегия тестирования Inputmask строится на слоях:

  • слой чистой логики форматирования
  • слой DOM-интеграции
  • слой событий
  • слой edge-case сценариев
  • слой конфигурационных параметров

Разделение слоев снижает хрупкость тестов и упрощает поддержку при обновлениях библиотеки.