Юнит-тестирование компонентов с Cleave.js

Особенности поведения форматируемых input-полей

Компоненты, использующие Cleave.js, изменяют поведение стандартного текстового ввода: значение поля преобразуется в процессе ввода, добавляются разделители, маски, префиксы и постфиксы. Это приводит к ключевой сложности при юнит-тестировании — тестируется не только логика компонента, но и промежуточное состояние DOM-элемента.

Основные характеристики, влияющие на тестирование:

  • изменение value в момент input события;
  • синхронная трансформация строки;
  • вмешательство в нативное поведение input;
  • зависимость от DOM-среды (не чистая функция);
  • различие между отображаемым значением и «сырой» моделью данных.

Из-за этого классические unit-тесты «вызвал функцию — проверил результат» недостаточны.


Выбор тестового окружения

На практике используется комбинация:

  • Jest или Vitest — тест-раннер
  • jsdom — эмуляция DOM
  • Testing Library — взаимодействие с компонентами

jsdom критичен, так как Cleave.js работает с DOM напрямую.

Пример базовой конфигурации (Jest):

module.exports = {
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["@testing-library/jest-dom"],
};

Базовая стратегия тестирования input-форматирования

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

  1. Проверка инициализации
  2. Проверка реакции на ввод
  3. Проверка синхронизации состояния (state ↔︎ DOM)

Тестирование инициализации Cleave-инстанса

При использовании Cleave.js важно убедиться, что инстанс создаётся корректно.

Пример компонента (упрощённый):

import Cleave from "cleave.js";

export function createInput(el) {
  return new Cleave(el, {
    numeral: true,
    numeralThousandsGroupStyle: "thousand",
  });
}

Тест:

import { createInput } from "./createInput";

test("инициализация Cleave-инстанса", () => {
  document.body.innerHTML = `<input id="num" />`;
  const el = document.getElementById("num");

  const instance = createInput(el);

  expect(instance).toBeDefined();
  expect(instance.properties.numeral).toBe(true);
});

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

  • создание экземпляра
  • сохранение конфигурации
  • привязка к DOM-элементу

Тестирование форматирования ввода

Основной сценарий — проверка трансформации значения.

import Cleave from "cleave.js";

test("форматирование чисел с разделителями", () => {
  document.body.innerHTML = `<input id="num" />`;
  const el = document.getElementById("num");

  const cleave = new Cleave(el, {
    numeral: true,
    numeralThousandsGroupStyle: "thousand",
  });

  el.value = "1000000";
  el.dispatchEvent(new Event("input", { bubbles: true }));

  expect(el.value).toBe("1,000,000");
});

Ключевой момент: изменение происходит через событие input, а не прямой вызов метода.


Симуляция пользовательского ввода

В тестах важно имитировать поведение пользователя, а не напрямую присваивать value.

Корректный подход:

function setInputValue(input, value) {
  input.value = value;
  input.dispatchEvent(new Event("input", { bubbles: true }));
}

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

test("ввод форматируется в реальном времени", () => {
  document.body.innerHTML = `<input id="price" />`;
  const el = document.getElementById("price");

  new Cleave(el, {
    numeral: true,
    numeralDecimalMark: ".",
  });

  setInputValue(el, "12345");

  expect(el.value).toBe("12,345");
});

Проверка масок (phone, date, custom)

Cleave.js часто используется для масок:

  • телефон
  • дата
  • кредитные карты

Пример теста для телефона:

test("маска телефона", () => {
  document.body.innerHTML = `<input id="phone" />`;
  const el = document.getElementById("phone");

  new Cleave(el, {
    phone: true,
    phoneRegionCode: "RU",
  });

  setInputValue(el, "79991234567");

  expect(el.value).toContain("+7");
});

Особенность: формат может зависеть от региона, поэтому тесты должны учитывать частичное совпадение (toContain, regex), а не строгое равенство.


Тестирование React-компонентов с Cleave.js

При использовании React обычно применяется обёртка над input.

Пример:

import Cleave from "cleave.js";
import { useEffect, useRef } from "react";

export function PriceInput() {
  const ref = useRef(null);

  useEffect(() => {
    new Cleave(ref.current, {
      numeral: true,
    });
  }, []);

  return <input ref={ref} />;
}

Тест с React Testing Library:

import { render } from "@testing-library/react";
import { PriceInput } from "./PriceInput";

test("инициализация Cleave в React компоненте", () => {
  const { container } = render(<PriceInput />);
  const input = container.querySelector("input");

  input.value = "5000";
  input.dispatchEvent(new Event("input", { bubbles: true }));

  expect(input.value).toBe("5,000");
});

Тестирование Vue-компонентов

Во Vue интеграция обычно происходит через директиву или lifecycle hooks.

export default {
  mounted() {
    this.cleave = new Cleave(this.$refs.input, {
      date: true,
      datePattern: ["d", "m", "Y"],
    });
  },
};

Тест:

import { mount } from "@vue/test-utils";
import Component from "./Component.vue";

test("формат даты через Cleave", async () => {
  const wrapper = mount(Component);
  const input = wrapper.find("input");

  input.element.value = "01012025";
  await input.trigger("input");

  expect(input.element.value).toMatch(/01/);
});

Моки и изоляция Cleave-инстанса

Иногда требуется изолировать тесты от реального поведения Cleave.js.

Используется мокирование:

jest.mock("cleave.js", () => {
  return jest.fn().mockImplementation(() => ({
    setRawValue: jest.fn(),
    getRawValue: () => "mocked",
  }));
});

Это позволяет тестировать только логику компонента без реального форматирования.


Тестирование raw value vs formatted value

Одна из ключевых проблем — различие между:

  • el.value (форматированное значение)
  • raw value (исходные данные)

При тестировании важно фиксировать оба состояния:

test("raw и formatted значения", () => {
  document.body.innerHTML = `<input id="money" />`;
  const el = document.getElementById("money");

  const cleave = new Cleave(el, {
    numeral: true,
  });

  setInputValue(el, "1000");

  expect(el.value).toBe("1,000");
  expect(cleave.getRawValue()).toBe("1000");
});

Асинхронные нюансы и debounce-эффекты

Хотя Cleave.js работает синхронно, в реальных приложениях поверх него часто добавляется:

  • debounce
  • state management (Redux, Vuex, Pinia)
  • async validation

Тестирование таких цепочек требует waitFor:

import { waitFor } from "@testing-library/dom";

test("обновление состояния после ввода", async () => {
  document.body.innerHTML = `<input id="field" />`;
  const el = document.getElementById("field");

  new Cleave(el, { numeral: true });

  setInputValue(el, "9999");

  await waitFor(() => {
    expect(el.value).toBe("9,999");
  });
});

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

При работе с форматированными input-компонентами возникают повторяющиеся ошибки:

  • попытка тестировать через change вместо input
  • прямое присваивание value без события
  • отсутствие jsdom
  • сравнение строк без учёта форматирования
  • утечка состояния между тестами из-за глобальных инстансов

Организация тестовой структуры

Для крупных проектов с Cleave.js рационально разделять тесты:

  • unit: проверка инстанса и форматирования
  • integration: React/Vue компоненты
  • e2e: Cypress/Playwright сценарии ввода

Пример структуры:

/tests
  /unit
    cleave-init.test.js
    formatting.test.js
  /integration
    react-input.test.jsx
  /e2e
    input-flow.spec.js

Изоляция DOM-состояния между тестами

Поскольку Cleave мутирует DOM-элементы, важно очищать окружение:

afterEach(() => {
  document.body.innerHTML = "";
});

Без этого тесты начинают влиять друг на друга через сохранённые инстансы и обработчики событий.