Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 275 lines 9.4 kB view raw
1import { describe, it, expect } from 'vitest'; 2import { 3 extractFormulaRanges, 4 assignRangeColors, 5 RANGE_COLORS, 6} from '../src/sheets/range-highlight.js'; 7 8/** 9 * Tests for Range Highlighting While Editing (Chainlink #95). 10 * 11 * When editing a formula, referenced cells/ranges should be highlighted 12 * on the grid with colored borders/overlays. Each unique reference gets 13 * a color from a rotating palette. 14 */ 15 16describe('RANGE_COLORS', () => { 17 it('is an array of 6 colors', () => { 18 expect(Array.isArray(RANGE_COLORS)).toBe(true); 19 expect(RANGE_COLORS.length).toBe(6); 20 }); 21 22 it('each color is a string', () => { 23 for (const c of RANGE_COLORS) { 24 expect(typeof c).toBe('string'); 25 } 26 }); 27}); 28 29describe('extractFormulaRanges', () => { 30 it('is a function', () => { 31 expect(typeof extractFormulaRanges).toBe('function'); 32 }); 33 34 it('returns an array', () => { 35 const result = extractFormulaRanges('SUM(A1:B5)'); 36 expect(Array.isArray(result)).toBe(true); 37 }); 38 39 it('extracts a single cell reference', () => { 40 const result = extractFormulaRanges('A1+B1'); 41 expect(result.length).toBe(2); 42 expect(result[0].ref).toBe('A1'); 43 expect(result[1].ref).toBe('B1'); 44 }); 45 46 it('extracts a range reference', () => { 47 const result = extractFormulaRanges('SUM(A1:B5)'); 48 const range = result.find(r => r.ref === 'A1:B5'); 49 expect(range).toBeTruthy(); 50 }); 51 52 it('each result has ref, startIndex, and endIndex', () => { 53 const result = extractFormulaRanges('SUM(A1:B5)'); 54 for (const r of result) { 55 expect(r).toHaveProperty('ref'); 56 expect(r).toHaveProperty('startIndex'); 57 expect(r).toHaveProperty('endIndex'); 58 expect(typeof r.ref).toBe('string'); 59 expect(typeof r.startIndex).toBe('number'); 60 expect(typeof r.endIndex).toBe('number'); 61 } 62 }); 63 64 it('extracts absolute references', () => { 65 const result = extractFormulaRanges('$A$1+$B$2'); 66 expect(result.length).toBe(2); 67 expect(result[0].ref).toBe('$A$1'); 68 expect(result[1].ref).toBe('$B$2'); 69 }); 70 71 it('extracts cross-sheet references', () => { 72 const result = extractFormulaRanges('Sheet2!A1+B1'); 73 expect(result.some(r => r.ref === 'Sheet2!A1')).toBe(true); 74 }); 75 76 it('extracts multiple different references', () => { 77 const result = extractFormulaRanges('A1+B1+C1:D5'); 78 expect(result.length).toBe(3); 79 }); 80 81 it('returns positions that match the formula text', () => { 82 const formula = 'SUM(A1:B5)'; 83 const result = extractFormulaRanges(formula); 84 for (const r of result) { 85 expect(formula.substring(r.startIndex, r.endIndex)).toBe(r.ref); 86 } 87 }); 88 89 it('returns empty array for formulas with no references', () => { 90 const result = extractFormulaRanges('42+10'); 91 expect(result).toEqual([]); 92 }); 93 94 it('handles nested function calls', () => { 95 const result = extractFormulaRanges('IF(SUM(A1:A10)>100,B1,C1)'); 96 expect(result.length).toBe(3); 97 expect(result.some(r => r.ref === 'A1:A10')).toBe(true); 98 expect(result.some(r => r.ref === 'B1')).toBe(true); 99 expect(result.some(r => r.ref === 'C1')).toBe(true); 100 }); 101 102 it('handles quoted sheet names', () => { 103 const result = extractFormulaRanges("'My Sheet'!A1+B2"); 104 expect(result.some(r => r.ref.includes('My Sheet'))).toBe(true); 105 }); 106 107 it('does not extract function names as references', () => { 108 const result = extractFormulaRanges('SUM(A1)'); 109 expect(result.every(r => r.ref !== 'SUM')).toBe(true); 110 }); 111}); 112 113describe('assignRangeColors', () => { 114 it('is a function', () => { 115 expect(typeof assignRangeColors).toBe('function'); 116 }); 117 118 it('assigns a color to each range', () => { 119 const ranges = [ 120 { ref: 'A1', startIndex: 0, endIndex: 2 }, 121 { ref: 'B1', startIndex: 3, endIndex: 5 }, 122 ]; 123 const result = assignRangeColors(ranges); 124 expect(Array.isArray(result)).toBe(true); 125 expect(result.length).toBe(2); 126 for (const r of result) { 127 expect(r).toHaveProperty('color'); 128 expect(typeof r.color).toBe('string'); 129 } 130 }); 131 132 it('gives same ref same color', () => { 133 const ranges = [ 134 { ref: 'A1', startIndex: 0, endIndex: 2 }, 135 { ref: 'B1', startIndex: 3, endIndex: 5 }, 136 { ref: 'A1', startIndex: 6, endIndex: 8 }, 137 ]; 138 const result = assignRangeColors(ranges); 139 const a1Colors = result.filter(r => r.ref === 'A1').map(r => r.color); 140 expect(new Set(a1Colors).size).toBe(1); 141 }); 142 143 it('gives different refs different colors (up to palette size)', () => { 144 const ranges = [ 145 { ref: 'A1', startIndex: 0, endIndex: 2 }, 146 { ref: 'B1', startIndex: 3, endIndex: 5 }, 147 { ref: 'C1', startIndex: 6, endIndex: 8 }, 148 ]; 149 const result = assignRangeColors(ranges); 150 const colors = result.map(r => r.color); 151 expect(new Set(colors).size).toBe(3); 152 }); 153 154 it('cycles colors after palette is exhausted', () => { 155 const ranges = []; 156 const refs = ['A1', 'B1', 'C1', 'D1', 'E1', 'F1', 'G1', 'H1']; 157 for (let i = 0; i < refs.length; i++) { 158 ranges.push({ ref: refs[i], startIndex: i * 3, endIndex: i * 3 + 2 }); 159 } 160 const result = assignRangeColors(ranges); 161 // The 7th ref should cycle back to the 1st color 162 expect(result[6].color).toBe(result[0].color); 163 }); 164 165 it('preserves original ref, startIndex, endIndex', () => { 166 const ranges = [{ ref: 'A1', startIndex: 0, endIndex: 2 }]; 167 const result = assignRangeColors(ranges); 168 expect(result[0].ref).toBe('A1'); 169 expect(result[0].startIndex).toBe(0); 170 expect(result[0].endIndex).toBe(2); 171 }); 172 173 it('returns empty array for empty input', () => { 174 const result = assignRangeColors([]); 175 expect(result).toEqual([]); 176 }); 177}); 178 179// ===================================================================== 180// Edge cases 181// ===================================================================== 182 183describe('extractFormulaRanges — edge cases', () => { 184 it('handles empty string', () => { 185 expect(extractFormulaRanges('')).toEqual([]); 186 }); 187 188 it('handles formula with only numbers and operators', () => { 189 expect(extractFormulaRanges('1+2*3')).toEqual([]); 190 }); 191 192 it('handles string literals (should not extract refs inside strings)', () => { 193 // Strings in formulas are in quotes, but extractFormulaRanges operates on raw formula text 194 // The regex may pick up refs inside strings — testing actual behavior 195 const result = extractFormulaRanges('"A1"'); 196 // A1 inside quotes may or may not be extracted depending on implementation 197 expect(Array.isArray(result)).toBe(true); 198 }); 199 200 it('extracts mixed absolute and relative references', () => { 201 const result = extractFormulaRanges('$A1+B$2+$C$3'); 202 expect(result).toHaveLength(3); 203 expect(result[0].ref).toBe('$A1'); 204 expect(result[1].ref).toBe('B$2'); 205 expect(result[2].ref).toBe('$C$3'); 206 }); 207 208 it('extracts cross-sheet range references', () => { 209 const result = extractFormulaRanges('Sheet2!A1:B5'); 210 expect(result.some(r => r.ref.includes('Sheet2'))).toBe(true); 211 }); 212 213 it('handles multiple cross-sheet refs', () => { 214 const result = extractFormulaRanges('Sheet1!A1+Sheet2!B2'); 215 expect(result.length).toBeGreaterThanOrEqual(2); 216 }); 217 218 it('extracts large column refs like AA1, AZ99', () => { 219 const result = extractFormulaRanges('AA1+AZ99'); 220 expect(result.some(r => r.ref === 'AA1')).toBe(true); 221 expect(result.some(r => r.ref === 'AZ99')).toBe(true); 222 }); 223 224 it('handles a complex nested formula', () => { 225 const result = extractFormulaRanges('IF(AND(A1>0,B1<100),SUM(C1:C10),D1)'); 226 expect(result.some(r => r.ref === 'A1')).toBe(true); 227 expect(result.some(r => r.ref === 'B1')).toBe(true); 228 expect(result.some(r => r.ref === 'C1:C10')).toBe(true); 229 expect(result.some(r => r.ref === 'D1')).toBe(true); 230 }); 231 232 it('does not extract TRUE or FALSE as references', () => { 233 const result = extractFormulaRanges('IF(TRUE,A1,FALSE)'); 234 expect(result.every(r => r.ref !== 'TRUE' && r.ref !== 'FALSE')).toBe(true); 235 }); 236}); 237 238describe('assignRangeColors — edge cases', () => { 239 it('all 6 unique refs get distinct colors', () => { 240 const ranges = Array.from({ length: 6 }, (_, i) => ({ 241 ref: `${String.fromCharCode(65 + i)}1`, 242 startIndex: i * 3, 243 endIndex: i * 3 + 2, 244 })); 245 const result = assignRangeColors(ranges); 246 const colors = new Set(result.map(r => r.color)); 247 expect(colors.size).toBe(6); 248 }); 249 250 it('colors come from RANGE_COLORS palette', () => { 251 const ranges = [{ ref: 'A1', startIndex: 0, endIndex: 2 }]; 252 const result = assignRangeColors(ranges); 253 expect(RANGE_COLORS).toContain(result[0].color); 254 }); 255 256 it('handles single range', () => { 257 const result = assignRangeColors([{ ref: 'A1:B5', startIndex: 0, endIndex: 5 }]); 258 expect(result).toHaveLength(1); 259 expect(result[0].color).toBeTruthy(); 260 expect(result[0].ref).toBe('A1:B5'); 261 }); 262 263 it('duplicate refs in sequence share same color', () => { 264 const ranges = [ 265 { ref: 'A1', startIndex: 0, endIndex: 2 }, 266 { ref: 'B1', startIndex: 3, endIndex: 5 }, 267 { ref: 'A1', startIndex: 6, endIndex: 8 }, 268 { ref: 'B1', startIndex: 9, endIndex: 11 }, 269 ]; 270 const result = assignRangeColors(ranges); 271 expect(result[0].color).toBe(result[2].color); 272 expect(result[1].color).toBe(result[3].color); 273 expect(result[0].color).not.toBe(result[1].color); 274 }); 275});