Cross-field валидация в Vest применяется в ситуациях, когда корректность одного значения определяется другим значением внутри той же формы. Классические примеры включают проверку совпадения пароля и подтверждения, зависимость диапазонов дат, согласованность числовых границ, условную обязательность полей.
В основе подхода лежит возможность доступа к значениям других полей
внутри текущего прогона валидации. Vest предоставляет для этого механизм
ссылок на поля через field, а также композицию правил
внутри suite.
import { create, test, enforce, field } from 'vest';
const suite = create((data = {}) => {
test('password', 'Пароль обязателен', () => {
enforce(data.password).isNotEmpty();
});
test('confirm', 'Пароль не подтверждён', () => {
enforce(data.confirm).isNotEmpty();
});
test('confirm', 'Пароли не совпадают', () => {
enforce(data.confirm).equals(data.password);
});
});
Здесь второе и третье правило для confirm формируют
cross-field зависимость: значение подтверждения оценивается относительно
password.
field для явных зависимостейБолее декларативный подход строится на field,
позволяющем явно указывать зависимость между полями без обращения к
исходному объекту данных.
test('confirm', 'Пароли не совпадают', () => {
enforce(field('confirm')).equals(field('password'));
});
Такой стиль повышает читаемость и упрощает рефакторинг, поскольку логика сравнения отделяется от структуры входного объекта.
Cross-field логика часто выходит за рамки простого сравнения двух значений. Например, диапазон дат требует согласования нескольких полей одновременно.
test('endDate', 'Дата окончания должна быть позже начала', () => {
enforce(field('endDate')).isAfter(field('startDate'));
});
При этом возможны цепочки зависимостей, где одно поле влияет на несколько других правил:
test('discount', 'Скидка не может превышать цену', () => {
enforce(field('discount')).lessThanOrEquals(field('price'));
});
test('total', 'Итоговая сумма некорректна', () => {
enforce(field('total'))
.equals(field('price').minus(field('discount')));
});
Зависимость между полями часто сопровождается условностью. Vest
предоставляет механизм when, позволяющий активировать
проверку только при выполнении заданного условия.
test('state', 'Штат обязателен для выбранной страны', () => {
when(field('country')).equals('US', () => {
enforce(field('state')).isNotEmpty();
});
});
Такой подход предотвращает выполнение избыточных проверок и позволяет формировать контекстно-зависимые правила.
skipWhenВ сценариях, где одна ошибка делает бессмысленными другие проверки,
применяется skipWhen.
test('confirm', 'Пароли не совпадают', () => {
skipWhen(!field('password'), () => {
enforce(field('confirm')).equals(field('password'));
});
});
Логика становится чувствительной к состоянию других полей, что уменьшает шум от каскадных ошибок.
Cross-field валидация часто используется для числовых интервалов: минимальные и максимальные значения должны быть согласованы.
test('minPrice', 'Минимальная цена не может быть больше максимальной', () => {
enforce(field('minPrice')).lessThanOrEquals(field('maxPrice'));
});
test('maxPrice', 'Максимальная цена должна быть не меньше минимальной', () => {
enforce(field('maxPrice')).greaterThanOrEquals(field('minPrice'));
});
Дублирование проверок в обе стороны обеспечивает устойчивость интерфейса при изменении любого из полей.
Для коллекций значений cross-field логика распространяется на элементы массива и их агрегаты.
test('items', 'Сумма элементов должна совпадать с total', () => {
const sum = field('items').reduce((acc, x) => acc + x.amount, 0);
enforce(field('total')).equals(sum);
});
Также применяется проверка уникальности относительно других полей:
test('primaryEmail', 'Email должен отличаться от резервного', () => {
enforce(field('primaryEmail')).notEquals(field('backupEmail'));
});
Cross-field валидация может включать вычисляемые значения, формируемые на основе нескольких полей одновременно.
test('age', 'Возраст не соответствует дате рождения', () => {
const birthDate = new Date(field('birthDate'));
const age = new Date().getFullYear() - birthDate.getFullYear();
enforce(field('age')).equals(age);
});
Такие проверки часто применяются для согласования клиентских вычислений и серверной логики.
В Vest порядок объявления тестов влияет на читаемость, но не ограничивает возможность cross-field взаимодействия. Однако логически связанные проверки обычно группируются рядом, чтобы упростить сопровождение:
test('password', 'Пароль обязателен', () => {
enforce(field('password')).isNotEmpty();
});
test('confirm', 'Подтверждение обязательно', () => {
enforce(field('confirm')).isNotEmpty();
});
test('confirm', 'Пароли должны совпадать', () => {
enforce(field('confirm')).equals(field('password'));
});
Комплексные формы рассматриваются как система инвариантов, где каждое поле участвует в нескольких связях одновременно. Cross-field валидация формирует граф зависимостей, в котором изменение одного значения может повлиять на набор ошибок сразу нескольких тестов.
Типичные инварианты:
test('startDate', 'Неверный диапазон дат', () => {
enforce(field('startDate')).lessThan(field('endDate'));
});
test('endDate', 'Неверный диапазон дат', () => {
enforce(field('endDate')).greaterThan(field('startDate'));
});
Для повышения предсказуемости поведения формы cross-field проверки
выделяются в отдельные test, не смешиваясь с локальной
валидацией одного поля. Такой подход снижает связанность правил и
упрощает диагностику ошибок.
test('confirm', 'Некорректное подтверждение', () => {
enforce(field('confirm')).equals(field('password'));
});
Изоляция также позволяет переиспользовать набор зависимостей при изменении структуры формы без переработки всех правил.
Cross-field логика чувствительна к промежуточным состояниям формы.
При частичном заполнении некоторые зависимости становятся временно
некорректными. Vest позволяет учитывать это через комбинирование условий
when и проверок на пустые значения.
test('confirm', 'Пароли не совпадают', () => {
when(field('password')).isNotEmpty(() => {
enforce(field('confirm')).equals(field('password'));
});
});
Такой подход предотвращает ложные ошибки при незавершённом вводе данных.