Approval-based snapshot testing library for Go (mirror)
at main 1009 lines 27 kB view raw
1package pretty_test 2 3import ( 4 "fmt" 5 "math/rand" 6 "os" 7 "strings" 8 "testing" 9 10 "github.com/ptdewey/shutter" 11 "github.com/ptdewey/shutter/internal/diff" 12 "github.com/ptdewey/shutter/internal/files" 13 "github.com/ptdewey/shutter/internal/pretty" 14) 15 16// BoxValidation holds expected properties for validation 17type BoxValidation struct { 18 // Title and filename expectations 19 Title string 20 TestName string 21 FileName string 22 HasTitle bool 23 HasTestName bool 24 HasFileName bool 25 26 // Diff line expectations 27 ExpectedAdds []string // Lines that should appear as additions (green +) 28 ExpectedDeletes []string // Lines that should appear as deletions (red -) 29 ExpectedContext []string // Lines that should appear as context (gray │) 30 31 // Structural expectations 32 HasTopBar bool 33 HasBottomBar bool 34 MinLines int // Minimum number of content lines expected 35} 36 37// ValidateDiffBox checks that a diff box output matches expectations 38func ValidateDiffBox(t *testing.T, output string, validation BoxValidation) { 39 t.Helper() 40 41 // Remove ANSI codes for easier content checking 42 stripped := stripANSI(output) 43 44 // Check title/test/filename presence 45 if validation.HasTitle { 46 if !strings.Contains(stripped, "title: "+validation.Title) { 47 t.Errorf("Expected title '%s' not found in output", validation.Title) 48 } 49 } 50 51 if validation.HasTestName { 52 if !strings.Contains(stripped, "test: "+validation.TestName) { 53 t.Errorf("Expected test name '%s' not found in output", validation.TestName) 54 } 55 } 56 57 if validation.HasFileName { 58 if !strings.Contains(stripped, "file: "+validation.FileName) { 59 t.Errorf("Expected file name '%s' not found in output", validation.FileName) 60 } 61 } 62 63 // Check for box structure 64 if validation.HasTopBar { 65 if !strings.Contains(stripped, "┬") { 66 t.Error("Expected top bar with ┬ character") 67 } 68 } 69 70 if validation.HasBottomBar { 71 if !strings.Contains(stripped, "┴") { 72 t.Error("Expected bottom bar with ┴ character") 73 } 74 } 75 76 // Check expected additions (green + lines) 77 for _, expectedAdd := range validation.ExpectedAdds { 78 if !containsDiffLine(output, "+", expectedAdd) { 79 t.Errorf("Expected addition not found: + %s", expectedAdd) 80 } 81 } 82 83 // Check expected deletions (red - lines) 84 for _, expectedDelete := range validation.ExpectedDeletes { 85 if !containsDiffLine(output, "-", expectedDelete) { 86 t.Errorf("Expected deletion not found: - %s", expectedDelete) 87 } 88 } 89 90 // Check expected context (shared lines) 91 for _, expectedContext := range validation.ExpectedContext { 92 if !containsDiffLine(output, "│", expectedContext) { 93 t.Errorf("Expected context line not found: │ %s", expectedContext) 94 } 95 } 96 97 // Check minimum line count 98 if validation.MinLines > 0 { 99 lines := strings.Split(output, "\n") 100 contentLines := countContentLines(lines) 101 if contentLines < validation.MinLines { 102 t.Errorf("Expected at least %d content lines, got %d", validation.MinLines, contentLines) 103 } 104 } 105} 106 107// containsDiffLine checks if a line with the given prefix and content exists 108func containsDiffLine(output, prefix, content string) bool { 109 lines := strings.Split(output, "\n") 110 stripped := stripANSI(output) 111 strippedLines := strings.Split(stripped, "\n") 112 113 for i, line := range strippedLines { 114 // Check if line contains the prefix and content 115 if strings.Contains(line, prefix) && strings.Contains(line, content) { 116 // Verify the original line has proper coloring 117 originalLine := lines[i] 118 switch prefix { 119 case "+": 120 // Green additions should have ANSI codes 121 if !strings.Contains(originalLine, "\033[") { 122 continue // Skip if no color 123 } 124 case "-": 125 // Red deletions should have ANSI codes 126 if !strings.Contains(originalLine, "\033[") { 127 continue 128 } 129 case "│": 130 // Context lines may or may not have color 131 } 132 return true 133 } 134 } 135 return false 136} 137 138// countContentLines counts lines that contain diff content (not headers/borders) 139func countContentLines(lines []string) int { 140 count := 0 141 for _, line := range lines { 142 stripped := stripANSI(line) 143 // Content lines have line numbers followed by +, -, or │ 144 if strings.Contains(stripped, "+") || 145 strings.Contains(stripped, "-") || 146 strings.Contains(stripped, "│") { 147 count++ 148 } 149 } 150 return count 151} 152 153// stripANSI removes ANSI escape codes from a string 154func stripANSI(s string) string { 155 var result strings.Builder 156 inEscape := false 157 for _, r := range s { 158 if r == '\033' { 159 inEscape = true 160 continue 161 } 162 if inEscape { 163 if r == 'm' { 164 inEscape = false 165 } 166 continue 167 } 168 result.WriteRune(r) 169 } 170 return result.String() 171} 172 173// TestDiffSnapshotBox_SimpleModification tests a basic modification scenario 174func TestDiffSnapshotBox_SimpleModification(t *testing.T) { 175 os.Unsetenv("NO_COLOR") 176 os.Setenv("COLUMNS", "100") 177 defer os.Unsetenv("COLUMNS") 178 179 oldContent := "line1\nline2\nline3" 180 newContent := "line1\nmodified\nline3" 181 182 oldSnap := &files.Snapshot{ 183 Title: "Simple Modification", 184 Test: "TestSimple", 185 Content: oldContent, 186 } 187 188 newSnap := &files.Snapshot{ 189 Title: "Simple Modification", 190 Test: "TestSimple", 191 Content: newContent, 192 } 193 194 diffLines := diff.Histogram(oldContent, newContent) 195 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 196 197 validation := BoxValidation{ 198 Title: "Simple Modification", 199 TestName: "TestSimple", 200 FileName: "simple_modification.snap", 201 HasTitle: true, 202 HasTestName: true, 203 HasFileName: true, 204 ExpectedAdds: []string{"modified"}, 205 ExpectedDeletes: []string{"line2"}, 206 ExpectedContext: []string{"line1", "line3"}, 207 HasTopBar: true, 208 HasBottomBar: true, 209 MinLines: 4, // 1 shared + 1 delete + 1 add + 1 shared 210 } 211 212 ValidateDiffBox(t, result, validation) 213} 214 215// TestDiffSnapshotBox_PureAddition tests adding lines only 216func TestDiffSnapshotBox_PureAddition(t *testing.T) { 217 os.Unsetenv("NO_COLOR") 218 os.Setenv("COLUMNS", "100") 219 defer os.Unsetenv("COLUMNS") 220 221 oldContent := "line1\nline2" 222 newContent := "line1\nline2\nline3\nline4" 223 224 oldSnap := &files.Snapshot{ 225 Title: "Pure Addition", 226 Test: "TestAddition", 227 Content: oldContent, 228 } 229 230 newSnap := &files.Snapshot{ 231 Title: "Pure Addition", 232 Test: "TestAddition", 233 Content: newContent, 234 } 235 236 diffLines := diff.Histogram(oldContent, newContent) 237 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 238 239 validation := BoxValidation{ 240 Title: "Pure Addition", 241 TestName: "TestAddition", 242 FileName: "pure_addition.snap", 243 HasTitle: true, 244 HasTestName: true, 245 HasFileName: true, 246 ExpectedAdds: []string{"line3", "line4"}, 247 ExpectedDeletes: []string{}, 248 ExpectedContext: []string{"line1", "line2"}, 249 HasTopBar: true, 250 HasBottomBar: true, 251 MinLines: 4, // 2 shared + 2 adds 252 } 253 254 ValidateDiffBox(t, result, validation) 255} 256 257// TestDiffSnapshotBox_PureDeletion tests deleting lines only 258func TestDiffSnapshotBox_PureDeletion(t *testing.T) { 259 os.Unsetenv("NO_COLOR") 260 os.Setenv("COLUMNS", "100") 261 defer os.Unsetenv("COLUMNS") 262 263 oldContent := "line1\nline2\nline3\nline4" 264 newContent := "line1\nline2" 265 266 oldSnap := &files.Snapshot{ 267 Title: "Pure Deletion", 268 Test: "TestDeletion", 269 Content: oldContent, 270 } 271 272 newSnap := &files.Snapshot{ 273 Title: "Pure Deletion", 274 Test: "TestDeletion", 275 Content: newContent, 276 } 277 278 diffLines := diff.Histogram(oldContent, newContent) 279 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 280 281 validation := BoxValidation{ 282 Title: "Pure Deletion", 283 TestName: "TestDeletion", 284 FileName: "pure_deletion.snap", 285 HasTitle: true, 286 HasTestName: true, 287 HasFileName: true, 288 ExpectedAdds: []string{}, 289 ExpectedDeletes: []string{"line3", "line4"}, 290 ExpectedContext: []string{"line1", "line2"}, 291 HasTopBar: true, 292 HasBottomBar: true, 293 MinLines: 4, // 2 shared + 2 deletes 294 } 295 296 ValidateDiffBox(t, result, validation) 297} 298 299// TestDiffSnapshotBox_ComplexMixed tests multiple types of changes 300func TestDiffSnapshotBox_ComplexMixed(t *testing.T) { 301 os.Unsetenv("NO_COLOR") 302 os.Setenv("COLUMNS", "120") 303 defer os.Unsetenv("COLUMNS") 304 305 oldContent := `unchanged1 306delete1 307delete2 308unchanged2 309modify_old 310unchanged3` 311 312 newContent := `unchanged1 313unchanged2 314modify_new 315add1 316unchanged3 317add2` 318 319 oldSnap := &files.Snapshot{ 320 Title: "Complex Mixed", 321 Test: "TestComplexMixed", 322 Content: oldContent, 323 } 324 325 newSnap := &files.Snapshot{ 326 Title: "Complex Mixed", 327 Test: "TestComplexMixed", 328 Content: newContent, 329 } 330 331 diffLines := diff.Histogram(oldContent, newContent) 332 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 333 334 validation := BoxValidation{ 335 Title: "Complex Mixed", 336 TestName: "TestComplexMixed", 337 FileName: "complex_mixed.snap", 338 HasTitle: true, 339 HasTestName: true, 340 HasFileName: true, 341 ExpectedAdds: []string{"modify_new", "add1", "add2"}, 342 ExpectedDeletes: []string{"delete1", "delete2", "modify_old"}, 343 ExpectedContext: []string{"unchanged1", "unchanged2", "unchanged3"}, 344 HasTopBar: true, 345 HasBottomBar: true, 346 MinLines: 9, // 3 shared + 3 deletes + 3 adds 347 } 348 349 ValidateDiffBox(t, result, validation) 350} 351 352// TestDiffSnapshotBox_EmptyOld tests diff from empty to content 353func TestDiffSnapshotBox_EmptyOld(t *testing.T) { 354 os.Unsetenv("NO_COLOR") 355 os.Setenv("COLUMNS", "100") 356 defer os.Unsetenv("COLUMNS") 357 358 oldContent := "" 359 newContent := "line1\nline2\nline3" 360 361 oldSnap := &files.Snapshot{ 362 Title: "Empty to Content", 363 Test: "TestEmptyOld", 364 Content: oldContent, 365 } 366 367 newSnap := &files.Snapshot{ 368 Title: "Empty to Content", 369 Test: "TestEmptyOld", 370 Content: newContent, 371 } 372 373 diffLines := diff.Histogram(oldContent, newContent) 374 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 375 376 validation := BoxValidation{ 377 Title: "Empty to Content", 378 TestName: "TestEmptyOld", 379 FileName: "empty_to_content.snap", 380 HasTitle: true, 381 HasTestName: true, 382 HasFileName: true, 383 ExpectedAdds: []string{"line1", "line2", "line3"}, 384 ExpectedDeletes: []string{}, 385 ExpectedContext: []string{}, 386 HasTopBar: true, 387 HasBottomBar: true, 388 MinLines: 3, // 3 adds 389 } 390 391 ValidateDiffBox(t, result, validation) 392} 393 394// TestDiffSnapshotBox_EmptyNew tests diff from content to empty 395func TestDiffSnapshotBox_EmptyNew(t *testing.T) { 396 os.Unsetenv("NO_COLOR") 397 os.Setenv("COLUMNS", "100") 398 defer os.Unsetenv("COLUMNS") 399 400 oldContent := "line1\nline2\nline3" 401 newContent := "" 402 403 oldSnap := &files.Snapshot{ 404 Title: "Content to Empty", 405 Test: "TestEmptyNew", 406 Content: oldContent, 407 } 408 409 newSnap := &files.Snapshot{ 410 Title: "Content to Empty", 411 Test: "TestEmptyNew", 412 Content: newContent, 413 } 414 415 diffLines := diff.Histogram(oldContent, newContent) 416 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 417 418 validation := BoxValidation{ 419 Title: "Content to Empty", 420 TestName: "TestEmptyNew", 421 FileName: "content_to_empty.snap", 422 HasTitle: true, 423 HasTestName: true, 424 HasFileName: true, 425 ExpectedAdds: []string{}, 426 ExpectedDeletes: []string{"line1", "line2", "line3"}, 427 ExpectedContext: []string{}, 428 HasTopBar: true, 429 HasBottomBar: true, 430 MinLines: 3, // 3 deletes 431 } 432 433 ValidateDiffBox(t, result, validation) 434} 435 436// TestDiffSnapshotBox_NoTitle tests snapshot without title 437func TestDiffSnapshotBox_NoTitle(t *testing.T) { 438 os.Unsetenv("NO_COLOR") 439 os.Setenv("COLUMNS", "100") 440 defer os.Unsetenv("COLUMNS") 441 442 oldContent := "old" 443 newContent := "new" 444 445 oldSnap := &files.Snapshot{ 446 Title: "", 447 Test: "TestNoTitle", 448 Content: oldContent, 449 } 450 451 newSnap := &files.Snapshot{ 452 Title: "", 453 Test: "TestNoTitle", 454 Content: newContent, 455 } 456 457 diffLines := diff.Histogram(oldContent, newContent) 458 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 459 460 stripped := stripANSI(result) 461 462 // Should NOT contain "title:" line 463 if strings.Contains(stripped, "title:") { 464 t.Error("Expected no title line when title is empty") 465 } 466 467 // Should still contain test and file 468 if !strings.Contains(stripped, "test: TestNoTitle") { 469 t.Error("Expected test name to be present") 470 } 471 // When title is empty, filename should be based on test name 472 if !strings.Contains(stripped, "file: .snap") { 473 t.Error("Expected file name to be present") 474 } 475} 476 477// TestDiffSnapshotBox_LargeLineNumbers tests proper padding for multi-digit line numbers 478func TestDiffSnapshotBox_LargeLineNumbers(t *testing.T) { 479 os.Unsetenv("NO_COLOR") 480 os.Setenv("COLUMNS", "120") 481 defer os.Unsetenv("COLUMNS") 482 483 // Create content with 100+ lines to test 3-digit line numbers 484 oldLines := make([]string, 105) 485 newLines := make([]string, 105) 486 for i := 0; i < 105; i++ { 487 oldLines[i] = fmt.Sprintf("line %d", i+1) 488 newLines[i] = fmt.Sprintf("line %d", i+1) 489 } 490 // Modify lines 50, 75, and 100 491 oldLines[49] = "old line 50" 492 newLines[49] = "new line 50" 493 oldLines[74] = "old line 75" 494 newLines[74] = "new line 75" 495 oldLines[99] = "old line 100" 496 newLines[99] = "new line 100" 497 498 oldContent := strings.Join(oldLines, "\n") 499 newContent := strings.Join(newLines, "\n") 500 501 oldSnap := &files.Snapshot{ 502 Title: "Large Line Numbers", 503 Test: "TestLargeLineNumbers", 504 Content: oldContent, 505 } 506 507 newSnap := &files.Snapshot{ 508 Title: "Large Line Numbers", 509 Test: "TestLargeLineNumbers", 510 Content: newContent, 511 } 512 513 diffLines := diff.Histogram(oldContent, newContent) 514 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 515 516 stripped := stripANSI(result) 517 518 // Check that 3-digit line numbers appear 519 if !strings.Contains(stripped, "100") { 520 t.Error("Expected 3-digit line number 100 to appear") 521 } 522 523 // Validate line number alignment by checking that numbers are right-aligned 524 // Line 1 should have padding for 3 digits 525 lines := strings.Split(stripped, "\n") 526 foundSingleDigit := false 527 foundTripleDigit := false 528 529 for _, line := range lines { 530 // Look for lines with content markers 531 if strings.Contains(line, "│") || strings.Contains(line, "+") || strings.Contains(line, "-") { 532 // Single digit should have padding (e.g., " 1" or " 2") 533 if strings.Contains(line, " 1 ") || strings.Contains(line, " 2 ") { 534 foundSingleDigit = true 535 } 536 // Triple digit should align (e.g., "100" or "105") 537 if strings.Contains(line, "100 ") || strings.Contains(line, "105 ") { 538 foundTripleDigit = true 539 } 540 } 541 } 542 543 if !foundSingleDigit { 544 t.Error("Expected to find padded single-digit line numbers") 545 } 546 if !foundTripleDigit { 547 t.Error("Expected to find triple-digit line numbers") 548 } 549} 550 551// TestDiffSnapshotBox_UnicodeContent tests diff with unicode characters 552func TestDiffSnapshotBox_UnicodeContent(t *testing.T) { 553 os.Unsetenv("NO_COLOR") 554 os.Setenv("COLUMNS", "100") 555 defer os.Unsetenv("COLUMNS") 556 557 oldContent := "Hello 世界\nこんにちは\n🎉 emoji" 558 newContent := "Hello 世界\nさようなら\n🎊 party" 559 560 oldSnap := &files.Snapshot{ 561 Title: "Unicode Test", 562 Test: "TestUnicode", 563 Content: oldContent, 564 } 565 566 newSnap := &files.Snapshot{ 567 Title: "Unicode Test", 568 Test: "TestUnicode", 569 Content: newContent, 570 } 571 572 diffLines := diff.Histogram(oldContent, newContent) 573 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 574 575 validation := BoxValidation{ 576 Title: "Unicode Test", 577 TestName: "TestUnicode", 578 FileName: "unicode_test.snap", 579 HasTitle: true, 580 HasTestName: true, 581 HasFileName: true, 582 ExpectedAdds: []string{"さようなら", "🎊 party"}, 583 ExpectedDeletes: []string{"こんにちは", "🎉 emoji"}, 584 ExpectedContext: []string{"Hello 世界"}, 585 HasTopBar: true, 586 HasBottomBar: true, 587 MinLines: 5, // 1 shared + 2 deletes + 2 adds 588 } 589 590 ValidateDiffBox(t, result, validation) 591} 592 593// TestNewSnapshotBox_Basic tests the new snapshot box rendering 594func TestNewSnapshotBox_Basic(t *testing.T) { 595 os.Unsetenv("NO_COLOR") 596 os.Setenv("COLUMNS", "100") 597 defer os.Unsetenv("COLUMNS") 598 599 content := "line1\nline2\nline3" 600 601 snap := &files.Snapshot{ 602 Title: "New Snapshot", 603 Test: "TestNewSnapshot", 604 FileName: "test_new.snap", 605 Content: content, 606 } 607 608 result := pretty.NewSnapshotBox(snap) 609 610 stripped := stripANSI(result) 611 612 // Check header 613 if !strings.Contains(stripped, "New Snapshot") { 614 t.Error("Expected 'New Snapshot' header") 615 } 616 617 // Check metadata 618 if !strings.Contains(stripped, "title: New Snapshot") { 619 t.Error("Expected title in output") 620 } 621 if !strings.Contains(stripped, "test: TestNewSnapshot") { 622 t.Error("Expected test name in output") 623 } 624 if !strings.Contains(stripped, "file: test_new.snap") { 625 t.Error("Expected file name in output") 626 } 627 628 // Check content lines (all should be green additions) 629 if !containsDiffLine(result, "+", "line1") { 630 t.Error("Expected line1 as addition") 631 } 632 if !containsDiffLine(result, "+", "line2") { 633 t.Error("Expected line2 as addition") 634 } 635 if !containsDiffLine(result, "+", "line3") { 636 t.Error("Expected line3 as addition") 637 } 638 639 // Check box structure 640 if !strings.Contains(stripped, "┬") { 641 t.Error("Expected top bar with ┬") 642 } 643 if !strings.Contains(stripped, "┴") { 644 t.Error("Expected bottom bar with ┴") 645 } 646} 647 648// TestNewSnapshotBox_EmptyContent tests new snapshot with empty content 649func TestNewSnapshotBox_EmptyContent(t *testing.T) { 650 os.Unsetenv("NO_COLOR") 651 os.Setenv("COLUMNS", "100") 652 defer os.Unsetenv("COLUMNS") 653 654 snap := &files.Snapshot{ 655 Title: "Empty Snapshot", 656 Test: "TestEmpty", 657 FileName: "test_empty.snap", 658 Content: "", 659 } 660 661 result := pretty.NewSnapshotBox(snap) 662 663 // Should still render box with metadata, just no content lines 664 stripped := stripANSI(result) 665 666 if !strings.Contains(stripped, "title: Empty Snapshot") { 667 t.Error("Expected title in output") 668 } 669 670 // Should have box structure even with empty content 671 if !strings.Contains(stripped, "┬") { 672 t.Error("Expected top bar with ┬") 673 } 674 if !strings.Contains(stripped, "┴") { 675 t.Error("Expected bottom bar with ┴") 676 } 677} 678 679// Snapshot testing for visual regression 680 681func TestDiffSnapshotBox_VisualRegression_SimpleModification(t *testing.T) { 682 os.Unsetenv("NO_COLOR") 683 os.Setenv("COLUMNS", "100") 684 defer os.Unsetenv("COLUMNS") 685 686 oldContent := "line1\nline2\nline3" 687 newContent := "line1\nmodified\nline3" 688 689 oldSnap := &files.Snapshot{ 690 Title: "Visual Test", 691 Test: "TestVisualSimple", 692 Content: oldContent, 693 } 694 695 newSnap := &files.Snapshot{ 696 Title: "Visual Test", 697 Test: "TestVisualSimple", 698 Content: newContent, 699 } 700 701 diffLines := diff.Histogram(oldContent, newContent) 702 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 703 704 shutter.SnapString(t, "diff_box_simple_modification", result) 705} 706 707func TestDiffSnapshotBox_VisualRegression_ComplexMixed(t *testing.T) { 708 os.Unsetenv("NO_COLOR") 709 os.Setenv("COLUMNS", "120") 710 defer os.Unsetenv("COLUMNS") 711 712 oldContent := `unchanged1 713delete1 714delete2 715unchanged2 716modify_old 717unchanged3` 718 719 newContent := `unchanged1 720unchanged2 721modify_new 722add1 723unchanged3 724add2` 725 726 oldSnap := &files.Snapshot{ 727 Title: "Visual Complex", 728 Test: "TestVisualComplex", 729 Content: oldContent, 730 } 731 732 newSnap := &files.Snapshot{ 733 Title: "Visual Complex", 734 Test: "TestVisualComplex", 735 Content: newContent, 736 } 737 738 diffLines := diff.Histogram(oldContent, newContent) 739 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 740 741 shutter.SnapString(t, "diff_box_complex_mixed", result) 742} 743 744func TestDiffSnapshotBox_VisualRegression_LargeLineNumbers(t *testing.T) { 745 os.Unsetenv("NO_COLOR") 746 os.Setenv("COLUMNS", "120") 747 defer os.Unsetenv("COLUMNS") 748 749 // Create content with 100+ lines 750 oldLines := make([]string, 105) 751 newLines := make([]string, 105) 752 for i := 0; i < 105; i++ { 753 oldLines[i] = fmt.Sprintf("line %d", i+1) 754 newLines[i] = fmt.Sprintf("line %d", i+1) 755 } 756 oldLines[49] = "old line 50" 757 newLines[49] = "new line 50" 758 oldLines[99] = "old line 100" 759 newLines[99] = "new line 100" 760 761 oldContent := strings.Join(oldLines, "\n") 762 newContent := strings.Join(newLines, "\n") 763 764 oldSnap := &files.Snapshot{ 765 Title: "Large Line Numbers", 766 Test: "TestVisualLarge", 767 Content: oldContent, 768 } 769 770 newSnap := &files.Snapshot{ 771 Title: "Large Line Numbers", 772 Test: "TestVisualLarge", 773 Content: newContent, 774 } 775 776 diffLines := diff.Histogram(oldContent, newContent) 777 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 778 779 shutter.SnapString(t, "diff_box_large_line_numbers", result) 780} 781 782func TestNewSnapshotBox_VisualRegression(t *testing.T) { 783 os.Unsetenv("NO_COLOR") 784 os.Setenv("COLUMNS", "100") 785 defer os.Unsetenv("COLUMNS") 786 787 content := "line1\nline2\nline3\nline4\nline5" 788 789 snap := &files.Snapshot{ 790 Title: "New Snapshot Visual", 791 Test: "TestNewVisual", 792 FileName: "test_new_visual.snap", 793 Content: content, 794 } 795 796 result := pretty.NewSnapshotBox(snap) 797 798 shutter.SnapString(t, "new_snapshot_box", result) 799} 800 801// Randomized testing 802 803func TestDiffSnapshotBox_Random_Additions(t *testing.T) { 804 os.Unsetenv("NO_COLOR") 805 os.Setenv("COLUMNS", "120") 806 defer os.Unsetenv("COLUMNS") 807 808 rng := rand.New(rand.NewSource(12345)) // Fixed seed for reproducibility 809 810 for i := 0; i < 10; i++ { 811 t.Run(fmt.Sprintf("random_addition_%d", i), func(t *testing.T) { 812 // Generate random number of old lines (5-20) 813 numOldLines := rng.Intn(16) + 5 814 oldLines := make([]string, numOldLines) 815 for j := 0; j < numOldLines; j++ { 816 oldLines[j] = fmt.Sprintf("old_line_%d", j+1) 817 } 818 819 // Add random number of new lines (1-10) 820 numNewLines := rng.Intn(10) + 1 821 newLines := make([]string, numOldLines+numNewLines) 822 copy(newLines, oldLines) 823 for j := 0; j < numNewLines; j++ { 824 newLines[numOldLines+j] = fmt.Sprintf("new_line_%d", j+1) 825 } 826 827 oldContent := strings.Join(oldLines, "\n") 828 newContent := strings.Join(newLines, "\n") 829 830 oldSnap := &files.Snapshot{ 831 Title: fmt.Sprintf("Random Addition %d", i), 832 Test: fmt.Sprintf("TestRandomAdd_%d", i), 833 Content: oldContent, 834 } 835 836 newSnap := &files.Snapshot{ 837 Title: fmt.Sprintf("Random Addition %d", i), 838 Test: fmt.Sprintf("TestRandomAdd_%d", i), 839 Content: newContent, 840 } 841 842 diffLines := diff.Histogram(oldContent, newContent) 843 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 844 845 // Validate structure 846 stripped := stripANSI(result) 847 848 // Should have box structure 849 if !strings.Contains(stripped, "┬") { 850 t.Error("Missing top bar") 851 } 852 if !strings.Contains(stripped, "┴") { 853 t.Error("Missing bottom bar") 854 } 855 856 // Should contain title and test name 857 if !strings.Contains(stripped, fmt.Sprintf("Random Addition %d", i)) { 858 t.Error("Missing title") 859 } 860 861 // Count additions 862 addCount := strings.Count(result, "+") 863 if addCount < numNewLines { 864 t.Errorf("Expected at least %d additions, got %d", numNewLines, addCount) 865 } 866 }) 867 } 868} 869 870func TestDiffSnapshotBox_Random_Deletions(t *testing.T) { 871 os.Unsetenv("NO_COLOR") 872 os.Setenv("COLUMNS", "120") 873 defer os.Unsetenv("COLUMNS") 874 875 rng := rand.New(rand.NewSource(54321)) 876 877 for i := 0; i < 10; i++ { 878 t.Run(fmt.Sprintf("random_deletion_%d", i), func(t *testing.T) { 879 // Generate random number of old lines (10-30) 880 numOldLines := rng.Intn(21) + 10 881 oldLines := make([]string, numOldLines) 882 for j := 0; j < numOldLines; j++ { 883 oldLines[j] = fmt.Sprintf("line_%d", j+1) 884 } 885 886 // Delete random number of lines (1-5) 887 numToDelete := rng.Intn(5) + 1 888 if numToDelete > numOldLines { 889 numToDelete = numOldLines / 2 890 } 891 newLines := make([]string, numOldLines-numToDelete) 892 copy(newLines, oldLines[:len(newLines)]) 893 894 oldContent := strings.Join(oldLines, "\n") 895 newContent := strings.Join(newLines, "\n") 896 897 oldSnap := &files.Snapshot{ 898 Title: fmt.Sprintf("Random Deletion %d", i), 899 Test: fmt.Sprintf("TestRandomDel_%d", i), 900 Content: oldContent, 901 } 902 903 newSnap := &files.Snapshot{ 904 Title: fmt.Sprintf("Random Deletion %d", i), 905 Test: fmt.Sprintf("TestRandomDel_%d", i), 906 Content: newContent, 907 } 908 909 diffLines := diff.Histogram(oldContent, newContent) 910 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 911 912 // Validate structure 913 stripped := stripANSI(result) 914 915 if !strings.Contains(stripped, "┬") { 916 t.Error("Missing top bar") 917 } 918 if !strings.Contains(stripped, "┴") { 919 t.Error("Missing bottom bar") 920 } 921 922 // Count deletions (at least numToDelete should appear) 923 delCount := strings.Count(result, "-") 924 if delCount < numToDelete { 925 t.Errorf("Expected at least %d deletions, got %d", numToDelete, delCount) 926 } 927 }) 928 } 929} 930 931func TestDiffSnapshotBox_Random_Mixed(t *testing.T) { 932 os.Unsetenv("NO_COLOR") 933 os.Setenv("COLUMNS", "140") 934 defer os.Unsetenv("COLUMNS") 935 936 rng := rand.New(rand.NewSource(99999)) 937 938 for i := 0; i < 10; i++ { 939 t.Run(fmt.Sprintf("random_mixed_%d", i), func(t *testing.T) { 940 // Generate random old content (10-30 lines) 941 numOldLines := rng.Intn(21) + 10 942 oldLines := make([]string, numOldLines) 943 for j := 0; j < numOldLines; j++ { 944 oldLines[j] = fmt.Sprintf("old_line_%d_%s", j+1, randomWord(rng)) 945 } 946 947 // Randomly modify, add, delete 948 newLines := make([]string, 0, numOldLines*2) 949 for j := 0; j < numOldLines; j++ { 950 action := rng.Intn(100) 951 if action < 70 { // 70% keep unchanged 952 newLines = append(newLines, oldLines[j]) 953 } else if action < 85 { // 15% modify 954 newLines = append(newLines, fmt.Sprintf("modified_%d_%s", j+1, randomWord(rng))) 955 } else if action < 95 { // 10% add 956 newLines = append(newLines, oldLines[j]) 957 newLines = append(newLines, fmt.Sprintf("added_%d_%s", j+1, randomWord(rng))) 958 } 959 // 5% delete (skip adding line) 960 } 961 962 oldContent := strings.Join(oldLines, "\n") 963 newContent := strings.Join(newLines, "\n") 964 965 oldSnap := &files.Snapshot{ 966 Title: fmt.Sprintf("Random Mixed %d", i), 967 Test: fmt.Sprintf("TestRandomMixed_%d", i), 968 Content: oldContent, 969 } 970 971 newSnap := &files.Snapshot{ 972 Title: fmt.Sprintf("Random Mixed %d", i), 973 Test: fmt.Sprintf("TestRandomMixed_%d", i), 974 Content: newContent, 975 } 976 977 diffLines := diff.Histogram(oldContent, newContent) 978 result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 979 980 // Validate basic structure 981 stripped := stripANSI(result) 982 983 if !strings.Contains(stripped, "┬") { 984 t.Error("Missing top bar") 985 } 986 if !strings.Contains(stripped, "┴") { 987 t.Error("Missing bottom bar") 988 } 989 if !strings.Contains(stripped, fmt.Sprintf("Random Mixed %d", i)) { 990 t.Error("Missing title") 991 } 992 993 // Should have some diff markers 994 hasPlus := strings.Contains(result, "+") 995 hasMinus := strings.Contains(result, "-") 996 hasPipe := strings.Contains(result, "│") 997 998 if !hasPlus && !hasMinus && !hasPipe { 999 t.Error("Expected at least one type of diff marker") 1000 } 1001 }) 1002 } 1003} 1004 1005// Helper function for random word generation 1006func randomWord(rng *rand.Rand) string { 1007 words := []string{"apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew"} 1008 return words[rng.Intn(len(words))] 1009}