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

Configure Feed

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

test: formula-tooltip parser + sort compareValues edge cases (#429, #430)

formula-tooltip: escaped quotes in strings, whitespace before paren,
grouping parens, unrecognized outer function, 3-level nesting, string
with parens, high param index, null input.

sort: null/undefined sort first, numeric strings coerced, zero is not
empty, negative numbers, Infinity, booleans as strings not numbers,
descending with nulls, NaN-producing strings, scientific notation.

+193
+93
tests/formula-tooltip.test.ts
··· 198 198 expect(result).not.toBeNull(); 199 199 expect(result.functionName).toBe('SUM'); 200 200 }); 201 + 202 + // --- Edge cases identified by QA --- 203 + 204 + it('handles escaped quotes inside string literals', () => { 205 + // =CONCATENATE("a\"b",C1) 206 + // The parser should skip the escaped quote and not toggle inString 207 + const formula = '=CONCATENATE("a\\"b",C1)'; 208 + const result = detectCurrentFunction(formula, formula.length - 1); 209 + expect(result).not.toBeNull(); 210 + expect(result!.functionName).toBe('CONCATENATE'); 211 + expect(result!.paramIndex).toBe(1); 212 + }); 213 + 214 + it('handles whitespace before opening paren', () => { 215 + // =SUM (A1) — space between function name and paren 216 + const result = detectCurrentFunction('=SUM (A1)', 6); 217 + expect(result).not.toBeNull(); 218 + expect(result!.functionName).toBe('SUM'); 219 + expect(result!.paramIndex).toBe(0); 220 + }); 221 + 222 + it('returns null for grouping parens (not a function call)', () => { 223 + // =(A1+B1)*C1 — cursor inside grouping parens 224 + const result = detectCurrentFunction('=(A1+B1)*C1', 4); 225 + expect(result).toBeNull(); 226 + }); 227 + 228 + it('skips unrecognized outer function to find inner recognized one', () => { 229 + // =MYFUNC(SUM(A1)) — cursor inside SUM, MYFUNC is not in metadata 230 + const result = detectCurrentFunction('=MYFUNC(SUM(A1))', 13); 231 + expect(result).not.toBeNull(); 232 + expect(result!.functionName).toBe('SUM'); 233 + expect(result!.paramIndex).toBe(0); 234 + }); 235 + 236 + it('handles 3-level nesting', () => { 237 + // =IF(SUM(ABS(A1)),B1,C1) — cursor inside ABS 238 + const result = detectCurrentFunction('=IF(SUM(ABS(A1)),B1,C1)', 13); 239 + expect(result).not.toBeNull(); 240 + expect(result!.functionName).toBe('ABS'); 241 + expect(result!.paramIndex).toBe(0); 242 + }); 243 + 244 + it('returns outer function after closing deep nesting', () => { 245 + // =IF(SUM(ABS(A1)),B1,C1) — cursor at B1, param 1 of IF 246 + const result = detectCurrentFunction('=IF(SUM(ABS(A1)),B1,C1)', 19); 247 + expect(result).not.toBeNull(); 248 + expect(result!.functionName).toBe('IF'); 249 + expect(result!.paramIndex).toBe(1); 250 + }); 251 + 252 + it('returns null for null/empty formula', () => { 253 + expect(detectCurrentFunction(null as unknown as string, 5)).toBeNull(); 254 + expect(detectCurrentFunction('', 0)).toBeNull(); 255 + }); 256 + 257 + it('handles formula with only opening paren and no function name', () => { 258 + // =( — no function name before paren 259 + const result = detectCurrentFunction('=(', 2); 260 + expect(result).toBeNull(); 261 + }); 262 + 263 + it('handles multiple commas for high param index', () => { 264 + // =CONCATENATE(A1,B1,C1,D1,E1) — cursor at E1, param 4 265 + const formula = '=CONCATENATE(A1,B1,C1,D1,E1)'; 266 + const result = detectCurrentFunction(formula, formula.length - 1); 267 + expect(result).not.toBeNull(); 268 + expect(result!.functionName).toBe('CONCATENATE'); 269 + expect(result!.paramIndex).toBe(4); 270 + }); 271 + 272 + it('handles string containing parens', () => { 273 + // =CONCATENATE("(hello)",B1) — parens inside string should be ignored 274 + const formula = '=CONCATENATE("(hello)",B1)'; 275 + const result = detectCurrentFunction(formula, formula.length - 1); 276 + expect(result).not.toBeNull(); 277 + expect(result!.functionName).toBe('CONCATENATE'); 278 + expect(result!.paramIndex).toBe(1); 279 + }); 280 + 281 + it('handles adjacent closing parens from nested calls', () => { 282 + // =ROUND(SUM(A1:A10),2) — cursor at 2, param 1 of ROUND 283 + const result = detectCurrentFunction('=ROUND(SUM(A1:A10),2)', 20); 284 + expect(result).not.toBeNull(); 285 + expect(result!.functionName).toBe('ROUND'); 286 + expect(result!.paramIndex).toBe(1); 287 + }); 288 + 289 + it('handles function name with underscores', () => { 290 + // Functions with underscores in names aren't in metadata, returns null 291 + const result = detectCurrentFunction('=MY_FUNC(A1)', 9); 292 + expect(result).toBeNull(); 293 + }); 201 294 });
+100
tests/multi-sort.test.ts
··· 167 167 multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 168 168 expect(colValues(rows, 1)).toEqual(colValues(original, 1)); 169 169 }); 170 + 171 + it('single-row array returns that row unchanged', () => { 172 + const rows = makeRows([['only']]); 173 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 174 + expect(sorted).toHaveLength(1); 175 + expect(sorted[0][1]).toBe('only'); 176 + }); 177 + }); 178 + 179 + // ===================================================================== 180 + // compareValues edge cases (tested via multiColumnSort) 181 + // ===================================================================== 182 + 183 + describe('multiColumnSort — compareValues type coercion', () => { 184 + it('null values sort first (before strings and numbers)', () => { 185 + const rows = makeRows([['B'], [null], ['A']]); 186 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 187 + expect(colValues(sorted, 1)).toEqual([null, 'A', 'B']); 188 + }); 189 + 190 + it('undefined values sort first', () => { 191 + const rows = makeRows([['B'], [undefined], ['A']]); 192 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 193 + expect(colValues(sorted, 1)).toEqual([undefined, 'A', 'B']); 194 + }); 195 + 196 + it('null and undefined are equivalent in sort order', () => { 197 + const rows = makeRows([[null], [undefined], [null]]); 198 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 199 + // All empty — stable sort preserves order 200 + expect(sorted).toHaveLength(3); 201 + }); 202 + 203 + it('numeric strings sort numerically with actual numbers', () => { 204 + const rows = makeRows([['100'], [3], ['20']]); 205 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 206 + expect(colValues(sorted, 1)).toEqual([3, '20', '100']); 207 + }); 208 + 209 + it('zero is not empty — sorts after empty/null but before positive numbers', () => { 210 + const rows = makeRows([[5], [''], [0], [null]]); 211 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 212 + // Empty/null first, then 0, then 5 213 + expect(colValues(sorted, 1)).toEqual(['', null, 0, 5]); 214 + }); 215 + 216 + it('negative numbers sort before positive', () => { 217 + const rows = makeRows([[3], [-5], [0], [-1]]); 218 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 219 + expect(colValues(sorted, 1)).toEqual([-5, -1, 0, 3]); 220 + }); 221 + 222 + it('Infinity sorts after all finite numbers', () => { 223 + const rows = makeRows([[100], [Infinity], [1], [-Infinity]]); 224 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 225 + expect(colValues(sorted, 1)).toEqual([-Infinity, 1, 100, Infinity]); 226 + }); 227 + 228 + it('non-numeric strings sort after numbers', () => { 229 + const rows = makeRows([['hello'], [42], ['world'], [1]]); 230 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 231 + // Numbers first (1, 42), then strings alphabetically 232 + expect(colValues(sorted, 1)).toEqual([1, 42, 'hello', 'world']); 233 + }); 234 + 235 + it('boolean true sorts after numbers (not coerced to 1)', () => { 236 + const rows = makeRows([[3], [true], [0]]); 237 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 238 + // Booleans are typeof boolean, not number or string — treated as strings via String() 239 + // Numbers sort before strings: 0, 3, then true ("true" as string) 240 + expect(colValues(sorted, 1)).toEqual([0, 3, true]); 241 + }); 242 + 243 + it('boolean false sorts after numbers (not coerced to 0)', () => { 244 + const rows = makeRows([[1], [false], [2]]); 245 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 246 + // Numbers first, then false ("false" as string) 247 + expect(colValues(sorted, 1)).toEqual([1, 2, false]); 248 + }); 249 + 250 + it('descending with nulls: nulls move to end', () => { 251 + const rows = makeRows([[null], [3], [1], ['']]); 252 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'desc' }]); 253 + // Desc reverses: numbers first (3, 1), then empty/null last 254 + expect(colValues(sorted, 1)).toEqual([3, 1, null, '']); 255 + }); 256 + 257 + it('NaN-producing strings sort as strings, not numbers', () => { 258 + const rows = makeRows([['hello'], [5], ['world']]); 259 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 260 + // 5 (number) first, then strings alphabetically 261 + expect(colValues(sorted, 1)).toEqual([5, 'hello', 'world']); 262 + }); 263 + 264 + it('scientific notation strings are treated as numbers', () => { 265 + const rows = makeRows([['1e2'], [50], ['2e1']]); 266 + const sorted = multiColumnSort(rows, [{ col: 1, order: 'asc' }]); 267 + // 1e2=100, 2e1=20: 20, 50, 100 268 + expect(colValues(sorted, 1)).toEqual(['2e1', 50, '1e2']); 269 + }); 170 270 });