Библиотека Cleave.js предназначена для форматирования пользовательского ввода в режиме реального времени. Несмотря на то что основной задачей библиотеки является визуальное представление данных, а не полноценная проверка значений, корректное тестирование валидации становится критически важным этапом разработки.
Форматирование и валидация — разные процессы:
Пример:
new Cleave('#card', {
creditCard: true
});
Поле автоматически разбивает номер карты на блоки:
4111 1111 1111 1111
Однако визуально корректный номер не означает:
Именно поэтому тестирование валидации должно охватывать:
Одной из ключевых особенностей Cleave.js является наличие двух представлений значения:
| Тип | Описание |
|---|---|
| formatted value | значение в поле |
| rawValue | очищенное значение |
Пример:
const cleave = new Cleave('#phone', {
phone: true,
phoneRegionCode: 'US'
});
Пользователь вводит:
(213) 373-4253
Внутреннее значение:
event.target.rawValue
содержит:
2133734253
Тестирование должно проверять оба значения одновременно.
test('formatted phone value', () => {
input.value = '2133734253';
input.dispatchEvent(new Event('input'));
expect(input.value).toBe('(213) 373-4253');
});
test('raw phone value', () => {
const event = {
target: {
rawValue: '2133734253'
}
};
expect(event.target.rawValue).toBe('2133734253');
});
test('credit card formatting', () => {
const cleave = new Cleave(input, {
creditCard: true
});
input.value = '4111111111111111';
input.dispatchEvent(new Event('input'));
expect(input.value).toBe('4111 1111 1111 1111');
});
test('raw card value', () => {
const cleave = new Cleave(input, {
creditCard: true
});
input.value = '4111 1111 1111 1111';
input.dispatchEvent(new Event('input'));
expect(cleave.getRawValue()).toBe('4111111111111111');
});
test('max card length', () => {
const cleave = new Cleave(input, {
creditCard: true
});
input.value = '41111111111111112222';
input.dispatchEvent(new Event('input'));
expect(cleave.getRawValue().length).toBeLessThanOrEqual(19);
});
Телефонные номера особенно сложны из-за:
test('kazakhstan phone format', () => {
const cleave = new Cleave(input, {
phone: true,
phoneRegionCode: 'KZ'
});
input.value = '7771234567';
input.dispatchEvent(new Event('input'));
expect(input.value).toContain('777');
});
test('phone raw value contains digits only', () => {
const cleave = new Cleave(input, {
phone: true,
phoneRegionCode: 'US'
});
input.value = '(213) 373-4253';
input.dispatchEvent(new Event('input'));
expect(cleave.getRawValue()).toMatch(/^\d+$/);
});
test('date formatting', () => {
const cleave = new Cleave(input, {
date: true,
datePattern: ['d', 'm', 'Y']
});
input.value = '25052026';
input.dispatchEvent(new Event('input'));
expect(input.value).toBe('25/05/2026');
});
Cleave.js не выполняет полноценную календарную валидацию.
Это означает, что строка:
39/19/2026
может быть успешно отформатирована.
Поэтому необходимы дополнительные тесты.
function isValidDate(value) {
const [day, month, year] = value.split('/').map(Number);
return (
day >= 1 &&
day <= 31 &&
month >= 1 &&
month <= 12
);
}
test('invalid date validation', () => {
expect(isValidDate('39/19/2026')).toBe(false);
});
test('numeral formatting', () => {
const cleave = new Cleave(input, {
numeral: true,
numeralThousandsGroupStyle: 'thousand'
});
input.value = '1000000';
input.dispatchEvent(new Event('input'));
expect(input.value).toBe('1,000,000');
});
test('decimal mark formatting', () => {
const cleave = new Cleave(input, {
numeral: true,
numeralDecimalMark: ','
});
input.value = '1234,56';
input.dispatchEvent(new Event('input'));
expect(input.value).toContain(',');
});
test('negative numbers support', () => {
const cleave = new Cleave(input, {
numeral: true,
numeralPositiveOnly: false
});
input.value = '-1000';
input.dispatchEvent(new Event('input'));
expect(input.value).toContain('-');
});
test('positive only restriction', () => {
const cleave = new Cleave(input, {
numeral: true,
numeralPositiveOnly: true
});
input.value = '-1000';
input.dispatchEvent(new Event('input'));
expect(input.value).not.toContain('-');
});
test('custom blocks formatting', () => {
const cleave = new Cleave(input, {
blocks: [4, 4, 4],
delimiter: '-'
});
input.value = '123456789012';
input.dispatchEvent(new Event('input'));
expect(input.value).toBe('1234-5678-9012');
});
test('block overflow', () => {
const cleave = new Cleave(input, {
blocks: [2, 2]
});
input.value = '123456';
input.dispatchEvent(new Event('input'));
expect(cleave.getRawValue().length).toBeLessThanOrEqual(4);
});
Вставка текста часто ломает форматирование сильнее обычного ввода.
test('paste credit card', () => {
const cleave = new Cleave(input, {
creditCard: true
});
input.value = '4111111111111111';
input.dispatchEvent(new Event('paste'));
expect(input.value).toBe('4111 1111 1111 1111');
});
test('paste invalid symbols', () => {
const cleave = new Cleave(input, {
numeral: true
});
input.value = '12abc34';
input.dispatchEvent(new Event('input'));
expect(cleave.getRawValue()).toBe('1234');
});
test('backspace handling', () => {
const cleave = new Cleave(input, {
creditCard: true
});
input.value = '4111 1111';
input.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Backspace'
}));
expect(input.value.length).toBeLessThanOrEqual(9);
});
Некорректная работа курсора — одна из самых частых проблем масок ввода.
test('cursor position after delimiter', () => {
const cleave = new Cleave(input, {
blocks: [4, 4],
delimiter: ' '
});
input.value = '12345';
input.dispatchEvent(new Event('input'));
expect(input.selectionStart).toBeGreaterThan(4);
});
test('onValueChanged callback', () => {
const handler = jest.fn();
const cleave = new Cleave(input, {
numeral: true,
onValueChanged: handler
});
input.value = '1000';
input.dispatchEvent(new Event('input'));
expect(handler).toHaveBeenCalled();
});
test('rawValue in callback', () => {
let raw = '';
const cleave = new Cleave(input, {
numeral: true,
onValueChanged: (e) => {
raw = e.target.rawValue;
}
});
input.value = '1000';
input.dispatchEvent(new Event('input'));
expect(raw).toBe('1000');
});
Cleave.js часто используется вместе с:
test('required field validation', () => {
input.required = true;
input.value = '';
expect(input.checkValidity()).toBe(false);
});
test('pattern validation', () => {
input.pattern = '\\d{4}';
input.value = '123';
expect(input.checkValidity()).toBe(false);
});
test('react controlled input', () => {
render(<CardInput />);
const input = screen.getByRole('textbox');
fireEvent.change(input, {
target: {
value: '4111111111111111'
}
});
expect(input.value).toContain(' ');
});
test('empty input', () => {
const cleave = new Cleave(input, {
numeral: true
});
input.value = '';
input.dispatchEvent(new Event('input'));
expect(cleave.getRawValue()).toBe('');
});
test('null value handling', () => {
expect(() => {
input.value = null;
}).not.toThrow();
});
test('very long input', () => {
const cleave = new Cleave(input, {
numeral: true
});
input.value = '1'.repeat(10000);
input.dispatchEvent(new Event('input'));
expect(cleave.getRawValue().length)
.toBeLessThanOrEqual(10000);
});
Большое количество форматирований может вызывать:
test('formatting performance', () => {
const start = performance.now();
for (let i = 0; i < 1000; i++) {
input.value = '4111111111111111';
input.dispatchEvent(new Event('input'));
}
const end = performance.now();
expect(end - start).toBeLessThan(100);
});
Неправильное уничтожение экземпляров Cleave.js способно вызывать утечки памяти.
test('destroy instance', () => {
const cleave = new Cleave(input, {
numeral: true
});
cleave.destroy();
expect(cleave.element).toBeNull();
});
Некоторые сценарии требуют проверки значения через API.
Пример:
async function validateCard(card) {
const response = await fetch('/validate-card', {
method: 'POST',
body: JSON.stringify({ card })
});
return response.json();
}
test('async card validation', async () => {
const result = await validateCard(
'4111111111111111'
);
expect(result.valid).toBe(true);
});
Snapshot-тесты помогают фиксировать состояние поля после форматирования.
test('snapshot formatted input', () => {
const cleave = new Cleave(input, {
creditCard: true
});
input.value = '4111111111111111';
input.dispatchEvent(new Event('input'));
expect(input.value).toMatchSnapshot();
});
Проверяют:
Проверяют:
Проверяют:
Ошибка:
expect(input.value).toBe('4111 1111 1111 1111');
Без проверки:
cleave.getRawValue()
Тест становится неполным.
Paste-сценарии часто работают иначе, чем обычный input.
Форматирование может быть корректным, но UX — полностью сломанным.
Мобильные клавиатуры:
Особенно важно для:
test('composition input', () => {
input.dispatchEvent(
new CompositionEvent('compositionstart')
);
input.value = '1234';
input.dispatchEvent(
new CompositionEvent('compositionend')
);
expect(input.value).toContain('1234');
});
Хорошей практикой является вынесение валидации отдельно от Cleave.js.
if (input.value.length === 19)
if (cleave.getRawValue().length === 16)
Иногда библиотеку необходимо мокировать.
jest.mock('cleave.js', () => {
return jest.fn().mockImplementation(() => ({
getRawValue: () => '4111111111111111',
destroy: jest.fn()
}));
});
Форматирование часто комбинируется с debounce-валидацией.
test('debounced validation', () => {
jest.useFakeTimers();
const validate = jest.fn();
input.addEventListener(
'input',
debounce(validate, 300)
);
input.dispatchEvent(new Event('input'));
jest.advanceTimersByTime(300);
expect(validate).toHaveBeenCalled();
});