changelog generator & diff tool
stormlightlabs.github.io/git-storm/
changelog
changeset
markdown
golang
git
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}