Компоненты, использующие Cleave.js, изменяют поведение стандартного текстового ввода: значение поля преобразуется в процессе ввода, добавляются разделители, маски, префиксы и постфиксы. Это приводит к ключевой сложности при юнит-тестировании — тестируется не только логика компонента, но и промежуточное состояние DOM-элемента.
Основные характеристики, влияющие на тестирование:
value в момент input
события;Из-за этого классические unit-тесты «вызвал функцию — проверил результат» недостаточны.
На практике используется комбинация:
jsdom критичен, так как Cleave.js работает с DOM напрямую.
Пример базовой конфигурации (Jest):
module.exports = {
testEnvironment: "jsdom",
setupFilesAfterEnv: ["@testing-library/jest-dom"],
};
Юнит-тесты для компонентов с форматированием обычно делятся на три уровня:
При использовании 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);
});
Проверяется:
Основной сценарий — проверка трансформации значения.
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");
});
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 обычно применяется обёртка над 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 интеграция обычно происходит через директиву или 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.js.
Используется мокирование:
jest.mock("cleave.js", () => {
return jest.fn().mockImplementation(() => ({
setRawValue: jest.fn(),
getRawValue: () => "mocked",
}));
});
Это позволяет тестировать только логику компонента без реального форматирования.
Одна из ключевых проблем — различие между:
el.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");
});
Хотя Cleave.js работает синхронно, в реальных приложениях поверх него часто добавляется:
Тестирование таких цепочек требует 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 вместо
inputvalue без событияДля крупных проектов с Cleave.js рационально разделять тесты:
Пример структуры:
/tests
/unit
cleave-init.test.js
formatting.test.js
/integration
react-input.test.jsx
/e2e
input-flow.spec.js
Поскольку Cleave мутирует DOM-элементы, важно очищать окружение:
afterEach(() => {
document.body.innerHTML = "";
});
Без этого тесты начинают влиять друг на друга через сохранённые инстансы и обработчики событий.