- validation.ts: ruleError now uses /^\d+$/ regex instead of Number.isInteger - dateMacro.ts: isValidN now uses /^\d+$/ regex instead of Number.isInteger - Prevents accepting scientific/decimal notation (e.g. '1e2', '7.0') that break compile↔parse contract - Added tests for both invalid cases returning null/error Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
60 lines
3.0 KiB
TypeScript
60 lines
3.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { groupHasErrors, normalizeGroup, ruleError, topGroupAllNegative } from './validation';
|
|
import type { Group, Rule } from './types';
|
|
|
|
const r = (o: Partial<Rule>): 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');
|
|
});
|
|
});
|