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

Configure Feed

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

test: add 119 edge case tests across 5 modules (batch 20)

Coverage expansion for ai-actions (diagram/slide/form type guards,
validation, describeAction), chart-embed (multi-series, bar layout
zeros, pie slices, resize clamping), backup (manifest edge cases,
version validation), command-palette (special chars, regex safety,
navigation consistency), and version-panel (boundary times, null
wordCount, metadata parsing).

Total test count: 6,638 → 6,757.

+780
+325
tests/ai-actions.test.ts
··· 7 7 describeAction, 8 8 isDocAction, 9 9 isSheetAction, 10 + isDiagramAction, 11 + isSlideAction, 12 + isFormAction, 10 13 type AIAction, 11 14 } from '../src/lib/ai-actions.js'; 12 15 ··· 306 309 expect(isDocAction({ type: 'sheet_set', cells: [{ ref: 'A1', value: 'x' }] } as AIAction)).toBe(false); 307 310 }); 308 311 }); 312 + 313 + // ===================================================================== 314 + // Edge cases 315 + // ===================================================================== 316 + 317 + describe('validateAction — diagram/slide/form types', () => { 318 + it('validates diagram_add_shape', () => { 319 + expect(validateAction({ type: 'diagram_add_shape', kind: 'rect', x: 10, y: 20, w: 100, h: 50 }).valid).toBe(true); 320 + }); 321 + 322 + it('rejects diagram_add_shape without kind', () => { 323 + expect(validateAction({ type: 'diagram_add_shape', x: 10, y: 20, w: 100, h: 50 }).valid).toBe(false); 324 + }); 325 + 326 + it('rejects diagram_add_shape with non-numeric coordinates', () => { 327 + expect(validateAction({ type: 'diagram_add_shape', kind: 'rect', x: 'a', y: 20, w: 100, h: 50 }).valid).toBe(false); 328 + }); 329 + 330 + it('rejects diagram_add_shape without dimensions', () => { 331 + expect(validateAction({ type: 'diagram_add_shape', kind: 'rect', x: 10, y: 20 }).valid).toBe(false); 332 + }); 333 + 334 + it('validates diagram_add_arrow', () => { 335 + expect(validateAction({ type: 'diagram_add_arrow', fromLabel: 'A', toLabel: 'B' }).valid).toBe(true); 336 + }); 337 + 338 + it('rejects diagram_add_arrow without labels', () => { 339 + expect(validateAction({ type: 'diagram_add_arrow', fromLabel: 'A' }).valid).toBe(false); 340 + }); 341 + 342 + it('validates diagram_modify_shape', () => { 343 + expect(validateAction({ type: 'diagram_modify_shape', label: 'Box1' }).valid).toBe(true); 344 + }); 345 + 346 + it('rejects diagram_modify_shape without label', () => { 347 + expect(validateAction({ type: 'diagram_modify_shape' }).valid).toBe(false); 348 + }); 349 + 350 + it('validates diagram_remove_shape', () => { 351 + expect(validateAction({ type: 'diagram_remove_shape', label: 'Box1' }).valid).toBe(true); 352 + }); 353 + 354 + it('rejects diagram_remove_shape without label', () => { 355 + expect(validateAction({ type: 'diagram_remove_shape' }).valid).toBe(false); 356 + }); 357 + 358 + it('validates diagram_add_text', () => { 359 + expect(validateAction({ type: 'diagram_add_text', x: 10, y: 20, text: 'Hello' }).valid).toBe(true); 360 + }); 361 + 362 + it('rejects diagram_add_text without text', () => { 363 + expect(validateAction({ type: 'diagram_add_text', x: 10, y: 20 }).valid).toBe(false); 364 + }); 365 + 366 + it('rejects diagram_add_text with non-numeric coords', () => { 367 + expect(validateAction({ type: 'diagram_add_text', x: 'a', y: 20, text: 'Hello' }).valid).toBe(false); 368 + }); 369 + 370 + it('validates slide_add with no options', () => { 371 + expect(validateAction({ type: 'slide_add' }).valid).toBe(true); 372 + }); 373 + 374 + it('validates slide_add with layout', () => { 375 + expect(validateAction({ type: 'slide_add', layout: 'title' }).valid).toBe(true); 376 + }); 377 + 378 + it('validates slide_add_text', () => { 379 + expect(validateAction({ type: 'slide_add_text', x: 10, y: 20, w: 200, h: 50, text: 'Title' }).valid).toBe(true); 380 + }); 381 + 382 + it('rejects slide_add_text without text', () => { 383 + expect(validateAction({ type: 'slide_add_text', x: 10, y: 20, w: 200, h: 50 }).valid).toBe(false); 384 + }); 385 + 386 + it('rejects slide_add_text without dimensions', () => { 387 + expect(validateAction({ type: 'slide_add_text', x: 10, y: 20, text: 'Title' }).valid).toBe(false); 388 + }); 389 + 390 + it('validates slide_add_shape', () => { 391 + expect(validateAction({ type: 'slide_add_shape', element: 'circle', x: 10, y: 20, w: 50, h: 50 }).valid).toBe(true); 392 + }); 393 + 394 + it('rejects slide_add_shape without element', () => { 395 + expect(validateAction({ type: 'slide_add_shape', x: 10, y: 20, w: 50, h: 50 }).valid).toBe(false); 396 + }); 397 + 398 + it('validates form_add_question', () => { 399 + expect(validateAction({ type: 'form_add_question', questionType: 'text', title: 'Name?' }).valid).toBe(true); 400 + }); 401 + 402 + it('rejects form_add_question without questionType', () => { 403 + expect(validateAction({ type: 'form_add_question', title: 'Name?' }).valid).toBe(false); 404 + }); 405 + 406 + it('rejects form_add_question without title', () => { 407 + expect(validateAction({ type: 'form_add_question', questionType: 'text' }).valid).toBe(false); 408 + }); 409 + 410 + it('validates form_modify_question', () => { 411 + expect(validateAction({ type: 'form_modify_question', title: 'Name?' }).valid).toBe(true); 412 + }); 413 + 414 + it('rejects form_modify_question without title', () => { 415 + expect(validateAction({ type: 'form_modify_question' }).valid).toBe(false); 416 + }); 417 + 418 + it('validates form_remove_question', () => { 419 + expect(validateAction({ type: 'form_remove_question', title: 'Name?' }).valid).toBe(true); 420 + }); 421 + 422 + it('rejects form_remove_question without title', () => { 423 + expect(validateAction({ type: 'form_remove_question' }).valid).toBe(false); 424 + }); 425 + }); 426 + 427 + describe('describeAction — diagram/slide/form types', () => { 428 + it('describes diagram_add_shape with label', () => { 429 + const desc = describeAction({ type: 'diagram_add_shape', kind: 'rect', x: 0, y: 0, w: 100, h: 50, label: 'Start' } as AIAction); 430 + expect(desc).toContain('rect'); 431 + expect(desc).toContain('Start'); 432 + }); 433 + 434 + it('describes diagram_add_shape without label', () => { 435 + const desc = describeAction({ type: 'diagram_add_shape', kind: 'circle', x: 0, y: 0, w: 50, h: 50 } as AIAction); 436 + expect(desc).toContain('circle'); 437 + expect(desc).not.toContain('undefined'); 438 + }); 439 + 440 + it('describes diagram_add_arrow', () => { 441 + const desc = describeAction({ type: 'diagram_add_arrow', fromLabel: 'A', toLabel: 'B' } as AIAction); 442 + expect(desc).toContain('arrow'); 443 + expect(desc).toContain('A'); 444 + expect(desc).toContain('B'); 445 + }); 446 + 447 + it('describes diagram_modify_shape', () => { 448 + const desc = describeAction({ type: 'diagram_modify_shape', label: 'Box1' } as AIAction); 449 + expect(desc).toContain('Modify'); 450 + expect(desc).toContain('Box1'); 451 + }); 452 + 453 + it('describes diagram_remove_shape', () => { 454 + const desc = describeAction({ type: 'diagram_remove_shape', label: 'Box1' } as AIAction); 455 + expect(desc).toContain('Remove'); 456 + expect(desc).toContain('Box1'); 457 + }); 458 + 459 + it('describes diagram_add_text and truncates long text', () => { 460 + const longText = 'a'.repeat(60); 461 + const desc = describeAction({ type: 'diagram_add_text', x: 0, y: 0, text: longText } as AIAction); 462 + expect(desc).toContain('...'); 463 + expect(desc.length).toBeLessThan(80); 464 + }); 465 + 466 + it('describes slide_add with layout', () => { 467 + const desc = describeAction({ type: 'slide_add', layout: 'title' } as AIAction); 468 + expect(desc).toContain('title'); 469 + expect(desc).toContain('slide'); 470 + }); 471 + 472 + it('describes slide_add without layout (blank)', () => { 473 + const desc = describeAction({ type: 'slide_add' } as AIAction); 474 + expect(desc).toContain('blank'); 475 + }); 476 + 477 + it('describes slide_add_text', () => { 478 + const desc = describeAction({ type: 'slide_add_text', x: 0, y: 0, w: 200, h: 50, text: 'Hello Slide' } as AIAction); 479 + expect(desc).toContain('Hello Slide'); 480 + }); 481 + 482 + it('describes slide_add_shape', () => { 483 + const desc = describeAction({ type: 'slide_add_shape', element: 'arrow', x: 0, y: 0, w: 50, h: 50 } as AIAction); 484 + expect(desc).toContain('arrow'); 485 + }); 486 + 487 + it('describes form_add_question', () => { 488 + const desc = describeAction({ type: 'form_add_question', questionType: 'multiple_choice', title: 'Favorite color?' } as AIAction); 489 + expect(desc).toContain('multiple_choice'); 490 + expect(desc).toContain('Favorite color?'); 491 + }); 492 + 493 + it('describes form_add_question and truncates long title', () => { 494 + const longTitle = 'Q'.repeat(60); 495 + const desc = describeAction({ type: 'form_add_question', questionType: 'text', title: longTitle } as AIAction); 496 + expect(desc).toContain('...'); 497 + }); 498 + 499 + it('describes form_modify_question', () => { 500 + const desc = describeAction({ type: 'form_modify_question', title: 'Name?' } as AIAction); 501 + expect(desc).toContain('Modify'); 502 + expect(desc).toContain('Name?'); 503 + }); 504 + 505 + it('describes form_remove_question', () => { 506 + const desc = describeAction({ type: 'form_remove_question', title: 'Name?' } as AIAction); 507 + expect(desc).toContain('Remove'); 508 + expect(desc).toContain('Name?'); 509 + }); 510 + }); 511 + 512 + describe('isDiagramAction / isSlideAction / isFormAction', () => { 513 + it('identifies all diagram action types', () => { 514 + expect(isDiagramAction({ type: 'diagram_add_shape', kind: 'rect', x: 0, y: 0, w: 50, h: 50 })).toBe(true); 515 + expect(isDiagramAction({ type: 'diagram_add_arrow', fromLabel: 'A', toLabel: 'B' })).toBe(true); 516 + expect(isDiagramAction({ type: 'diagram_modify_shape', label: 'X' })).toBe(true); 517 + expect(isDiagramAction({ type: 'diagram_remove_shape', label: 'X' })).toBe(true); 518 + expect(isDiagramAction({ type: 'diagram_add_text', x: 0, y: 0, text: 'Hi' })).toBe(true); 519 + }); 520 + 521 + it('identifies all slide action types', () => { 522 + expect(isSlideAction({ type: 'slide_add' })).toBe(true); 523 + expect(isSlideAction({ type: 'slide_add_text', x: 0, y: 0, w: 100, h: 50, text: 'Hi' })).toBe(true); 524 + expect(isSlideAction({ type: 'slide_add_shape', element: 'rect', x: 0, y: 0, w: 50, h: 50 })).toBe(true); 525 + }); 526 + 527 + it('identifies all form action types', () => { 528 + expect(isFormAction({ type: 'form_add_question', questionType: 'text', title: 'Q' })).toBe(true); 529 + expect(isFormAction({ type: 'form_modify_question', title: 'Q' })).toBe(true); 530 + expect(isFormAction({ type: 'form_remove_question', title: 'Q' })).toBe(true); 531 + }); 532 + 533 + it('diagram actions are not slide/form/doc/sheet actions', () => { 534 + const action = { type: 'diagram_add_shape', kind: 'rect', x: 0, y: 0, w: 50, h: 50 } as AIAction; 535 + expect(isDiagramAction(action)).toBe(true); 536 + expect(isSlideAction(action)).toBe(false); 537 + expect(isFormAction(action)).toBe(false); 538 + expect(isDocAction(action)).toBe(false); 539 + expect(isSheetAction(action)).toBe(false); 540 + }); 541 + 542 + it('slide actions are not diagram/form/doc/sheet actions', () => { 543 + const action = { type: 'slide_add' } as AIAction; 544 + expect(isSlideAction(action)).toBe(true); 545 + expect(isDiagramAction(action)).toBe(false); 546 + expect(isFormAction(action)).toBe(false); 547 + expect(isDocAction(action)).toBe(false); 548 + expect(isSheetAction(action)).toBe(false); 549 + }); 550 + 551 + it('form actions are not diagram/slide/doc/sheet actions', () => { 552 + const action = { type: 'form_add_question', questionType: 'text', title: 'Q' } as AIAction; 553 + expect(isFormAction(action)).toBe(true); 554 + expect(isDiagramAction(action)).toBe(false); 555 + expect(isSlideAction(action)).toBe(false); 556 + expect(isDocAction(action)).toBe(false); 557 + expect(isSheetAction(action)).toBe(false); 558 + }); 559 + }); 560 + 561 + describe('parseActions — diagram/slide/form blocks', () => { 562 + it('parses a diagram action block', () => { 563 + const text = '```action\n{"type":"diagram_add_shape","kind":"rect","x":10,"y":20,"w":100,"h":50}\n```'; 564 + const { actions, errors } = parseActions(text); 565 + expect(actions).toHaveLength(1); 566 + expect(actions[0].type).toBe('diagram_add_shape'); 567 + expect(errors).toHaveLength(0); 568 + }); 569 + 570 + it('parses a slide action block', () => { 571 + const text = '```action\n{"type":"slide_add","layout":"title"}\n```'; 572 + const { actions } = parseActions(text); 573 + expect(actions).toHaveLength(1); 574 + expect(actions[0].type).toBe('slide_add'); 575 + }); 576 + 577 + it('parses a form action block', () => { 578 + const text = '```action\n{"type":"form_add_question","questionType":"text","title":"Name?"}\n```'; 579 + const { actions } = parseActions(text); 580 + expect(actions).toHaveLength(1); 581 + expect(actions[0].type).toBe('form_add_question'); 582 + }); 583 + 584 + it('parses mixed action types in one response', () => { 585 + const text = [ 586 + '```action\n{"type":"doc_insert","position":"end","content":"Hi"}\n```', 587 + '```action\n{"type":"diagram_add_shape","kind":"rect","x":0,"y":0,"w":50,"h":50}\n```', 588 + '```action\n{"type":"slide_add"}\n```', 589 + '```action\n{"type":"form_add_question","questionType":"text","title":"Q"}\n```', 590 + ].join('\n\n'); 591 + const { actions } = parseActions(text); 592 + expect(actions).toHaveLength(4); 593 + expect(actions.map(a => a.type)).toEqual([ 594 + 'doc_insert', 'diagram_add_shape', 'slide_add', 'form_add_question', 595 + ]); 596 + }); 597 + }); 598 + 599 + describe('splitResponse — edge cases', () => { 600 + it('handles empty string', () => { 601 + const { displayText, actions, errors } = splitResponse(''); 602 + expect(displayText).toBe(''); 603 + expect(actions).toHaveLength(0); 604 + expect(errors).toHaveLength(0); 605 + }); 606 + 607 + it('handles text with only whitespace', () => { 608 + const { displayText } = splitResponse(' \n\n '); 609 + expect(displayText.trim()).toBe(''); 610 + }); 611 + }); 612 + 613 + describe('describeAction — edge cases', () => { 614 + it('describes sheet_set with exactly 1 cell (no plural)', () => { 615 + const desc = describeAction({ type: 'sheet_set', cells: [{ ref: 'A1', value: 'x' }] }); 616 + expect(desc).toContain('1 cell'); 617 + expect(desc).not.toContain('1 cells'); 618 + }); 619 + 620 + it('describes doc_replace with short search (no truncation)', () => { 621 + const desc = describeAction({ type: 'doc_replace', search: 'hello', replace: 'world' }); 622 + expect(desc).toContain('hello'); 623 + expect(desc).not.toContain('...'); 624 + }); 625 + 626 + it('describes sheet_set with exactly 3 cells (no ellipsis)', () => { 627 + const cells = [{ ref: 'A1', value: '1' }, { ref: 'B1', value: '2' }, { ref: 'C1', value: '3' }]; 628 + const desc = describeAction({ type: 'sheet_set', cells }); 629 + expect(desc).toContain('A1'); 630 + expect(desc).toContain('C1'); 631 + expect(desc).not.toContain('...'); 632 + }); 633 + });
+89
tests/backup.test.ts
··· 92 92 }); 93 93 }); 94 94 }); 95 + 96 + // ===================================================================== 97 + // Edge cases 98 + // ===================================================================== 99 + 100 + describe('Encrypted Backup — edge cases', () => { 101 + describe('createBackupManifest edge cases', () => { 102 + it('creates manifest with zero documents', () => { 103 + const manifest = createBackupManifest([]); 104 + expect(manifest.magic).toBe(BACKUP_MAGIC); 105 + expect(manifest.version).toBe(BACKUP_VERSION); 106 + expect(manifest.documents).toEqual([]); 107 + }); 108 + 109 + it('preserves null name_encrypted', () => { 110 + const entries: BackupDocEntry[] = [ 111 + { id: 'd1', type: 'doc', name_encrypted: null, snapshot: 'snap', created_at: '2026-01-01', updated_at: '2026-01-02' }, 112 + ]; 113 + const manifest = createBackupManifest(entries); 114 + expect(manifest.documents[0].name_encrypted).toBeNull(); 115 + }); 116 + 117 + it('preserves document order', () => { 118 + const entries: BackupDocEntry[] = [ 119 + { id: 'z', type: 'sheet', name_encrypted: 'e1', snapshot: 's1', created_at: '2026-01-01', updated_at: '2026-01-02' }, 120 + { id: 'a', type: 'doc', name_encrypted: 'e2', snapshot: 's2', created_at: '2026-01-03', updated_at: '2026-01-04' }, 121 + { id: 'm', type: 'doc', name_encrypted: 'e3', snapshot: 's3', created_at: '2026-01-05', updated_at: '2026-01-06' }, 122 + ]; 123 + const manifest = createBackupManifest(entries); 124 + expect(manifest.documents.map(d => d.id)).toEqual(['z', 'a', 'm']); 125 + }); 126 + }); 127 + 128 + describe('parseBackupManifest edge cases', () => { 129 + it('parses manifest with empty documents array', () => { 130 + const manifest = { magic: BACKUP_MAGIC, version: BACKUP_VERSION, documents: [], created_at: new Date().toISOString() }; 131 + const parsed = parseBackupManifest(JSON.stringify(manifest)); 132 + expect(parsed).not.toBeNull(); 133 + expect(parsed!.documents).toEqual([]); 134 + }); 135 + 136 + it('accepts version equal to BACKUP_VERSION', () => { 137 + const manifest = { magic: BACKUP_MAGIC, version: BACKUP_VERSION, documents: [], created_at: new Date().toISOString() }; 138 + const parsed = parseBackupManifest(JSON.stringify(manifest)); 139 + expect(parsed).not.toBeNull(); 140 + }); 141 + 142 + it('accepts version less than BACKUP_VERSION (forward compat)', () => { 143 + if (BACKUP_VERSION > 1) { 144 + const manifest = { magic: BACKUP_MAGIC, version: 1, documents: [], created_at: new Date().toISOString() }; 145 + expect(parseBackupManifest(JSON.stringify(manifest))).not.toBeNull(); 146 + } 147 + }); 148 + 149 + it('rejects version greater than BACKUP_VERSION', () => { 150 + const manifest = { magic: BACKUP_MAGIC, version: BACKUP_VERSION + 1, documents: [], created_at: new Date().toISOString() }; 151 + expect(parseBackupManifest(JSON.stringify(manifest))).toBeNull(); 152 + }); 153 + 154 + it('ignores extra fields in manifest', () => { 155 + const manifest = { 156 + magic: BACKUP_MAGIC, version: BACKUP_VERSION, 157 + documents: [], created_at: new Date().toISOString(), 158 + extraField: 'should be ignored', 159 + }; 160 + const parsed = parseBackupManifest(JSON.stringify(manifest)); 161 + expect(parsed).not.toBeNull(); 162 + }); 163 + 164 + it('returns null for non-numeric version', () => { 165 + const manifest = { magic: BACKUP_MAGIC, version: 'one', documents: [], created_at: new Date().toISOString() }; 166 + expect(parseBackupManifest(JSON.stringify(manifest))).toBeNull(); 167 + }); 168 + 169 + it('returns null for null documents field', () => { 170 + const manifest = { magic: BACKUP_MAGIC, version: BACKUP_VERSION, documents: null, created_at: new Date().toISOString() }; 171 + expect(parseBackupManifest(JSON.stringify(manifest))).toBeNull(); 172 + }); 173 + 174 + it('returns null for documents as object instead of array', () => { 175 + const manifest = { magic: BACKUP_MAGIC, version: BACKUP_VERSION, documents: {}, created_at: new Date().toISOString() }; 176 + expect(parseBackupManifest(JSON.stringify(manifest))).toBeNull(); 177 + }); 178 + 179 + it('returns null for empty string input', () => { 180 + expect(parseBackupManifest('')).toBeNull(); 181 + }); 182 + }); 183 + });
+190
tests/chart-embed.test.ts
··· 173 173 expect(isChartValid(config)).toBe(false); 174 174 }); 175 175 }); 176 + 177 + // ===================================================================== 178 + // Edge cases 179 + // ===================================================================== 180 + 181 + describe('extractChartData — edge cases', () => { 182 + it('extracts multiple series', () => { 183 + const values = new Map<string, unknown>([ 184 + ['A0', 'Label'], ['B0', 'Sales'], ['C0', 'Costs'], 185 + ['A1', 'Q1'], ['B1', 100], ['C1', 80], 186 + ['A2', 'Q2'], ['B2', 200], ['C2', 120], 187 + ]); 188 + const { xLabels, series } = extractChartData(values, colToLetter, 1, 2, 0, 2, 0); 189 + expect(xLabels).toEqual(['Q1', 'Q2']); 190 + expect(series).toHaveLength(2); 191 + expect(series[0].label).toBe('Sales'); 192 + expect(series[0].values).toEqual([100, 200]); 193 + expect(series[1].label).toBe('Costs'); 194 + expect(series[1].values).toEqual([80, 120]); 195 + }); 196 + 197 + it('handles missing cell values as 0 for series', () => { 198 + const values = new Map<string, unknown>([ 199 + ['A1', 'Q1'], // B1 is missing 200 + ]); 201 + const { series } = extractChartData(values, colToLetter, 1, 1, 0, 1); 202 + expect(series[0].values).toEqual([0]); 203 + }); 204 + 205 + it('handles missing label values as empty string', () => { 206 + const values = new Map<string, unknown>([ 207 + ['B1', 42], // A1 label missing 208 + ]); 209 + const { xLabels } = extractChartData(values, colToLetter, 1, 1, 0, 1); 210 + expect(xLabels).toEqual(['']); 211 + }); 212 + 213 + it('extracts single row of data', () => { 214 + const values = new Map<string, unknown>([ 215 + ['A1', 'Only'], ['B1', 99], 216 + ]); 217 + const { xLabels, series } = extractChartData(values, colToLetter, 1, 1, 0, 1); 218 + expect(xLabels).toHaveLength(1); 219 + expect(series).toHaveLength(1); 220 + expect(series[0].values).toEqual([99]); 221 + }); 222 + }); 223 + 224 + describe('computeBarLayout — edge cases', () => { 225 + it('handles all zero values (minimum max of 1)', () => { 226 + const series = [{ label: 'A', values: [0, 0, 0] }]; 227 + const bars = computeBarLayout(series); 228 + expect(bars).toHaveLength(3); 229 + for (const bar of bars) { 230 + expect(bar.heightPct).toBe(0); 231 + } 232 + }); 233 + 234 + it('handles single value', () => { 235 + const bars = computeBarLayout([{ label: 'A', values: [42] }]); 236 + expect(bars).toHaveLength(1); 237 + expect(bars[0].heightPct).toBe(100); 238 + }); 239 + 240 + it('handles empty series array', () => { 241 + const bars = computeBarLayout([]); 242 + expect(bars).toEqual([]); 243 + }); 244 + 245 + it('handles series with empty values', () => { 246 + const bars = computeBarLayout([{ label: 'A', values: [] }]); 247 + expect(bars).toEqual([]); 248 + }); 249 + 250 + it('computes correct indices for multiple series', () => { 251 + const series = [ 252 + { label: 'A', values: [10, 20] }, 253 + { label: 'B', values: [30] }, 254 + ]; 255 + const bars = computeBarLayout(series); 256 + expect(bars).toHaveLength(3); 257 + expect(bars[0]).toMatchObject({ seriesIndex: 0, barIndex: 0, value: 10 }); 258 + expect(bars[1]).toMatchObject({ seriesIndex: 0, barIndex: 1, value: 20 }); 259 + expect(bars[2]).toMatchObject({ seriesIndex: 1, barIndex: 0, value: 30 }); 260 + }); 261 + }); 262 + 263 + describe('computePieSlices — edge cases', () => { 264 + it('handles two equal values', () => { 265 + const slices = computePieSlices([50, 50]); 266 + expect(slices).toHaveLength(2); 267 + expect(slices[0].percentage).toBeCloseTo(0.5); 268 + expect(slices[1].percentage).toBeCloseTo(0.5); 269 + expect(slices[0].endAngle).toBeCloseTo(180); 270 + expect(slices[1].endAngle).toBeCloseTo(360); 271 + }); 272 + 273 + it('returns empty for empty array', () => { 274 + expect(computePieSlices([])).toEqual([]); 275 + }); 276 + 277 + it('handles many small equal slices', () => { 278 + const values = Array(10).fill(1); 279 + const slices = computePieSlices(values); 280 + expect(slices).toHaveLength(10); 281 + for (const s of slices) { 282 + expect(s.percentage).toBeCloseTo(0.1); 283 + } 284 + expect(slices[9].endAngle).toBeCloseTo(360); 285 + }); 286 + }); 287 + 288 + describe('resizeEmbed — edge cases', () => { 289 + it('clamps to exactly 100 when given 100', () => { 290 + const embed = createChartEmbed('c1', 'd1', 0); 291 + const resized = resizeEmbed(embed, 100, 100); 292 + expect(resized.width).toBe(100); 293 + expect(resized.height).toBe(100); 294 + }); 295 + 296 + it('clamps negative values to 100', () => { 297 + const embed = createChartEmbed('c1', 'd1', 0); 298 + const resized = resizeEmbed(embed, -50, -100); 299 + expect(resized.width).toBe(100); 300 + expect(resized.height).toBe(100); 301 + }); 302 + 303 + it('preserves other embed properties', () => { 304 + const embed = createChartEmbed('c1', 'd1', 42, 600, 400); 305 + const resized = resizeEmbed(embed, 800, 500); 306 + expect(resized.chartId).toBe('c1'); 307 + expect(resized.documentId).toBe('d1'); 308 + expect(resized.position).toBe(42); 309 + }); 310 + }); 311 + 312 + describe('createChartEmbed — edge cases', () => { 313 + it('accepts custom dimensions', () => { 314 + const embed = createChartEmbed('c1', 'd1', 50, 800, 600); 315 + expect(embed.width).toBe(800); 316 + expect(embed.height).toBe(600); 317 + }); 318 + }); 319 + 320 + describe('changeChartType — edge cases', () => { 321 + it('preserves all other config fields', () => { 322 + const config = createChartConfig('bar', 'Test', ['A', 'B'], [{ label: 'S1', values: [1, 2] }], 's1', 'A1:B3'); 323 + const changed = changeChartType(config, 'line'); 324 + expect(changed.type).toBe('line'); 325 + expect(changed.title).toBe('Test'); 326 + expect(changed.xLabels).toEqual(['A', 'B']); 327 + expect(changed.series).toEqual(config.series); 328 + expect(changed.id).toBe(config.id); 329 + }); 330 + }); 331 + 332 + describe('totalDataPoints — edge cases', () => { 333 + it('returns 0 for config with no series', () => { 334 + const config = createChartConfig('bar', 'Empty', [], [], 's1', 'A1:A1'); 335 + expect(totalDataPoints(config)).toBe(0); 336 + }); 337 + 338 + it('returns 0 for series with empty values', () => { 339 + const config = createChartConfig('bar', 'Empty', [], [{ label: 'S1', values: [] }], 's1', 'A1:A1'); 340 + expect(totalDataPoints(config)).toBe(0); 341 + }); 342 + }); 343 + 344 + describe('isChartValid — edge cases', () => { 345 + it('returns true for single data point', () => { 346 + const config = createChartConfig('pie', 'Single', ['X'], [{ label: 'S', values: [1] }], 's1', 'A1:B1'); 347 + expect(isChartValid(config)).toBe(true); 348 + }); 349 + 350 + it('returns false when all series have empty values', () => { 351 + const config = createChartConfig('bar', 'Multi', [], [ 352 + { label: 'S1', values: [] }, 353 + { label: 'S2', values: [] }, 354 + ], 's1', 'A1:C1'); 355 + expect(isChartValid(config)).toBe(false); 356 + }); 357 + 358 + it('returns true when at least one series has values', () => { 359 + const config = createChartConfig('bar', 'Mixed', [], [ 360 + { label: 'S1', values: [] }, 361 + { label: 'S2', values: [1] }, 362 + ], 's1', 'A1:C1'); 363 + expect(isChartValid(config)).toBe(true); 364 + }); 365 + });
+82
tests/command-palette.test.ts
··· 146 146 expect(navigateIndex(0, 1, 'up')).toBe(0); 147 147 }); 148 148 }); 149 + 150 + // ===================================================================== 151 + // Edge cases 152 + // ===================================================================== 153 + 154 + describe('fuzzyMatch — edge cases', () => { 155 + it('handles special regex characters in query', () => { 156 + expect(fuzzyMatch('File (copy)', '(copy)')).toBe(true); 157 + expect(fuzzyMatch('test [bracket]', '[bracket]')).toBe(true); 158 + expect(fuzzyMatch('pattern .*+?', '.*+?')).toBe(true); 159 + }); 160 + 161 + it('handles query with repeated words', () => { 162 + expect(fuzzyMatch('Bold Bold Text', 'bold bold')).toBe(true); 163 + }); 164 + 165 + it('returns false for query that partially overlaps', () => { 166 + expect(fuzzyMatch('New Document', 'new spreadsheet')).toBe(false); 167 + }); 168 + 169 + it('handles single character query', () => { 170 + expect(fuzzyMatch('Bold', 'b')).toBe(true); 171 + expect(fuzzyMatch('Bold', 'z')).toBe(false); 172 + }); 173 + 174 + it('handles label with numbers', () => { 175 + expect(fuzzyMatch('Q1 Budget 2026', '2026')).toBe(true); 176 + expect(fuzzyMatch('Q1 Budget 2026', 'q1 2026')).toBe(true); 177 + }); 178 + }); 179 + 180 + describe('filterActions — edge cases', () => { 181 + it('returns empty array when filtering empty actions', () => { 182 + expect(filterActions([], 'anything')).toEqual([]); 183 + }); 184 + 185 + it('preserves action properties in results', () => { 186 + const result = filterActions(actions, 'bold'); 187 + expect(result[0].id).toBe('bold'); 188 + expect(result[0].shortcut).toBe('\u2318B'); 189 + expect(result[0].category).toBe('action'); 190 + }); 191 + 192 + it('matches across all fields of label only (not id)', () => { 193 + // 'new-doc' is in the ID but 'New Document' is the label 194 + const result = filterActions(actions, 'doc'); 195 + // Should match 'New Document' by label 196 + expect(result.some(a => a.id === 'new-doc')).toBe(true); 197 + }); 198 + }); 199 + 200 + describe('groupByCategory — edge cases', () => { 201 + it('handles all actions in one category', () => { 202 + const actionOnly = actions.filter(a => a.category === 'action'); 203 + const groups = groupByCategory(actionOnly); 204 + expect(groups).toHaveLength(1); 205 + expect(groups[0].category).toBe('Actions'); 206 + }); 207 + 208 + it('groups items maintain original order', () => { 209 + const groups = groupByCategory(actions); 210 + const docItems = groups.find(g => g.category === 'Documents')!.items; 211 + expect(docItems.map(i => i.id)).toEqual(['doc-1', 'doc-2', 'doc-3']); 212 + }); 213 + }); 214 + 215 + describe('navigateIndex — edge cases', () => { 216 + it('handles large total with wrap from end', () => { 217 + expect(navigateIndex(99, 100, 'down')).toBe(0); 218 + }); 219 + 220 + it('handles large total with wrap from start', () => { 221 + expect(navigateIndex(0, 100, 'up')).toBe(99); 222 + }); 223 + 224 + it('mid-list navigation is consistent', () => { 225 + // Down then up returns to same position 226 + const after = navigateIndex(5, 10, 'down'); 227 + const back = navigateIndex(after, 10, 'up'); 228 + expect(back).toBe(5); 229 + }); 230 + });
+94
tests/version-panel.test.ts
··· 199 199 expect(result.label).toBe('+50000'); 200 200 }); 201 201 }); 202 + 203 + // ===================================================================== 204 + // Edge cases 205 + // ===================================================================== 206 + 207 + describe('formatRelativeTime — edge cases', () => { 208 + it('returns "Unknown" for undefined-like input', () => { 209 + expect(formatRelativeTime(null as unknown as string)).toBe('Unknown'); 210 + }); 211 + 212 + it('returns Unknown for timezone offset format (only Z-suffix supported)', () => { 213 + // formatRelativeTime appends Z to non-Z strings, making +05:00Z invalid 214 + const result = formatRelativeTime('2025-06-15T10:00:00+05:00'); 215 + expect(result).toBe('Unknown'); 216 + }); 217 + 218 + it('returns correct format for exactly 59 seconds ago', () => { 219 + const almost1Min = new Date(Date.now() - 59 * 1000); 220 + expect(formatRelativeTime(almost1Min.toISOString())).toBe('Just now'); 221 + }); 222 + 223 + it('returns "N min ago" at exactly 60 seconds', () => { 224 + const exactly1Min = new Date(Date.now() - 60 * 1000); 225 + expect(formatRelativeTime(exactly1Min.toISOString())).toBe('1 min ago'); 226 + }); 227 + 228 + it('returns "N hours ago" at exactly 60 minutes', () => { 229 + const exactly1Hr = new Date(Date.now() - 60 * 60 * 1000); 230 + expect(formatRelativeTime(exactly1Hr.toISOString())).toBe('1 hour ago'); 231 + }); 232 + 233 + it('returns "Yesterday" at exactly 24 hours', () => { 234 + const exactly24h = new Date(Date.now() - 24 * 60 * 60 * 1000); 235 + expect(formatRelativeTime(exactly24h.toISOString())).toBe('Yesterday'); 236 + }); 237 + 238 + it('returns "Mon DD" at exactly 48 hours', () => { 239 + const exactly48h = new Date(Date.now() - 48 * 60 * 60 * 1000); 240 + const result = formatRelativeTime(exactly48h.toISOString()); 241 + // Should be "Mon DD" format, not "Yesterday" 242 + expect(result).toMatch(/^[A-Z][a-z]{2} \d{1,2}$/); 243 + }); 244 + 245 + it('handles very old date (year 2000)', () => { 246 + const result = formatRelativeTime('2000-07-04T12:00:00Z'); 247 + expect(result).toBe('Jul 4'); 248 + }); 249 + }); 250 + 251 + describe('parseMetadata — edge cases', () => { 252 + it('returns empty object for JSON boolean', () => { 253 + expect(parseMetadata('true')).toEqual({}); 254 + expect(parseMetadata('false')).toEqual({}); 255 + }); 256 + 257 + it('returns empty object for JSON null', () => { 258 + expect(parseMetadata('null')).toEqual({}); 259 + }); 260 + 261 + it('handles object with array values', () => { 262 + const result = parseMetadata('{"tags":["a","b"]}'); 263 + expect(result).toEqual({ tags: ['a', 'b'] }); 264 + }); 265 + 266 + it('handles deeply nested objects', () => { 267 + const result = parseMetadata('{"a":{"b":{"c":{"d":1}}}}'); 268 + expect((result['a'] as Record<string, unknown>)['b']).toBeDefined(); 269 + }); 270 + }); 271 + 272 + describe('computeDiffStats — edge cases', () => { 273 + it('handles wordCount as null (treated as 0 via ??)', () => { 274 + const result = computeDiffStats({ wordCount: null as unknown as number }, { wordCount: 10 }); 275 + expect(result.delta).toBe(-10); 276 + }); 277 + 278 + it('handles negative word counts gracefully', () => { 279 + const result = computeDiffStats({ wordCount: -5 }, { wordCount: 10 }); 280 + expect(result.delta).toBe(-15); 281 + expect(result.label).toBe('-15'); 282 + }); 283 + 284 + it('handles first version with zero words', () => { 285 + const result = computeDiffStats({ wordCount: 0 }, undefined); 286 + expect(result.delta).toBe(0); 287 + expect(result.label).toBe('+0'); 288 + }); 289 + 290 + it('handles transition from non-zero to zero', () => { 291 + const result = computeDiffStats({ wordCount: 0 }, { wordCount: 50 }); 292 + expect(result.delta).toBe(-50); 293 + expect(result.label).toBe('-50'); 294 + }); 295 + });