import { describe, expect, it } from 'vitest'; import { groupHasErrors, normalizeGroup, ruleError, topGroupAllNegative } from './validation'; import type { Group, Rule } from './types'; const r = (o: Partial): Rule => ({ field: 'title', operator: 'is', value: 'x', ...o }); describe('ruleError', () => { it('flags between with a missing upper bound', () => { expect(ruleError(r({ field: 'minutes', operator: 'between', value: '30', value2: '' }))).toBeTruthy(); expect(ruleError(r({ field: 'minutes', operator: 'between', value: '', value2: '60' }))).toBeTruthy(); expect(ruleError(r({ field: 'minutes', operator: 'between', value: '30', value2: '60' }))).toBeNull(); }); it('flags empty text values', () => { for (const operator of ['is', 'isNot', 'contains', 'startsWith'] as const) { expect(ruleError(r({ operator, value: '' }))).toBeTruthy(); expect(ruleError(r({ operator, value: 'x' }))).toBeNull(); } }); it('flags relative-date rule with a non-positive or non-integer N', () => { expect(ruleError(r({ field: 'release_date', operator: 'inLast', value: '', unit: 'day' }))).toBeTruthy(); expect(ruleError(r({ field: 'release_date', operator: 'inLast', value: '0', unit: 'day' }))).toBeTruthy(); expect(ruleError(r({ field: 'release_date', operator: 'inLast', value: '7', unit: 'day' }))).toBeNull(); }); it('flags relative-date rule with decimal notation N', () => { expect(ruleError(r({ field: 'release_date', operator: 'inLast', value: '7.0', unit: 'day' }))).toBeTruthy(); }); it('flags relative-date rule with scientific notation N', () => { expect(ruleError(r({ field: 'release_date', operator: 'inLast', value: '1e2', unit: 'day' }))).toBeTruthy(); }); }); describe('groupHasErrors', () => { it('walks nested groups', () => { const g: Group = { match: 'all', children: [{ match: 'any', children: [r({ value: '' })] }] }; expect(groupHasErrors(g)).toBe(true); expect(groupHasErrors({ match: 'all', children: [r({ value: 'ok' })] })).toBe(false); }); }); describe('topGroupAllNegative', () => { it('is true only when every top-level child is a negative rule', () => { expect(topGroupAllNegative({ match: 'any', children: [r({ operator: 'isNot' }), r({ operator: 'notMatches', field: 'plot' })] })).toBe(true); expect(topGroupAllNegative({ match: 'any', children: [r({ operator: 'isNot' }), r({ operator: 'is' })] })).toBe(false); expect(topGroupAllNegative({ match: 'all', children: [] })).toBe(false); }); }); describe('normalizeGroup', () => { it("coerces a single-child group's match to 'all'", () => { const g: Group = { match: 'any', children: [r({})] }; expect(normalizeGroup(g).match).toBe('all'); }); it('leaves a 2+ child group match unchanged and recurses', () => { const g: Group = { match: 'any', children: [r({}), { match: 'any', children: [r({})] }] }; const n = normalizeGroup(g); expect(n.match).toBe('any'); expect((n.children[1] as Group).match).toBe('all'); }); });