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

Configure Feed

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

Merge pull request 'test: batch 17 — formula-tooltip & sparkline edge cases (44 tests)' (#259) from test/batch17-tooltip-sparkline-edges into main

scott 86042248 3b412061

+305
+92
tests/formula-tooltip.test.ts
··· 292 292 expect(result).toBeNull(); 293 293 }); 294 294 }); 295 + 296 + // ===================================================================== 297 + // detectCurrentFunction — edge cases 298 + // ===================================================================== 299 + 300 + describe('detectCurrentFunction — edge cases', () => { 301 + it('returns null for empty formula', () => { 302 + expect(detectCurrentFunction('', 0)).toBeNull(); 303 + }); 304 + 305 + it('returns null for cursor at position 0', () => { 306 + expect(detectCurrentFunction('=SUM(A1)', 0)).toBeNull(); 307 + }); 308 + 309 + it('returns null for formula without function call', () => { 310 + expect(detectCurrentFunction('=A1+B1', 3)).toBeNull(); 311 + }); 312 + 313 + it('detects function at very start of formula', () => { 314 + const result = detectCurrentFunction('=SUM(', 5); 315 + expect(result).not.toBeNull(); 316 + expect(result!.functionName).toBe('SUM'); 317 + expect(result!.paramIndex).toBe(0); 318 + }); 319 + 320 + it('handles triple-nested function correctly', () => { 321 + // =IF(SUM(ROUND(A1,2)), B1, C1) — cursor at position 17 = '2' inside ROUND 322 + const formula = '=IF(SUM(ROUND(A1,2)), B1, C1)'; 323 + const result = detectCurrentFunction(formula, 17); // at the '2' 324 + expect(result).not.toBeNull(); 325 + expect(result!.functionName).toBe('ROUND'); 326 + expect(result!.paramIndex).toBe(1); 327 + }); 328 + 329 + it('handles cursor right after opening paren with no args yet', () => { 330 + const result = detectCurrentFunction('=AVERAGE(', 9); 331 + expect(result).not.toBeNull(); 332 + expect(result!.functionName).toBe('AVERAGE'); 333 + expect(result!.paramIndex).toBe(0); 334 + }); 335 + 336 + it('handles string containing commas (should not count as param separator)', () => { 337 + const formula = '=CONCATENATE("a,b,c",D1)'; 338 + const result = detectCurrentFunction(formula, formula.length - 1); 339 + expect(result).not.toBeNull(); 340 + expect(result!.functionName).toBe('CONCATENATE'); 341 + expect(result!.paramIndex).toBe(1); // only 1 comma outside string 342 + }); 343 + 344 + it('handles escaped quotes inside string', () => { 345 + // String with escaped quote: "he said \\"hi\\"" 346 + const formula = '=CONCATENATE("he said \\"hi\\"",B1)'; 347 + const result = detectCurrentFunction(formula, formula.length - 1); 348 + expect(result).not.toBeNull(); 349 + expect(result!.functionName).toBe('CONCATENATE'); 350 + expect(result!.paramIndex).toBe(1); 351 + }); 352 + 353 + it('case-insensitive function name detection', () => { 354 + const result = detectCurrentFunction('=sum(A1,B1)', 10); 355 + expect(result).not.toBeNull(); 356 + expect(result!.functionName).toBe('SUM'); 357 + }); 358 + 359 + it('handles space between function name and paren', () => { 360 + const result = detectCurrentFunction('=SUM (A1)', 6); 361 + expect(result).not.toBeNull(); 362 + expect(result!.functionName).toBe('SUM'); 363 + }); 364 + 365 + it('returns null when cursor is outside all function calls', () => { 366 + const result = detectCurrentFunction('=SUM(A1)+B1', 11); 367 + expect(result).toBeNull(); 368 + }); 369 + 370 + it('detects outer function when cursor is between nested calls', () => { 371 + // =IF(A1, SUM(B1:B5), C1) — cursor at C1, param 2 of IF 372 + const formula = '=IF(A1, SUM(B1:B5), C1)'; 373 + const result = detectCurrentFunction(formula, 22); // at C1 374 + expect(result).not.toBeNull(); 375 + expect(result!.functionName).toBe('IF'); 376 + expect(result!.paramIndex).toBe(2); 377 + }); 378 + 379 + it('handles grouping parens that are not function calls', () => { 380 + // =(A1+B1)*SUM(C1) — cursor inside SUM 381 + const formula = '=(A1+B1)*SUM(C1)'; 382 + const result = detectCurrentFunction(formula, 15); // inside SUM 383 + expect(result).not.toBeNull(); 384 + expect(result!.functionName).toBe('SUM'); 385 + }); 386 + });
+213
tests/sparkline.test.ts
··· 260 260 expect(sr.data).toEqual([42]); 261 261 }); 262 262 }); 263 + 264 + // ===================================================================== 265 + // parseSparklineArgs — edge cases 266 + // ===================================================================== 267 + 268 + describe('parseSparklineArgs — edge cases', () => { 269 + it('returns null for empty args', () => { 270 + expect(parseSparklineArgs([])).toBeNull(); 271 + }); 272 + 273 + it('returns null when data is all non-numeric', () => { 274 + expect(parseSparklineArgs([['a', 'b', 'c']])).toBeNull(); 275 + }); 276 + 277 + it('filters NaN from data', () => { 278 + const result = parseSparklineArgs([[1, NaN, 3]]); 279 + expect(result).not.toBeNull(); 280 + expect(result!.data).toEqual([1, 3]); 281 + }); 282 + 283 + it('returns null for invalid chart type string', () => { 284 + expect(parseSparklineArgs([[1, 2], 'pie'])).toBeNull(); 285 + }); 286 + 287 + it('case-insensitive chart type', () => { 288 + const result = parseSparklineArgs([[1, 2], 'BAR']); 289 + expect(result).not.toBeNull(); 290 + expect(result!.chartType).toBe('bar'); 291 + }); 292 + 293 + it('parses options object with all fields', () => { 294 + const result = parseSparklineArgs([[1, 2], { 295 + type: 'bar', 296 + color: '#ff0000', 297 + negColor: '#00ff00', 298 + lineWidth: 3, 299 + minValue: 0, 300 + maxValue: 10, 301 + showAxis: true, 302 + }]); 303 + expect(result).not.toBeNull(); 304 + expect(result!.chartType).toBe('bar'); 305 + expect(result!.options.color).toBe('#ff0000'); 306 + expect(result!.options.negColor).toBe('#00ff00'); 307 + expect(result!.options.lineWidth).toBe(3); 308 + expect(result!.options.minValue).toBe(0); 309 + expect(result!.options.maxValue).toBe(10); 310 + expect(result!.options.showAxis).toBe(true); 311 + }); 312 + 313 + it('ignores null/undefined second arg', () => { 314 + const result = parseSparklineArgs([[1, 2], null]); 315 + expect(result).not.toBeNull(); 316 + expect(result!.chartType).toBe('line'); 317 + }); 318 + 319 + it('handles non-array first arg (wraps in array)', () => { 320 + const result = parseSparklineArgs([5]); 321 + expect(result).not.toBeNull(); 322 + expect(result!.data).toEqual([5]); 323 + }); 324 + 325 + it('flattens nested arrays', () => { 326 + const result = parseSparklineArgs([[[1, 2], [3, 4]]]); 327 + expect(result).not.toBeNull(); 328 + expect(result!.data).toEqual([1, 2, 3, 4]); 329 + }); 330 + }); 331 + 332 + // ===================================================================== 333 + // isSparklineResult — edge cases 334 + // ===================================================================== 335 + 336 + describe('isSparklineResult — edge cases', () => { 337 + it('returns false for null', () => { 338 + expect(isSparklineResult(null)).toBe(false); 339 + }); 340 + 341 + it('returns false for undefined', () => { 342 + expect(isSparklineResult(undefined)).toBe(false); 343 + }); 344 + 345 + it('returns false for string', () => { 346 + expect(isSparklineResult('sparkline')).toBe(false); 347 + }); 348 + 349 + it('returns false for number', () => { 350 + expect(isSparklineResult(42)).toBe(false); 351 + }); 352 + 353 + it('returns false for plain object without __sparkline', () => { 354 + expect(isSparklineResult({ data: [1, 2] })).toBe(false); 355 + }); 356 + 357 + it('returns true for valid SparklineResult', () => { 358 + expect(isSparklineResult({ __sparkline: true, chartType: 'line', data: [1], options: {} })).toBe(true); 359 + }); 360 + }); 361 + 362 + // ===================================================================== 363 + // computeLinePoints — edge cases 364 + // ===================================================================== 365 + 366 + describe('computeLinePoints — edge cases', () => { 367 + it('returns empty for empty data', () => { 368 + expect(computeLinePoints([], 100, 50, {})).toEqual([]); 369 + }); 370 + 371 + it('single data point is centered', () => { 372 + const points = computeLinePoints([5], 100, 50, {}); 373 + expect(points.length).toBe(1); 374 + expect(points[0].x).toBe(50); // width/2 375 + }); 376 + 377 + it('all equal values produce y at height/2', () => { 378 + const points = computeLinePoints([5, 5, 5], 100, 60, {}); 379 + for (const p of points) { 380 + expect(p.y).toBe(30); // height/2 381 + } 382 + }); 383 + 384 + it('first point at x=0 and last at x=width', () => { 385 + const points = computeLinePoints([1, 2, 3], 100, 50, {}); 386 + expect(points[0].x).toBe(0); 387 + expect(points[points.length - 1].x).toBe(100); 388 + }); 389 + 390 + it('min value maps to y=height (bottom) and max to y=0 (top)', () => { 391 + const points = computeLinePoints([0, 10], 100, 50, {}); 392 + expect(points[0].y).toBe(50); // min at bottom 393 + expect(points[1].y).toBe(0); // max at top 394 + }); 395 + 396 + it('respects custom minValue/maxValue options', () => { 397 + const points = computeLinePoints([5], 100, 100, { minValue: 0, maxValue: 10 }); 398 + expect(points[0].y).toBe(50); // midpoint 399 + }); 400 + }); 401 + 402 + // ===================================================================== 403 + // computeBarRects — edge cases 404 + // ===================================================================== 405 + 406 + describe('computeBarRects — edge cases', () => { 407 + it('returns empty for empty data', () => { 408 + expect(computeBarRects([], 100, 50, {})).toEqual([]); 409 + }); 410 + 411 + it('all positive bars have negative=false', () => { 412 + const rects = computeBarRects([1, 2, 3], 90, 60, {}); 413 + for (const r of rects) { 414 + expect(r.negative).toBe(false); 415 + } 416 + }); 417 + 418 + it('mixed positive/negative marks negative bars correctly', () => { 419 + const rects = computeBarRects([5, -3], 100, 60, {}); 420 + expect(rects[0].negative).toBe(false); 421 + expect(rects[1].negative).toBe(true); 422 + }); 423 + 424 + it('bar widths sum to total width', () => { 425 + const rects = computeBarRects([1, 2, 3, 4], 100, 50, {}); 426 + const totalWidth = rects.reduce((sum, r) => sum + r.width, 0); 427 + expect(totalWidth).toBeCloseTo(100); 428 + }); 429 + 430 + it('all-equal values produce full-height bars', () => { 431 + const rects = computeBarRects([5, 5, 5], 90, 60, {}); 432 + for (const r of rects) { 433 + expect(r.height).toBe(60); 434 + } 435 + }); 436 + }); 437 + 438 + // ===================================================================== 439 + // computeWinLossRects — edge cases 440 + // ===================================================================== 441 + 442 + describe('computeWinLossRects — edge cases', () => { 443 + it('returns empty for empty data', () => { 444 + expect(computeWinLossRects([], 100, 50, {})).toEqual([]); 445 + }); 446 + 447 + it('zero values produce thin center rects', () => { 448 + const rects = computeWinLossRects([0, 0], 100, 60, {}); 449 + for (const r of rects) { 450 + expect(r.category).toBe('zero'); 451 + expect(r.height).toBe(2); // zeroHeight constant 452 + } 453 + }); 454 + 455 + it('all positive values are wins', () => { 456 + const rects = computeWinLossRects([1, 2, 3], 90, 60, {}); 457 + for (const r of rects) { 458 + expect(r.category).toBe('win'); 459 + } 460 + }); 461 + 462 + it('all negative values are losses', () => { 463 + const rects = computeWinLossRects([-1, -2], 80, 60, {}); 464 + for (const r of rects) { 465 + expect(r.category).toBe('loss'); 466 + } 467 + }); 468 + 469 + it('win rects are in top half, loss rects in bottom half', () => { 470 + const rects = computeWinLossRects([1, -1], 100, 60, {}); 471 + const halfHeight = 30; 472 + expect(rects[0].y).toBeLessThan(halfHeight); // win in top 473 + expect(rects[1].y).toBeGreaterThanOrEqual(halfHeight); // loss in bottom 474 + }); 475 + });