changelog generator & diff tool stormlightlabs.github.io/git-storm/
changelog changeset markdown golang git
at main 10 kB view raw
1package diff 2 3import ( 4 "strings" 5 "testing" 6 7 "github.com/charmbracelet/lipgloss" 8) 9 10func TestSideBySideFormatter_Format(t *testing.T) { 11 tests := []struct { 12 name string 13 edits []Edit 14 width int 15 expect func(string) bool 16 }{ 17 { 18 name: "empty edits", 19 edits: []Edit{}, 20 width: 80, 21 expect: func(output string) bool { 22 return strings.Contains(output, "No changes") 23 }, 24 }, 25 { 26 name: "equal lines", 27 edits: []Edit{ 28 {Kind: Equal, AIndex: 0, BIndex: 0, Content: "hello world"}, 29 }, 30 width: 80, 31 expect: func(output string) bool { 32 return strings.Contains(output, "hello world") 33 }, 34 }, 35 { 36 name: "insert operation", 37 edits: []Edit{ 38 {Kind: Insert, AIndex: -1, BIndex: 0, Content: "new line"}, 39 }, 40 width: 80, 41 expect: func(output string) bool { 42 return strings.Contains(output, "new line") && strings.Contains(output, SymbolAdd) 43 }, 44 }, 45 { 46 name: "delete operation", 47 edits: []Edit{ 48 {Kind: Delete, AIndex: 0, BIndex: -1, Content: "old line"}, 49 }, 50 width: 80, 51 expect: func(output string) bool { 52 return strings.Contains(output, "old line") && strings.Contains(output, SymbolDeleteLine) 53 }, 54 }, 55 { 56 name: "mixed operations", 57 edits: []Edit{ 58 {Kind: Equal, AIndex: 0, BIndex: 0, Content: "unchanged"}, 59 {Kind: Delete, AIndex: 1, BIndex: -1, Content: "removed"}, 60 {Kind: Insert, AIndex: -1, BIndex: 1, Content: "added"}, 61 {Kind: Equal, AIndex: 2, BIndex: 2, Content: "also unchanged"}, 62 }, 63 width: 100, 64 expect: func(output string) bool { 65 return strings.Contains(output, "unchanged") && 66 strings.Contains(output, "removed") && 67 strings.Contains(output, "added") 68 }, 69 }, 70 } 71 72 for _, tt := range tests { 73 t.Run(tt.name, func(t *testing.T) { 74 formatter := &SideBySideFormatter{ 75 TerminalWidth: tt.width, 76 ShowLineNumbers: true, 77 } 78 79 output := formatter.Format(tt.edits) 80 81 if !tt.expect(output) { 82 t.Errorf("Format() output did not meet expectations.\nGot:\n%s", output) 83 } 84 }) 85 } 86} 87 88func TestSideBySideFormatter_CalculatePaneWidth(t *testing.T) { 89 tests := []struct { 90 name string 91 terminalWidth int 92 showLineNumbers bool 93 minExpected int 94 }{ 95 { 96 name: "standard width with line numbers", 97 terminalWidth: 120, 98 showLineNumbers: true, 99 minExpected: 40, 100 }, 101 { 102 name: "narrow terminal", 103 terminalWidth: 60, 104 showLineNumbers: true, 105 minExpected: 20, 106 }, 107 { 108 name: "without line numbers", 109 terminalWidth: 100, 110 showLineNumbers: false, 111 minExpected: 40, 112 }, 113 } 114 115 for _, tt := range tests { 116 t.Run(tt.name, func(t *testing.T) { 117 formatter := &SideBySideFormatter{ 118 TerminalWidth: tt.terminalWidth, 119 ShowLineNumbers: tt.showLineNumbers, 120 } 121 122 paneWidth := formatter.calculatePaneWidth() 123 124 if paneWidth < tt.minExpected { 125 t.Errorf("calculatePaneWidth() = %d, expected at least %d", paneWidth, tt.minExpected) 126 } 127 128 usedWidth := gutterWidth 129 if tt.showLineNumbers { 130 usedWidth += 2 * lineNumWidth 131 } 132 totalWidth := usedWidth + (2 * paneWidth) 133 if totalWidth > tt.terminalWidth { 134 t.Errorf("Total width %d exceeds terminal width %d (paneWidth=%d)", totalWidth, tt.terminalWidth, paneWidth) 135 } 136 }) 137 } 138} 139 140func TestPadToWidth(t *testing.T) { 141 formatter := &SideBySideFormatter{} 142 143 tests := []struct { 144 name string 145 input string 146 targetWidth int 147 }{ 148 { 149 name: "short string gets padded", 150 input: "hello", 151 targetWidth: 10, 152 }, 153 { 154 name: "exact width unchanged", 155 input: "hello world", 156 targetWidth: 11, 157 }, 158 { 159 name: "long string gets truncated", 160 input: "this is a very long string that exceeds the target width", 161 targetWidth: 20, 162 }, 163 } 164 165 for _, tt := range tests { 166 t.Run(tt.name, func(t *testing.T) { 167 result := formatter.padToWidth(tt.input, tt.targetWidth) 168 169 resultWidth := lipgloss.Width(result) 170 if resultWidth != tt.targetWidth { 171 t.Errorf("padToWidth() width = %d, expected exactly %d", resultWidth, tt.targetWidth) 172 } 173 }) 174 } 175} 176 177func TestSideBySideFormatter_TruncateContent(t *testing.T) { 178 formatter := &SideBySideFormatter{} 179 180 tests := []struct { 181 name string 182 content string 183 maxWidth int 184 expected string 185 }{ 186 { 187 name: "short content", 188 content: "hello", 189 maxWidth: 10, 190 expected: "hello", 191 }, 192 { 193 name: "exact fit", 194 content: "hello world", 195 maxWidth: 11, 196 expected: "hello world", 197 }, 198 { 199 name: "needs truncation", 200 content: "hello world this is a long line", 201 maxWidth: 10, 202 expected: "hello w...", 203 }, 204 { 205 name: "very small width", 206 content: "hello", 207 maxWidth: 3, 208 expected: "hel", 209 }, 210 { 211 name: "trailing whitespace removed", 212 content: "hello ", 213 maxWidth: 10, 214 expected: "hello", 215 }, 216 { 217 name: "very long line", 218 content: "github.com/charmbracelet/x/ansi v0.10.3 h1:3WoV9XN8uMEnFRZZ+vBPRy59TaI", 219 maxWidth: 40, 220 expected: "", 221 }, 222 } 223 224 for _, tt := range tests { 225 t.Run(tt.name, func(t *testing.T) { 226 result := formatter.truncateContent(tt.content, tt.maxWidth) 227 228 displayWidth := lipgloss.Width(result) 229 if displayWidth > tt.maxWidth { 230 t.Errorf("truncateContent() display width = %d, exceeds max %d", displayWidth, tt.maxWidth) 231 } 232 233 if tt.expected != "" && result != tt.expected { 234 t.Errorf("truncateContent() = %q, expected %q", result, tt.expected) 235 } 236 237 if lipgloss.Width(tt.content) > tt.maxWidth && tt.maxWidth > 3 { 238 if !strings.HasSuffix(result, "...") { 239 t.Errorf("truncateContent() should end with '...' for long content") 240 } 241 } 242 }) 243 } 244} 245 246func TestSideBySideFormatter_RenderEdit(t *testing.T) { 247 formatter := &SideBySideFormatter{ 248 TerminalWidth: 100, 249 ShowLineNumbers: true, 250 } 251 paneWidth := 40 252 253 tests := []struct { 254 name string 255 edit Edit 256 expect func(left, right string) bool 257 }{ 258 { 259 name: "equal edit shows on both sides", 260 edit: Edit{Kind: Equal, AIndex: 0, BIndex: 0, Content: "same"}, 261 expect: func(left, right string) bool { 262 return strings.Contains(left, "same") && strings.Contains(right, "same") 263 }, 264 }, 265 { 266 name: "delete shows only on left", 267 edit: Edit{Kind: Delete, AIndex: 0, BIndex: -1, Content: "removed"}, 268 expect: func(left, right string) bool { 269 return strings.Contains(left, "removed") && !strings.Contains(right, "removed") 270 }, 271 }, 272 { 273 name: "insert shows only on right", 274 edit: Edit{Kind: Insert, AIndex: -1, BIndex: 0, Content: "added"}, 275 expect: func(left, right string) bool { 276 return !strings.Contains(left, "added") && strings.Contains(right, "added") 277 }, 278 }, 279 } 280 281 for _, tt := range tests { 282 t.Run(tt.name, func(t *testing.T) { 283 left, right := formatter.renderEdit(tt.edit, paneWidth) 284 285 if !tt.expect(left, right) { 286 t.Errorf("renderEdit() failed expectations.\nLeft: %q\nRight: %q", left, right) 287 } 288 }) 289 } 290} 291 292func TestUnifiedFormatter_Format(t *testing.T) { 293 tests := []struct { 294 name string 295 edits []Edit 296 width int 297 expect func(string) bool 298 }{ 299 { 300 name: "empty edits", 301 edits: []Edit{}, 302 width: 80, 303 expect: func(output string) bool { 304 return strings.Contains(output, "No changes") 305 }, 306 }, 307 { 308 name: "equal lines", 309 edits: []Edit{ 310 {Kind: Equal, AIndex: 0, BIndex: 0, Content: "hello world"}, 311 }, 312 width: 80, 313 expect: func(output string) bool { 314 return strings.Contains(output, " hello world") 315 }, 316 }, 317 { 318 name: "insert operation", 319 edits: []Edit{ 320 {Kind: Insert, AIndex: -1, BIndex: 0, Content: "new line"}, 321 }, 322 width: 80, 323 expect: func(output string) bool { 324 return strings.Contains(output, "+new line") 325 }, 326 }, 327 { 328 name: "delete operation", 329 edits: []Edit{ 330 {Kind: Delete, AIndex: 0, BIndex: -1, Content: "old line"}, 331 }, 332 width: 80, 333 expect: func(output string) bool { 334 return strings.Contains(output, "-old line") 335 }, 336 }, 337 { 338 name: "replace operation", 339 edits: []Edit{ 340 {Kind: Replace, AIndex: 0, BIndex: 0, Content: "old content", NewContent: "new content"}, 341 }, 342 width: 100, 343 expect: func(output string) bool { 344 return strings.Contains(output, "-old content") && 345 strings.Contains(output, "+new content") 346 }, 347 }, 348 { 349 name: "mixed operations", 350 edits: []Edit{ 351 {Kind: Equal, AIndex: 0, BIndex: 0, Content: "unchanged"}, 352 {Kind: Delete, AIndex: 1, BIndex: -1, Content: "removed"}, 353 {Kind: Insert, AIndex: -1, BIndex: 1, Content: "added"}, 354 {Kind: Equal, AIndex: 2, BIndex: 2, Content: "also unchanged"}, 355 }, 356 width: 100, 357 expect: func(output string) bool { 358 return strings.Contains(output, " unchanged") && 359 strings.Contains(output, "-removed") && 360 strings.Contains(output, "+added") && 361 strings.Contains(output, " also unchanged") 362 }, 363 }, 364 } 365 366 for _, tt := range tests { 367 t.Run(tt.name, func(t *testing.T) { 368 formatter := &UnifiedFormatter{ 369 TerminalWidth: tt.width, 370 ShowLineNumbers: true, 371 } 372 373 output := formatter.Format(tt.edits) 374 375 if !tt.expect(output) { 376 t.Errorf("Format() output did not meet expectations.\nGot:\n%s", output) 377 } 378 }) 379 } 380} 381 382func TestUnifiedFormatter_CalculateContentWidth(t *testing.T) { 383 tests := []struct { 384 name string 385 terminalWidth int 386 showLineNumbers bool 387 minExpected int 388 }{ 389 { 390 name: "standard width with line numbers", 391 terminalWidth: 120, 392 showLineNumbers: true, 393 minExpected: 40, 394 }, 395 { 396 name: "narrow terminal", 397 terminalWidth: 60, 398 showLineNumbers: true, 399 minExpected: 40, 400 }, 401 { 402 name: "without line numbers", 403 terminalWidth: 100, 404 showLineNumbers: false, 405 minExpected: 40, 406 }, 407 } 408 409 for _, tt := range tests { 410 t.Run(tt.name, func(t *testing.T) { 411 formatter := &UnifiedFormatter{ 412 TerminalWidth: tt.terminalWidth, 413 ShowLineNumbers: tt.showLineNumbers, 414 } 415 416 contentWidth := formatter.calculateContentWidth() 417 418 if contentWidth < tt.minExpected { 419 t.Errorf("calculateContentWidth() = %d, expected at least %d", contentWidth, tt.minExpected) 420 } 421 }) 422 } 423}