Approval-based snapshot testing library for Go (mirror)

refactor: extract helper functions in internal packages test: add comprehensive test suite for diff box rendering

ptdewey 68588138 1847e83d

+1275 -69
+1 -1
README.md
··· 35 35 response := handleRequest(request) 36 36 37 37 // Snapshot both request and response together 38 - shutter.SnapMany(t, "request and response", []any{request, response}) 38 + shutter.SnapMany(t, "title", []any{request, response}) 39 39 } 40 40 ``` 41 41
+17 -8
internal/files/files.go
··· 92 92 } 93 93 } 94 94 95 + // getSnapshotPath returns the full path for a snapshot file 96 + func getSnapshotPath(snapTitle string, state string) (string, error) { 97 + snapshotDir, err := getSnapshotDir() 98 + if err != nil { 99 + return "", err 100 + } 101 + 102 + fileName := getSnapshotFileName(snapTitle, state) 103 + return filepath.Join(snapshotDir, fileName), nil 104 + } 105 + 95 106 func SaveSnapshot(snap *Snapshot, state string) error { 96 107 snapshotDir, err := getSnapshotDir() 97 108 if err != nil { ··· 152 163 } 153 164 154 165 func AcceptSnapshot(snapTitle string) error { 155 - snapshotDir, err := getSnapshotDir() 166 + newPath, err := getSnapshotPath(snapTitle, "new") 156 167 if err != nil { 157 168 return err 158 169 } 159 170 160 - fileName := SnapshotFileName(snapTitle) 161 - newPath := filepath.Join(snapshotDir, fileName+".snap.new") 162 - acceptedPath := filepath.Join(snapshotDir, fileName+".snap") 171 + acceptedPath, err := getSnapshotPath(snapTitle, "accepted") 172 + if err != nil { 173 + return err 174 + } 163 175 164 176 data, err := os.ReadFile(newPath) 165 177 if err != nil { ··· 174 186 } 175 187 176 188 func RejectSnapshot(snapTitle string) error { 177 - snapshotDir, err := getSnapshotDir() 189 + filePath, err := getSnapshotPath(snapTitle, "new") 178 190 if err != nil { 179 191 return err 180 192 } 181 - 182 - fileName := SnapshotFileName(snapTitle) + ".snap.new" 183 - filePath := filepath.Join(snapshotDir, fileName) 184 193 185 194 return os.Remove(filePath) 186 195 }
+23
internal/pretty/__snapshots__/diff_box_complex_mixed.snap
··· 1 + --- 2 + title: diff_box_complex_mixed 3 + test_name: TestDiffSnapshotBox_VisualRegression_ComplexMixed 4 + file_name: boxes_test.go 5 + version: 0.1.0 6 + --- 7 + ─── Snapshot Diff ───────────────────────────────────────────────────────────────────────────────────────────────────────── 8 + 9 +  title: Visual Complex 10 +  test: TestVisualComplex 11 +  file: testvisualcomplex.snap 12 + 13 + ──────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 14 + 1 │ unchanged1 15 + 2 - delete1 16 + 3 - delete2 17 + 2 │ unchanged2 18 + 5 - modify_old 19 + 3 + modify_new 20 + 4 + add1 21 + 5 │ unchanged3 22 + 6 + add2 23 + ──────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+121
internal/pretty/__snapshots__/diff_box_large_line_numbers.snap
··· 1 + --- 2 + title: diff_box_large_line_numbers 3 + test_name: TestDiffSnapshotBox_VisualRegression_LargeLineNumbers 4 + file_name: boxes_test.go 5 + version: 0.1.0 6 + --- 7 + ─── Snapshot Diff ───────────────────────────────────────────────────────────────────────────────────────────────────────── 8 + 9 +  title: Large Line Numbers 10 +  test: TestVisualLarge 11 +  file: testvisuallarge.snap 12 + 13 + ──────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────── 14 +  1 │ line 1 15 +  2 │ line 2 16 +  3 │ line 3 17 +  4 │ line 4 18 +  5 │ line 5 19 +  6 │ line 6 20 +  7 │ line 7 21 +  8 │ line 8 22 +  9 │ line 9 23 +  10 │ line 10 24 +  11 │ line 11 25 +  12 │ line 12 26 +  13 │ line 13 27 +  14 │ line 14 28 +  15 │ line 15 29 +  16 │ line 16 30 +  17 │ line 17 31 +  18 │ line 18 32 +  19 │ line 19 33 +  20 │ line 20 34 +  21 │ line 21 35 +  22 │ line 22 36 +  23 │ line 23 37 +  24 │ line 24 38 +  25 │ line 25 39 +  26 │ line 26 40 +  27 │ line 27 41 +  28 │ line 28 42 +  29 │ line 29 43 +  30 │ line 30 44 +  31 │ line 31 45 +  32 │ line 32 46 +  33 │ line 33 47 +  34 │ line 34 48 +  35 │ line 35 49 +  36 │ line 36 50 +  37 │ line 37 51 +  38 │ line 38 52 +  39 │ line 39 53 +  40 │ line 40 54 +  41 │ line 41 55 +  42 │ line 42 56 +  43 │ line 43 57 +  44 │ line 44 58 +  45 │ line 45 59 +  46 │ line 46 60 +  47 │ line 47 61 +  48 │ line 48 62 +  49 │ line 49 63 +  50 - old line 50 64 +  50 + new line 50 65 +  51 │ line 51 66 +  52 │ line 52 67 +  53 │ line 53 68 +  54 │ line 54 69 +  55 │ line 55 70 +  56 │ line 56 71 +  57 │ line 57 72 +  58 │ line 58 73 +  59 │ line 59 74 +  60 │ line 60 75 +  61 │ line 61 76 +  62 │ line 62 77 +  63 │ line 63 78 +  64 │ line 64 79 +  65 │ line 65 80 +  66 │ line 66 81 +  67 │ line 67 82 +  68 │ line 68 83 +  69 │ line 69 84 +  70 │ line 70 85 +  71 │ line 71 86 +  72 │ line 72 87 +  73 │ line 73 88 +  74 │ line 74 89 +  75 │ line 75 90 +  76 │ line 76 91 +  77 │ line 77 92 +  78 │ line 78 93 +  79 │ line 79 94 +  80 │ line 80 95 +  81 │ line 81 96 +  82 │ line 82 97 +  83 │ line 83 98 +  84 │ line 84 99 +  85 │ line 85 100 +  86 │ line 86 101 +  87 │ line 87 102 +  88 │ line 88 103 +  89 │ line 89 104 +  90 │ line 90 105 +  91 │ line 91 106 +  92 │ line 92 107 +  93 │ line 93 108 +  94 │ line 94 109 +  95 │ line 95 110 +  96 │ line 96 111 +  97 │ line 97 112 +  98 │ line 98 113 +  99 │ line 99 114 + 100 - old line 100 115 + 100 + new line 100 116 + 101 │ line 101 117 + 102 │ line 102 118 + 103 │ line 103 119 + 104 │ line 104 120 + 105 │ line 105 121 + ──────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+18
internal/pretty/__snapshots__/diff_box_simple_modification.snap
··· 1 + --- 2 + title: diff_box_simple_modification 3 + test_name: TestDiffSnapshotBox_VisualRegression_SimpleModification 4 + file_name: boxes_test.go 5 + version: 0.1.0 6 + --- 7 + ─── Snapshot Diff ───────────────────────────────────────────────────────────────────────────────────── 8 + 9 +  title: Visual Test 10 +  test: TestVisualSimple 11 +  file: testvisualsimple.snap 12 + 13 + ──────┬───────────────────────────────────────────────────────────────────────────────────────────────── 14 + 1 │ line1 15 + 2 - line2 16 + 2 + modified 17 + 3 │ line3 18 + ──────┴─────────────────────────────────────────────────────────────────────────────────────────────────
+19
internal/pretty/__snapshots__/new_snapshot_box.snap
··· 1 + --- 2 + title: new_snapshot_box 3 + test_name: TestNewSnapshotBox_VisualRegression 4 + file_name: boxes_test.go 5 + version: 0.1.0 6 + --- 7 + ─── New Snapshot ───────────────────────────────────────────────────────────────────────────────────── 8 + 9 +  title: New Snapshot Visual 10 +  test: TestNewVisual 11 +  file: test_new_visual.snap 12 + 13 + ────┬───────────────────────────────────────────────────────────────────────────────────────────────── 14 + 1 + line1 15 + 2 + line2 16 + 3 + line3 17 + 4 + line4 18 + 5 + line5 19 + ────┴─────────────────────────────────────────────────────────────────────────────────────────────────
+22 -14
internal/pretty/boxes.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 - "strconv" 6 5 "strings" 7 6 8 7 "github.com/ptdewey/shutter/internal/diff" ··· 13 12 return newSnapshotBoxInternal(snap) 14 13 } 15 14 15 + // calculateLineNumWidth returns the width needed to display line numbers 16 + func calculateLineNumWidth(maxLineNum int) int { 17 + return len(fmt.Sprintf("%d", maxLineNum)) 18 + } 19 + 20 + // formatColoredLine applies color to a line based on diff kind 21 + func formatColoredLine(line string, kind diff.DiffKind) string { 22 + switch kind { 23 + case diff.DiffOld: 24 + return Red(line) 25 + case diff.DiffNew: 26 + return Green(line) 27 + case diff.DiffShared: 28 + return line 29 + default: 30 + return line 31 + } 32 + } 33 + 16 34 func DiffSnapshotBox(old, newSnapshot *files.Snapshot, diffLines []diff.DiffLine) string { 17 35 width := TerminalWidth() 18 36 snapshotFileName := files.SnapshotFileName(newSnapshot.Test) + ".snap" ··· 49 67 if maxNewNum > maxLineNum { 50 68 maxLineNum = maxNewNum 51 69 } 52 - lineNumWidth := len(fmt.Sprintf("%d", maxLineNum)) 70 + lineNumWidth := calculateLineNumWidth(maxLineNum) 53 71 54 72 // Top bar with corner (account for both line number columns) 55 73 topBar := strings.Repeat("─", (lineNumWidth*2)+4) + "┬" + ··· 86 104 maxContentWidth := width - (lineNumWidth * 2) - 8 87 105 if len(dl.Line) > maxContentWidth { 88 106 truncated := dl.Line[:maxContentWidth-3] + "..." 89 - switch dl.Kind { 90 - case diff.DiffOld: 91 - formatted = Red(truncated) 92 - case diff.DiffNew: 93 - formatted = Green(truncated) 94 - case diff.DiffShared: 95 - formatted = truncated 96 - } 107 + formatted = formatColoredLine(truncated, dl.Kind) 97 108 } 98 109 99 110 display := fmt.Sprintf("%s %s %s %s", leftNum, rightNum, prefix, formatted) ··· 116 127 117 128 if snap.Title != "" { 118 129 sb.WriteString(Blue(" title: ") + snap.Title + "\n") 119 - // sb.WriteString(fmt.Sprintf(" title: %s\n", Blue(snap.Title))) 120 130 } 121 131 if snap.Test != "" { 122 - // sb.WriteString(fmt.Sprintf(" test: %s\n", Blue(snap.Test))) 123 132 sb.WriteString(Blue(" test: ") + snap.Test + "\n") 124 133 } 125 134 if snap.FileName != "" { 126 - // sb.WriteString(fmt.Sprintf(" file: %s\n", Gray(snap.FileName))) 127 135 sb.WriteString(Blue(" file: ") + snap.FileName + "\n") 128 136 } 129 137 sb.WriteString("\n") 130 138 131 139 lines := strings.Split(snap.Content, "\n") 132 140 numLines := len(lines) 133 - lineNumWidth := len(strconv.Itoa(numLines)) 141 + lineNumWidth := calculateLineNumWidth(numLines) 134 142 135 143 topBar := strings.Repeat("─", lineNumWidth+3) + "┬" + 136 144 strings.Repeat("─", width-lineNumWidth-2) + "\n"
+1008
internal/pretty/boxes_test.go
··· 1 + package pretty_test 2 + 3 + import ( 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 17 + type 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 38 + func 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 108 + func 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) 139 + func 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 154 + func 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 174 + func 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: "testsimple.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 216 + func 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: "testaddition.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 258 + func 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: "testdeletion.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 300 + func TestDiffSnapshotBox_ComplexMixed(t *testing.T) { 301 + os.Unsetenv("NO_COLOR") 302 + os.Setenv("COLUMNS", "120") 303 + defer os.Unsetenv("COLUMNS") 304 + 305 + oldContent := `unchanged1 306 + delete1 307 + delete2 308 + unchanged2 309 + modify_old 310 + unchanged3` 311 + 312 + newContent := `unchanged1 313 + unchanged2 314 + modify_new 315 + add1 316 + unchanged3 317 + add2` 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: "testcomplexmixed.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 353 + func 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: "testemptyold.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 395 + func 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: "testemptynew.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 437 + func 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 + if !strings.Contains(stripped, "file: testnotitle.snap") { 472 + t.Error("Expected file name to be present") 473 + } 474 + } 475 + 476 + // TestDiffSnapshotBox_LargeLineNumbers tests proper padding for multi-digit line numbers 477 + func TestDiffSnapshotBox_LargeLineNumbers(t *testing.T) { 478 + os.Unsetenv("NO_COLOR") 479 + os.Setenv("COLUMNS", "120") 480 + defer os.Unsetenv("COLUMNS") 481 + 482 + // Create content with 100+ lines to test 3-digit line numbers 483 + oldLines := make([]string, 105) 484 + newLines := make([]string, 105) 485 + for i := 0; i < 105; i++ { 486 + oldLines[i] = fmt.Sprintf("line %d", i+1) 487 + newLines[i] = fmt.Sprintf("line %d", i+1) 488 + } 489 + // Modify lines 50, 75, and 100 490 + oldLines[49] = "old line 50" 491 + newLines[49] = "new line 50" 492 + oldLines[74] = "old line 75" 493 + newLines[74] = "new line 75" 494 + oldLines[99] = "old line 100" 495 + newLines[99] = "new line 100" 496 + 497 + oldContent := strings.Join(oldLines, "\n") 498 + newContent := strings.Join(newLines, "\n") 499 + 500 + oldSnap := &files.Snapshot{ 501 + Title: "Large Line Numbers", 502 + Test: "TestLargeLineNumbers", 503 + Content: oldContent, 504 + } 505 + 506 + newSnap := &files.Snapshot{ 507 + Title: "Large Line Numbers", 508 + Test: "TestLargeLineNumbers", 509 + Content: newContent, 510 + } 511 + 512 + diffLines := diff.Histogram(oldContent, newContent) 513 + result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 514 + 515 + stripped := stripANSI(result) 516 + 517 + // Check that 3-digit line numbers appear 518 + if !strings.Contains(stripped, "100") { 519 + t.Error("Expected 3-digit line number 100 to appear") 520 + } 521 + 522 + // Validate line number alignment by checking that numbers are right-aligned 523 + // Line 1 should have padding for 3 digits 524 + lines := strings.Split(stripped, "\n") 525 + foundSingleDigit := false 526 + foundTripleDigit := false 527 + 528 + for _, line := range lines { 529 + // Look for lines with content markers 530 + if strings.Contains(line, "│") || strings.Contains(line, "+") || strings.Contains(line, "-") { 531 + // Single digit should have padding (e.g., " 1" or " 2") 532 + if strings.Contains(line, " 1 ") || strings.Contains(line, " 2 ") { 533 + foundSingleDigit = true 534 + } 535 + // Triple digit should align (e.g., "100" or "105") 536 + if strings.Contains(line, "100 ") || strings.Contains(line, "105 ") { 537 + foundTripleDigit = true 538 + } 539 + } 540 + } 541 + 542 + if !foundSingleDigit { 543 + t.Error("Expected to find padded single-digit line numbers") 544 + } 545 + if !foundTripleDigit { 546 + t.Error("Expected to find triple-digit line numbers") 547 + } 548 + } 549 + 550 + // TestDiffSnapshotBox_UnicodeContent tests diff with unicode characters 551 + func TestDiffSnapshotBox_UnicodeContent(t *testing.T) { 552 + os.Unsetenv("NO_COLOR") 553 + os.Setenv("COLUMNS", "100") 554 + defer os.Unsetenv("COLUMNS") 555 + 556 + oldContent := "Hello 世界\nこんにちは\n🎉 emoji" 557 + newContent := "Hello 世界\nさようなら\n🎊 party" 558 + 559 + oldSnap := &files.Snapshot{ 560 + Title: "Unicode Test", 561 + Test: "TestUnicode", 562 + Content: oldContent, 563 + } 564 + 565 + newSnap := &files.Snapshot{ 566 + Title: "Unicode Test", 567 + Test: "TestUnicode", 568 + Content: newContent, 569 + } 570 + 571 + diffLines := diff.Histogram(oldContent, newContent) 572 + result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 573 + 574 + validation := BoxValidation{ 575 + Title: "Unicode Test", 576 + TestName: "TestUnicode", 577 + FileName: "testunicode.snap", 578 + HasTitle: true, 579 + HasTestName: true, 580 + HasFileName: true, 581 + ExpectedAdds: []string{"さようなら", "🎊 party"}, 582 + ExpectedDeletes: []string{"こんにちは", "🎉 emoji"}, 583 + ExpectedContext: []string{"Hello 世界"}, 584 + HasTopBar: true, 585 + HasBottomBar: true, 586 + MinLines: 5, // 1 shared + 2 deletes + 2 adds 587 + } 588 + 589 + ValidateDiffBox(t, result, validation) 590 + } 591 + 592 + // TestNewSnapshotBox_Basic tests the new snapshot box rendering 593 + func TestNewSnapshotBox_Basic(t *testing.T) { 594 + os.Unsetenv("NO_COLOR") 595 + os.Setenv("COLUMNS", "100") 596 + defer os.Unsetenv("COLUMNS") 597 + 598 + content := "line1\nline2\nline3" 599 + 600 + snap := &files.Snapshot{ 601 + Title: "New Snapshot", 602 + Test: "TestNewSnapshot", 603 + FileName: "test_new.snap", 604 + Content: content, 605 + } 606 + 607 + result := pretty.NewSnapshotBox(snap) 608 + 609 + stripped := stripANSI(result) 610 + 611 + // Check header 612 + if !strings.Contains(stripped, "New Snapshot") { 613 + t.Error("Expected 'New Snapshot' header") 614 + } 615 + 616 + // Check metadata 617 + if !strings.Contains(stripped, "title: New Snapshot") { 618 + t.Error("Expected title in output") 619 + } 620 + if !strings.Contains(stripped, "test: TestNewSnapshot") { 621 + t.Error("Expected test name in output") 622 + } 623 + if !strings.Contains(stripped, "file: test_new.snap") { 624 + t.Error("Expected file name in output") 625 + } 626 + 627 + // Check content lines (all should be green additions) 628 + if !containsDiffLine(result, "+", "line1") { 629 + t.Error("Expected line1 as addition") 630 + } 631 + if !containsDiffLine(result, "+", "line2") { 632 + t.Error("Expected line2 as addition") 633 + } 634 + if !containsDiffLine(result, "+", "line3") { 635 + t.Error("Expected line3 as addition") 636 + } 637 + 638 + // Check box structure 639 + if !strings.Contains(stripped, "┬") { 640 + t.Error("Expected top bar with ┬") 641 + } 642 + if !strings.Contains(stripped, "┴") { 643 + t.Error("Expected bottom bar with ┴") 644 + } 645 + } 646 + 647 + // TestNewSnapshotBox_EmptyContent tests new snapshot with empty content 648 + func TestNewSnapshotBox_EmptyContent(t *testing.T) { 649 + os.Unsetenv("NO_COLOR") 650 + os.Setenv("COLUMNS", "100") 651 + defer os.Unsetenv("COLUMNS") 652 + 653 + snap := &files.Snapshot{ 654 + Title: "Empty Snapshot", 655 + Test: "TestEmpty", 656 + FileName: "test_empty.snap", 657 + Content: "", 658 + } 659 + 660 + result := pretty.NewSnapshotBox(snap) 661 + 662 + // Should still render box with metadata, just no content lines 663 + stripped := stripANSI(result) 664 + 665 + if !strings.Contains(stripped, "title: Empty Snapshot") { 666 + t.Error("Expected title in output") 667 + } 668 + 669 + // Should have box structure even with empty content 670 + if !strings.Contains(stripped, "┬") { 671 + t.Error("Expected top bar with ┬") 672 + } 673 + if !strings.Contains(stripped, "┴") { 674 + t.Error("Expected bottom bar with ┴") 675 + } 676 + } 677 + 678 + // Snapshot testing for visual regression 679 + 680 + func TestDiffSnapshotBox_VisualRegression_SimpleModification(t *testing.T) { 681 + os.Unsetenv("NO_COLOR") 682 + os.Setenv("COLUMNS", "100") 683 + defer os.Unsetenv("COLUMNS") 684 + 685 + oldContent := "line1\nline2\nline3" 686 + newContent := "line1\nmodified\nline3" 687 + 688 + oldSnap := &files.Snapshot{ 689 + Title: "Visual Test", 690 + Test: "TestVisualSimple", 691 + Content: oldContent, 692 + } 693 + 694 + newSnap := &files.Snapshot{ 695 + Title: "Visual Test", 696 + Test: "TestVisualSimple", 697 + Content: newContent, 698 + } 699 + 700 + diffLines := diff.Histogram(oldContent, newContent) 701 + result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 702 + 703 + shutter.SnapString(t, "diff_box_simple_modification", result) 704 + } 705 + 706 + func TestDiffSnapshotBox_VisualRegression_ComplexMixed(t *testing.T) { 707 + os.Unsetenv("NO_COLOR") 708 + os.Setenv("COLUMNS", "120") 709 + defer os.Unsetenv("COLUMNS") 710 + 711 + oldContent := `unchanged1 712 + delete1 713 + delete2 714 + unchanged2 715 + modify_old 716 + unchanged3` 717 + 718 + newContent := `unchanged1 719 + unchanged2 720 + modify_new 721 + add1 722 + unchanged3 723 + add2` 724 + 725 + oldSnap := &files.Snapshot{ 726 + Title: "Visual Complex", 727 + Test: "TestVisualComplex", 728 + Content: oldContent, 729 + } 730 + 731 + newSnap := &files.Snapshot{ 732 + Title: "Visual Complex", 733 + Test: "TestVisualComplex", 734 + Content: newContent, 735 + } 736 + 737 + diffLines := diff.Histogram(oldContent, newContent) 738 + result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 739 + 740 + shutter.SnapString(t, "diff_box_complex_mixed", result) 741 + } 742 + 743 + func TestDiffSnapshotBox_VisualRegression_LargeLineNumbers(t *testing.T) { 744 + os.Unsetenv("NO_COLOR") 745 + os.Setenv("COLUMNS", "120") 746 + defer os.Unsetenv("COLUMNS") 747 + 748 + // Create content with 100+ lines 749 + oldLines := make([]string, 105) 750 + newLines := make([]string, 105) 751 + for i := 0; i < 105; i++ { 752 + oldLines[i] = fmt.Sprintf("line %d", i+1) 753 + newLines[i] = fmt.Sprintf("line %d", i+1) 754 + } 755 + oldLines[49] = "old line 50" 756 + newLines[49] = "new line 50" 757 + oldLines[99] = "old line 100" 758 + newLines[99] = "new line 100" 759 + 760 + oldContent := strings.Join(oldLines, "\n") 761 + newContent := strings.Join(newLines, "\n") 762 + 763 + oldSnap := &files.Snapshot{ 764 + Title: "Large Line Numbers", 765 + Test: "TestVisualLarge", 766 + Content: oldContent, 767 + } 768 + 769 + newSnap := &files.Snapshot{ 770 + Title: "Large Line Numbers", 771 + Test: "TestVisualLarge", 772 + Content: newContent, 773 + } 774 + 775 + diffLines := diff.Histogram(oldContent, newContent) 776 + result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 777 + 778 + shutter.SnapString(t, "diff_box_large_line_numbers", result) 779 + } 780 + 781 + func TestNewSnapshotBox_VisualRegression(t *testing.T) { 782 + os.Unsetenv("NO_COLOR") 783 + os.Setenv("COLUMNS", "100") 784 + defer os.Unsetenv("COLUMNS") 785 + 786 + content := "line1\nline2\nline3\nline4\nline5" 787 + 788 + snap := &files.Snapshot{ 789 + Title: "New Snapshot Visual", 790 + Test: "TestNewVisual", 791 + FileName: "test_new_visual.snap", 792 + Content: content, 793 + } 794 + 795 + result := pretty.NewSnapshotBox(snap) 796 + 797 + shutter.SnapString(t, "new_snapshot_box", result) 798 + } 799 + 800 + // Randomized testing 801 + 802 + func TestDiffSnapshotBox_Random_Additions(t *testing.T) { 803 + os.Unsetenv("NO_COLOR") 804 + os.Setenv("COLUMNS", "120") 805 + defer os.Unsetenv("COLUMNS") 806 + 807 + rng := rand.New(rand.NewSource(12345)) // Fixed seed for reproducibility 808 + 809 + for i := 0; i < 10; i++ { 810 + t.Run(fmt.Sprintf("random_addition_%d", i), func(t *testing.T) { 811 + // Generate random number of old lines (5-20) 812 + numOldLines := rng.Intn(16) + 5 813 + oldLines := make([]string, numOldLines) 814 + for j := 0; j < numOldLines; j++ { 815 + oldLines[j] = fmt.Sprintf("old_line_%d", j+1) 816 + } 817 + 818 + // Add random number of new lines (1-10) 819 + numNewLines := rng.Intn(10) + 1 820 + newLines := make([]string, numOldLines+numNewLines) 821 + copy(newLines, oldLines) 822 + for j := 0; j < numNewLines; j++ { 823 + newLines[numOldLines+j] = fmt.Sprintf("new_line_%d", j+1) 824 + } 825 + 826 + oldContent := strings.Join(oldLines, "\n") 827 + newContent := strings.Join(newLines, "\n") 828 + 829 + oldSnap := &files.Snapshot{ 830 + Title: fmt.Sprintf("Random Addition %d", i), 831 + Test: fmt.Sprintf("TestRandomAdd_%d", i), 832 + Content: oldContent, 833 + } 834 + 835 + newSnap := &files.Snapshot{ 836 + Title: fmt.Sprintf("Random Addition %d", i), 837 + Test: fmt.Sprintf("TestRandomAdd_%d", i), 838 + Content: newContent, 839 + } 840 + 841 + diffLines := diff.Histogram(oldContent, newContent) 842 + result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 843 + 844 + // Validate structure 845 + stripped := stripANSI(result) 846 + 847 + // Should have box structure 848 + if !strings.Contains(stripped, "┬") { 849 + t.Error("Missing top bar") 850 + } 851 + if !strings.Contains(stripped, "┴") { 852 + t.Error("Missing bottom bar") 853 + } 854 + 855 + // Should contain title and test name 856 + if !strings.Contains(stripped, fmt.Sprintf("Random Addition %d", i)) { 857 + t.Error("Missing title") 858 + } 859 + 860 + // Count additions 861 + addCount := strings.Count(result, "+") 862 + if addCount < numNewLines { 863 + t.Errorf("Expected at least %d additions, got %d", numNewLines, addCount) 864 + } 865 + }) 866 + } 867 + } 868 + 869 + func TestDiffSnapshotBox_Random_Deletions(t *testing.T) { 870 + os.Unsetenv("NO_COLOR") 871 + os.Setenv("COLUMNS", "120") 872 + defer os.Unsetenv("COLUMNS") 873 + 874 + rng := rand.New(rand.NewSource(54321)) 875 + 876 + for i := 0; i < 10; i++ { 877 + t.Run(fmt.Sprintf("random_deletion_%d", i), func(t *testing.T) { 878 + // Generate random number of old lines (10-30) 879 + numOldLines := rng.Intn(21) + 10 880 + oldLines := make([]string, numOldLines) 881 + for j := 0; j < numOldLines; j++ { 882 + oldLines[j] = fmt.Sprintf("line_%d", j+1) 883 + } 884 + 885 + // Delete random number of lines (1-5) 886 + numToDelete := rng.Intn(5) + 1 887 + if numToDelete > numOldLines { 888 + numToDelete = numOldLines / 2 889 + } 890 + newLines := make([]string, numOldLines-numToDelete) 891 + copy(newLines, oldLines[:len(newLines)]) 892 + 893 + oldContent := strings.Join(oldLines, "\n") 894 + newContent := strings.Join(newLines, "\n") 895 + 896 + oldSnap := &files.Snapshot{ 897 + Title: fmt.Sprintf("Random Deletion %d", i), 898 + Test: fmt.Sprintf("TestRandomDel_%d", i), 899 + Content: oldContent, 900 + } 901 + 902 + newSnap := &files.Snapshot{ 903 + Title: fmt.Sprintf("Random Deletion %d", i), 904 + Test: fmt.Sprintf("TestRandomDel_%d", i), 905 + Content: newContent, 906 + } 907 + 908 + diffLines := diff.Histogram(oldContent, newContent) 909 + result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 910 + 911 + // Validate structure 912 + stripped := stripANSI(result) 913 + 914 + if !strings.Contains(stripped, "┬") { 915 + t.Error("Missing top bar") 916 + } 917 + if !strings.Contains(stripped, "┴") { 918 + t.Error("Missing bottom bar") 919 + } 920 + 921 + // Count deletions (at least numToDelete should appear) 922 + delCount := strings.Count(result, "-") 923 + if delCount < numToDelete { 924 + t.Errorf("Expected at least %d deletions, got %d", numToDelete, delCount) 925 + } 926 + }) 927 + } 928 + } 929 + 930 + func TestDiffSnapshotBox_Random_Mixed(t *testing.T) { 931 + os.Unsetenv("NO_COLOR") 932 + os.Setenv("COLUMNS", "140") 933 + defer os.Unsetenv("COLUMNS") 934 + 935 + rng := rand.New(rand.NewSource(99999)) 936 + 937 + for i := 0; i < 10; i++ { 938 + t.Run(fmt.Sprintf("random_mixed_%d", i), func(t *testing.T) { 939 + // Generate random old content (10-30 lines) 940 + numOldLines := rng.Intn(21) + 10 941 + oldLines := make([]string, numOldLines) 942 + for j := 0; j < numOldLines; j++ { 943 + oldLines[j] = fmt.Sprintf("old_line_%d_%s", j+1, randomWord(rng)) 944 + } 945 + 946 + // Randomly modify, add, delete 947 + newLines := make([]string, 0, numOldLines*2) 948 + for j := 0; j < numOldLines; j++ { 949 + action := rng.Intn(100) 950 + if action < 70 { // 70% keep unchanged 951 + newLines = append(newLines, oldLines[j]) 952 + } else if action < 85 { // 15% modify 953 + newLines = append(newLines, fmt.Sprintf("modified_%d_%s", j+1, randomWord(rng))) 954 + } else if action < 95 { // 10% add 955 + newLines = append(newLines, oldLines[j]) 956 + newLines = append(newLines, fmt.Sprintf("added_%d_%s", j+1, randomWord(rng))) 957 + } 958 + // 5% delete (skip adding line) 959 + } 960 + 961 + oldContent := strings.Join(oldLines, "\n") 962 + newContent := strings.Join(newLines, "\n") 963 + 964 + oldSnap := &files.Snapshot{ 965 + Title: fmt.Sprintf("Random Mixed %d", i), 966 + Test: fmt.Sprintf("TestRandomMixed_%d", i), 967 + Content: oldContent, 968 + } 969 + 970 + newSnap := &files.Snapshot{ 971 + Title: fmt.Sprintf("Random Mixed %d", i), 972 + Test: fmt.Sprintf("TestRandomMixed_%d", i), 973 + Content: newContent, 974 + } 975 + 976 + diffLines := diff.Histogram(oldContent, newContent) 977 + result := pretty.DiffSnapshotBox(oldSnap, newSnap, diffLines) 978 + 979 + // Validate basic structure 980 + stripped := stripANSI(result) 981 + 982 + if !strings.Contains(stripped, "┬") { 983 + t.Error("Missing top bar") 984 + } 985 + if !strings.Contains(stripped, "┴") { 986 + t.Error("Missing bottom bar") 987 + } 988 + if !strings.Contains(stripped, fmt.Sprintf("Random Mixed %d", i)) { 989 + t.Error("Missing title") 990 + } 991 + 992 + // Should have some diff markers 993 + hasPlus := strings.Contains(result, "+") 994 + hasMinus := strings.Contains(result, "-") 995 + hasPipe := strings.Contains(result, "│") 996 + 997 + if !hasPlus && !hasMinus && !hasPipe { 998 + t.Error("Expected at least one type of diff marker") 999 + } 1000 + }) 1001 + } 1002 + } 1003 + 1004 + // Helper function for random word generation 1005 + func randomWord(rng *rand.Rand) string { 1006 + words := []string{"apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew"} 1007 + return words[rng.Intn(len(words))] 1008 + }
+16 -26
internal/pretty/pretty.go
··· 23 23 return 80 24 24 } 25 25 26 - func Red(s string) string { 26 + func hasColor() bool { 27 + return os.Getenv("NO_COLOR") == "" 28 + } 29 + 30 + // colorize wraps text with the given color code 31 + func colorize(s, code string) string { 27 32 if !hasColor() { 28 33 return s 29 34 } 30 - return colorRed + s + colorReset 35 + return code + s + colorReset 36 + } 37 + 38 + func Red(s string) string { 39 + return colorize(s, colorRed) 31 40 } 32 41 33 42 func Green(s string) string { 34 - if !hasColor() { 35 - return s 36 - } 37 - return colorGreen + s + colorReset 43 + return colorize(s, colorGreen) 38 44 } 39 45 40 46 func Yellow(s string) string { 41 - if !hasColor() { 42 - return s 43 - } 44 - return colorYellow + s + colorReset 47 + return colorize(s, colorYellow) 45 48 } 46 49 47 50 func Blue(s string) string { 48 - if !hasColor() { 49 - return s 50 - } 51 - return colorBlue + s + colorReset 51 + return colorize(s, colorBlue) 52 52 } 53 53 54 54 func Gray(s string) string { 55 - if !hasColor() { 56 - return s 57 - } 58 - return colorGray + s + colorReset 55 + return colorize(s, colorGray) 59 56 } 60 57 61 58 func Bold(s string) string { 62 - if !hasColor() { 63 - return s 64 - } 65 - return colorBold + s + colorReset 66 - } 67 - 68 - func hasColor() bool { 69 - return os.Getenv("NO_COLOR") == "" 59 + return colorize(s, colorBold) 70 60 } 71 61 72 62 func Header(text string) string {
+30 -20
internal/review/review.go
··· 27 27 return diff.Histogram(old.Content, new.Content) 28 28 } 29 29 30 + // applyToSnapshots applies an operation to all snapshots and returns the count of successful operations 31 + func applyToSnapshots(snapshots []string, operation func(string) error) (int, error) { 32 + successCount := 0 33 + for _, snapTitle := range snapshots { 34 + if err := operation(snapTitle); err != nil { 35 + return successCount, err 36 + } 37 + successCount++ 38 + } 39 + return successCount, nil 40 + } 41 + 30 42 func Review() error { 31 43 snapshots, err := files.ListNewSnapshots() 32 44 if err != nil { ··· 87 99 case Skip: 88 100 fmt.Println(pretty.Warning("⊘ Snapshot skipped")) 89 101 case AcceptAllChoice: 90 - for j := i; j < len(snapshots); j++ { 91 - if err := files.AcceptSnapshot(snapshots[j]); err != nil { 92 - fmt.Println(pretty.Error("✗ Failed to accept snapshot: " + err.Error())) 93 - } 102 + remaining := snapshots[i:] 103 + if _, err := applyToSnapshots(remaining, files.AcceptSnapshot); err != nil { 104 + fmt.Println(pretty.Error("✗ Failed to accept snapshot: " + err.Error())) 105 + return err 94 106 } 95 - fmt.Printf(pretty.Success("✓ Accepted %d snapshot(s)\n"), len(snapshots)-i) 107 + fmt.Printf(pretty.Success("✓ Accepted %d snapshot(s)\n"), len(remaining)) 96 108 return nil 97 109 case RejectAllChoice: 98 - for j := i; j < len(snapshots); j++ { 99 - if err := files.RejectSnapshot(snapshots[j]); err != nil { 100 - fmt.Println(pretty.Error("✗ Failed to reject snapshot: " + err.Error())) 101 - } 110 + remaining := snapshots[i:] 111 + if _, err := applyToSnapshots(remaining, files.RejectSnapshot); err != nil { 112 + fmt.Println(pretty.Error("✗ Failed to reject snapshot: " + err.Error())) 113 + return err 102 114 } 103 - fmt.Printf(pretty.Warning("⊘ Rejected %d snapshot(s)\n"), len(snapshots)-i) 115 + fmt.Printf(pretty.Warning("⊘ Rejected %d snapshot(s)\n"), len(remaining)) 104 116 return nil 105 117 case SkipAllChoice: 106 118 fmt.Printf(pretty.Warning("⊘ Skipped %d snapshot(s)\n"), len(snapshots)-i) ··· 154 166 return err 155 167 } 156 168 157 - for _, testName := range snapshots { 158 - if err := files.AcceptSnapshot(testName); err != nil { 159 - return err 160 - } 169 + count, err := applyToSnapshots(snapshots, files.AcceptSnapshot) 170 + if err != nil { 171 + return err 161 172 } 162 173 163 - fmt.Printf(pretty.Success("✓ Accepted %d snapshot(s)\n"), len(snapshots)) 174 + fmt.Printf(pretty.Success("✓ Accepted %d snapshot(s)\n"), count) 164 175 return nil 165 176 } 166 177 ··· 170 181 return err 171 182 } 172 183 173 - for _, testName := range snapshots { 174 - if err := files.RejectSnapshot(testName); err != nil { 175 - return err 176 - } 184 + count, err := applyToSnapshots(snapshots, files.RejectSnapshot) 185 + if err != nil { 186 + return err 177 187 } 178 188 179 - fmt.Printf(pretty.Warning("⊘ Rejected %d snapshot(s)\n"), len(snapshots)) 189 + fmt.Printf(pretty.Warning("⊘ Rejected %d snapshot(s)\n"), count) 180 190 return nil 181 191 }