Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * Tests for data validation logic.
3 * VSDD: Red phase — these tests define the spec.
4 */
5import { describe, it, expect } from 'vitest';
6import { parseListItems, validateCell, getDropdownItems } from '../src/sheets/data-validation.js';
7
8describe('parseListItems', () => {
9 it('splits comma-separated string into trimmed items', () => {
10 expect(parseListItems('Red, Green, Blue')).toEqual(['Red', 'Green', 'Blue']);
11 });
12
13 it('handles no spaces around commas', () => {
14 expect(parseListItems('A,B,C')).toEqual(['A', 'B', 'C']);
15 });
16
17 it('filters out empty items from trailing commas', () => {
18 expect(parseListItems('A,B,')).toEqual(['A', 'B']);
19 });
20
21 it('returns empty array for empty string', () => {
22 expect(parseListItems('')).toEqual([]);
23 });
24
25 it('returns empty array for null', () => {
26 expect(parseListItems(null)).toEqual([]);
27 });
28
29 it('returns empty array for undefined', () => {
30 expect(parseListItems(undefined)).toEqual([]);
31 });
32
33 it('handles single item', () => {
34 expect(parseListItems('Only')).toEqual(['Only']);
35 });
36
37 it('trims whitespace from items', () => {
38 expect(parseListItems(' A , B ')).toEqual(['A', 'B']);
39 });
40});
41
42describe('validateCell', () => {
43 describe('list validation', () => {
44 const listRule = { type: 'list', value: 'Red, Green, Blue' };
45
46 it('accepts valid list item', () => {
47 expect(validateCell('Red', listRule)).toEqual({ valid: true });
48 });
49
50 it('accepts valid list item case-insensitively', () => {
51 expect(validateCell('red', listRule)).toEqual({ valid: true });
52 expect(validateCell('RED', listRule)).toEqual({ valid: true });
53 });
54
55 it('rejects invalid value', () => {
56 const result = validateCell('Yellow', listRule);
57 expect(result.valid).toBe(false);
58 expect(result.message).toContain('Red');
59 });
60
61 it('allows empty cell', () => {
62 expect(validateCell('', listRule)).toEqual({ valid: true });
63 expect(validateCell(null, listRule)).toEqual({ valid: true });
64 expect(validateCell(undefined, listRule)).toEqual({ valid: true });
65 });
66
67 it('works with items array instead of value string', () => {
68 const rule = { type: 'list', items: ['Yes', 'No', 'Maybe'] };
69 expect(validateCell('Yes', rule)).toEqual({ valid: true });
70 expect(validateCell('perhaps', rule).valid).toBe(false);
71 });
72
73 it('valid when list is empty', () => {
74 expect(validateCell('anything', { type: 'list', value: '' })).toEqual({ valid: true });
75 });
76 });
77
78 describe('numberBetween validation', () => {
79 const numRule = { type: 'numberBetween', value: '1', value2: '100' };
80
81 it('accepts number within range', () => {
82 expect(validateCell(50, numRule)).toEqual({ valid: true });
83 });
84
85 it('accepts value at minimum', () => {
86 expect(validateCell(1, numRule)).toEqual({ valid: true });
87 });
88
89 it('accepts value at maximum', () => {
90 expect(validateCell(100, numRule)).toEqual({ valid: true });
91 });
92
93 it('rejects number below minimum', () => {
94 const result = validateCell(0, numRule);
95 expect(result.valid).toBe(false);
96 expect(result.message).toContain('between');
97 });
98
99 it('rejects number above maximum', () => {
100 const result = validateCell(101, numRule);
101 expect(result.valid).toBe(false);
102 });
103
104 it('rejects non-numeric input', () => {
105 const result = validateCell('abc', numRule);
106 expect(result.valid).toBe(false);
107 expect(result.message).toContain('number');
108 });
109
110 it('allows empty cell', () => {
111 expect(validateCell('', numRule)).toEqual({ valid: true });
112 });
113
114 it('accepts string number', () => {
115 expect(validateCell('50', numRule)).toEqual({ valid: true });
116 });
117
118 it('handles reversed min/max', () => {
119 const reversed = { type: 'numberBetween', value: '100', value2: '1' };
120 expect(validateCell(50, reversed)).toEqual({ valid: true });
121 });
122 });
123
124 describe('textLength validation', () => {
125 const textRule = { type: 'textLength', value: '3', value2: '10' };
126
127 it('accepts text within length range', () => {
128 expect(validateCell('Hello', textRule)).toEqual({ valid: true });
129 });
130
131 it('accepts text at minimum length', () => {
132 expect(validateCell('abc', textRule)).toEqual({ valid: true });
133 });
134
135 it('accepts text at maximum length', () => {
136 expect(validateCell('0123456789', textRule)).toEqual({ valid: true });
137 });
138
139 it('rejects text below minimum length', () => {
140 const result = validateCell('ab', textRule);
141 expect(result.valid).toBe(false);
142 expect(result.message).toContain('length');
143 });
144
145 it('rejects text above maximum length', () => {
146 const result = validateCell('this is too long', textRule);
147 expect(result.valid).toBe(false);
148 });
149
150 it('allows empty cell', () => {
151 expect(validateCell('', textRule)).toEqual({ valid: true });
152 });
153
154 it('converts numbers to string for length check', () => {
155 expect(validateCell(12345, textRule)).toEqual({ valid: true });
156 });
157 });
158
159 describe('edge cases', () => {
160 it('returns valid for null rule', () => {
161 expect(validateCell('test', null)).toEqual({ valid: true });
162 });
163
164 it('returns valid for undefined rule', () => {
165 expect(validateCell('test', undefined)).toEqual({ valid: true });
166 });
167
168 it('returns valid for rule without type', () => {
169 expect(validateCell('test', {})).toEqual({ valid: true });
170 });
171
172 it('returns valid for unknown rule type', () => {
173 expect(validateCell('test', { type: 'unknownType' })).toEqual({ valid: true });
174 });
175 });
176});
177
178describe('getDropdownItems', () => {
179 it('returns items from value string', () => {
180 expect(getDropdownItems({ type: 'list', value: 'A, B, C' })).toEqual(['A', 'B', 'C']);
181 });
182
183 it('returns items array directly if provided', () => {
184 const items = ['X', 'Y', 'Z'];
185 expect(getDropdownItems({ type: 'list', items })).toEqual(items);
186 });
187
188 it('returns empty array for non-list type', () => {
189 expect(getDropdownItems({ type: 'numberBetween', value: '1' })).toEqual([]);
190 });
191
192 it('returns empty array for null rule', () => {
193 expect(getDropdownItems(null)).toEqual([]);
194 });
195
196 it('returns empty array for undefined rule', () => {
197 expect(getDropdownItems(undefined)).toEqual([]);
198 });
199});