Approval-based snapshot testing library for Go (mirror)
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}