import { describe, it, expect } from 'vitest'; import { validateValue, defaultValues, groupDefs, type PropertyDef, } from '../src/model/properties'; const def = (partial: Partial & Pick): PropertyDef => ({ key: 'p', label: 'Prop', group: 'general', ...partial, }); describe('property validation', () => { it('accepts any string and coerces null to empty', () => { expect(validateValue(def({ type: 'string' }), 'hello')).toEqual({ ok: true, value: 'hello' }); expect(validateValue(def({ type: 'string' }), null).value).toBe(''); }); it('validates int: coercion, rounding, bounds', () => { expect(validateValue(def({ type: 'int' }), '42').value).toBe(42); expect(validateValue(def({ type: 'int' }), 3.7).value).toBe(4); expect(validateValue(def({ type: 'int', min: 0 }), -1).ok).toBe(false); expect(validateValue(def({ type: 'int', max: 10 }), 11).ok).toBe(false); expect(validateValue(def({ type: 'int' }), 'abc').ok).toBe(false); }); it('validates float with bounds', () => { expect(validateValue(def({ type: 'float' }), '3.14').value).toBeCloseTo(3.14); expect(validateValue(def({ type: 'float', min: 0, max: 100 }), 50).ok).toBe(true); expect(validateValue(def({ type: 'float', min: 0 }), '-0.5').ok).toBe(false); expect(validateValue(def({ type: 'float' }), Infinity).ok).toBe(false); }); it('validates enum against options', () => { const e = def({ type: 'enum', options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }] }); expect(validateValue(e, 'a').ok).toBe(true); expect(validateValue(e, 'c').ok).toBe(false); }); it('validates color as hex', () => { expect(validateValue(def({ type: 'color' }), '#ff0000').ok).toBe(true); expect(validateValue(def({ type: 'color' }), '#f00').ok).toBe(true); expect(validateValue(def({ type: 'color' }), 'red').ok).toBe(false); expect(validateValue(def({ type: 'color' }), '#zzz').ok).toBe(false); }); it('handles catalogueItem: nullable single id', () => { const c = def({ type: 'catalogueItem', catalogueId: 'materials' }); expect(validateValue(c, 'steel').value).toBe('steel'); expect(validateValue(c, null).value).toBeNull(); expect(validateValue(c, '').value).toBeNull(); }); it('handles catalogueItems: array of ids', () => { const c = def({ type: 'catalogueItems', catalogueId: 'insulation' }); expect(validateValue(c, ['a', 'b']).value).toEqual(['a', 'b']); expect(validateValue(c, null).value).toEqual([]); expect(validateValue(c, 'single').value).toEqual(['single']); }); it('validates listOfValues with numeric item types', () => { const l = def({ type: 'listOfValues', itemType: 'float' }); expect(validateValue(l, ['1.5', '2'] as never).value).toEqual([1.5, 2]); expect(validateValue(l, ['x'] as never).ok).toBe(false); const ls = def({ type: 'listOfValues', itemType: 'string' }); expect(validateValue(ls, ['a', 1] as never).value).toEqual(['a', '1']); }); }); describe('defaults and grouping', () => { it('produces sensible defaults per type', () => { const defs: PropertyDef[] = [ def({ key: 's', type: 'string' }), def({ key: 'i', type: 'int', min: 5 }), def({ key: 'e', type: 'enum', options: [{ value: 'x', label: 'X' }] }), def({ key: 'c', type: 'color' }), def({ key: 'ci', type: 'catalogueItem' }), def({ key: 'cis', type: 'catalogueItems' }), def({ key: 'l', type: 'listOfValues' }), def({ key: 'd', type: 'float', default: 9.5 }), ]; const v = defaultValues(defs); expect(v.s).toBe(''); expect(v.i).toBe(5); expect(v.e).toBe('x'); expect(v.c).toBe('#000000'); expect(v.ci).toBeNull(); expect(v.cis).toEqual([]); expect(v.l).toEqual([]); expect(v.d).toBe(9.5); }); it('groups defs in canonical order', () => { const defs: PropertyDef[] = [ def({ key: 'a', group: 'physics' }), def({ key: 'b', group: 'general' }), def({ key: 'c', group: 'presentation' }), def({ key: 'd', group: 'physics' }), ]; const groups = groupDefs(defs.map((d) => ({ ...d, type: 'string' as const }))); expect(groups.map((g) => g.group.id)).toEqual(['general', 'presentation', 'physics']); expect(groups[2].defs.map((d) => d.key)).toEqual(['a', 'd']); }); });