/** * Tests for data validation logic. * VSDD: Red phase — these tests define the spec. */ import { describe, it, expect } from 'vitest'; import { parseListItems, validateCell, getDropdownItems } from '../src/sheets/data-validation.js'; describe('parseListItems', () => { it('splits comma-separated string into trimmed items', () => { expect(parseListItems('Red, Green, Blue')).toEqual(['Red', 'Green', 'Blue']); }); it('handles no spaces around commas', () => { expect(parseListItems('A,B,C')).toEqual(['A', 'B', 'C']); }); it('filters out empty items from trailing commas', () => { expect(parseListItems('A,B,')).toEqual(['A', 'B']); }); it('returns empty array for empty string', () => { expect(parseListItems('')).toEqual([]); }); it('returns empty array for null', () => { expect(parseListItems(null)).toEqual([]); }); it('returns empty array for undefined', () => { expect(parseListItems(undefined)).toEqual([]); }); it('handles single item', () => { expect(parseListItems('Only')).toEqual(['Only']); }); it('trims whitespace from items', () => { expect(parseListItems(' A , B ')).toEqual(['A', 'B']); }); }); describe('validateCell', () => { describe('list validation', () => { const listRule = { type: 'list', value: 'Red, Green, Blue' }; it('accepts valid list item', () => { expect(validateCell('Red', listRule)).toEqual({ valid: true }); }); it('accepts valid list item case-insensitively', () => { expect(validateCell('red', listRule)).toEqual({ valid: true }); expect(validateCell('RED', listRule)).toEqual({ valid: true }); }); it('rejects invalid value', () => { const result = validateCell('Yellow', listRule); expect(result.valid).toBe(false); expect(result.message).toContain('Red'); }); it('allows empty cell', () => { expect(validateCell('', listRule)).toEqual({ valid: true }); expect(validateCell(null, listRule)).toEqual({ valid: true }); expect(validateCell(undefined, listRule)).toEqual({ valid: true }); }); it('works with items array instead of value string', () => { const rule = { type: 'list', items: ['Yes', 'No', 'Maybe'] }; expect(validateCell('Yes', rule)).toEqual({ valid: true }); expect(validateCell('perhaps', rule).valid).toBe(false); }); it('valid when list is empty', () => { expect(validateCell('anything', { type: 'list', value: '' })).toEqual({ valid: true }); }); }); describe('numberBetween validation', () => { const numRule = { type: 'numberBetween', value: '1', value2: '100' }; it('accepts number within range', () => { expect(validateCell(50, numRule)).toEqual({ valid: true }); }); it('accepts value at minimum', () => { expect(validateCell(1, numRule)).toEqual({ valid: true }); }); it('accepts value at maximum', () => { expect(validateCell(100, numRule)).toEqual({ valid: true }); }); it('rejects number below minimum', () => { const result = validateCell(0, numRule); expect(result.valid).toBe(false); expect(result.message).toContain('between'); }); it('rejects number above maximum', () => { const result = validateCell(101, numRule); expect(result.valid).toBe(false); }); it('rejects non-numeric input', () => { const result = validateCell('abc', numRule); expect(result.valid).toBe(false); expect(result.message).toContain('number'); }); it('allows empty cell', () => { expect(validateCell('', numRule)).toEqual({ valid: true }); }); it('accepts string number', () => { expect(validateCell('50', numRule)).toEqual({ valid: true }); }); it('handles reversed min/max', () => { const reversed = { type: 'numberBetween', value: '100', value2: '1' }; expect(validateCell(50, reversed)).toEqual({ valid: true }); }); }); describe('textLength validation', () => { const textRule = { type: 'textLength', value: '3', value2: '10' }; it('accepts text within length range', () => { expect(validateCell('Hello', textRule)).toEqual({ valid: true }); }); it('accepts text at minimum length', () => { expect(validateCell('abc', textRule)).toEqual({ valid: true }); }); it('accepts text at maximum length', () => { expect(validateCell('0123456789', textRule)).toEqual({ valid: true }); }); it('rejects text below minimum length', () => { const result = validateCell('ab', textRule); expect(result.valid).toBe(false); expect(result.message).toContain('length'); }); it('rejects text above maximum length', () => { const result = validateCell('this is too long', textRule); expect(result.valid).toBe(false); }); it('allows empty cell', () => { expect(validateCell('', textRule)).toEqual({ valid: true }); }); it('converts numbers to string for length check', () => { expect(validateCell(12345, textRule)).toEqual({ valid: true }); }); }); describe('edge cases', () => { it('returns valid for null rule', () => { expect(validateCell('test', null)).toEqual({ valid: true }); }); it('returns valid for undefined rule', () => { expect(validateCell('test', undefined)).toEqual({ valid: true }); }); it('returns valid for rule without type', () => { expect(validateCell('test', {})).toEqual({ valid: true }); }); it('returns valid for unknown rule type', () => { expect(validateCell('test', { type: 'unknownType' })).toEqual({ valid: true }); }); }); }); describe('getDropdownItems', () => { it('returns items from value string', () => { expect(getDropdownItems({ type: 'list', value: 'A, B, C' })).toEqual(['A', 'B', 'C']); }); it('returns items array directly if provided', () => { const items = ['X', 'Y', 'Z']; expect(getDropdownItems({ type: 'list', items })).toEqual(items); }); it('returns empty array for non-list type', () => { expect(getDropdownItems({ type: 'numberBetween', value: '1' })).toEqual([]); }); it('returns empty array for null rule', () => { expect(getDropdownItems(null)).toEqual([]); }); it('returns empty array for undefined rule', () => { expect(getDropdownItems(undefined)).toEqual([]); }); });