|
6 | 6 |
|
7 | 7 | "github.com/Astro-Han/diffpane/internal" |
8 | 8 | tea "github.com/charmbracelet/bubbletea" |
| 9 | + "github.com/charmbracelet/lipgloss" |
9 | 10 | ) |
10 | 11 |
|
11 | 12 | func file(path string, adds int) internal.FileDiff { |
@@ -91,6 +92,31 @@ func TestModelPausedFollowTracksNewFiles(t *testing.T) { |
91 | 92 | } |
92 | 93 | } |
93 | 94 |
|
| 95 | +// TestModelPausedFollowIgnoresVanishedChangedPaths verifies transient changes |
| 96 | +// that disappear before the diff refresh lands do not inflate +N new. |
| 97 | +func TestModelPausedFollowIgnoresVanishedChangedPaths(t *testing.T) { |
| 98 | + model := NewModel("repo", "/tmp/repo", "sha", []internal.FileDiff{ |
| 99 | + file("a.txt", 1), |
| 100 | + }) |
| 101 | + model.FollowOn = false |
| 102 | + model.CurrentIdx = 0 |
| 103 | + |
| 104 | + updated, _ := model.Update(FilesUpdatedMsg{ |
| 105 | + Files: []internal.FileDiff{ |
| 106 | + file("a.txt", 1), |
| 107 | + }, |
| 108 | + ChangedPaths: []string{"temp.txt"}, |
| 109 | + }) |
| 110 | + |
| 111 | + got := updated.(Model) |
| 112 | + if got.NewCount != 0 { |
| 113 | + t.Fatalf("NewCount = %d, want 0", got.NewCount) |
| 114 | + } |
| 115 | + if len(got.NewFiles) != 0 { |
| 116 | + t.Fatalf("NewFiles = %#v, want empty", got.NewFiles) |
| 117 | + } |
| 118 | +} |
| 119 | + |
94 | 120 | // TestModelOverlayQueuesUpdates verifies overlay mode freezes the view and applies pending updates on close. |
95 | 121 | func TestModelOverlayQueuesUpdates(t *testing.T) { |
96 | 122 | model := NewModel("repo", "/tmp/repo", "sha", []internal.FileDiff{ |
@@ -281,6 +307,40 @@ func TestModelFollowClampsWhenCurrentFileDisappears(t *testing.T) { |
281 | 307 | } |
282 | 308 | } |
283 | 309 |
|
| 310 | +// TestModelOverlayAppliesFreshUpdateAfterBaselineReset verifies a newer |
| 311 | +// baseline refresh still lands after overlay close, even if an old update was |
| 312 | +// queued earlier in the same overlay session. |
| 313 | +func TestModelOverlayAppliesFreshUpdateAfterBaselineReset(t *testing.T) { |
| 314 | + model := NewModel("repo", "/tmp/repo", "old-sha", []internal.FileDiff{ |
| 315 | + file("old.txt", 1), |
| 316 | + }) |
| 317 | + |
| 318 | + opened, _ := model.Update(tea.KeyMsg{Type: tea.KeyTab}) |
| 319 | + withOverlay := opened.(Model) |
| 320 | + |
| 321 | + queuedOld, _ := withOverlay.Update(FilesUpdatedMsg{ |
| 322 | + BaselineSHA: "old-sha", |
| 323 | + Files: []internal.FileDiff{ |
| 324 | + file("stale.txt", 1), |
| 325 | + }, |
| 326 | + ChangedPaths: []string{"stale.txt"}, |
| 327 | + }) |
| 328 | + reset, _ := queuedOld.(Model).Update(BaselineResetMsg{NewSHA: "new-sha"}) |
| 329 | + queuedNew, _ := reset.(Model).Update(FilesUpdatedMsg{ |
| 330 | + BaselineSHA: "new-sha", |
| 331 | + Files: []internal.FileDiff{ |
| 332 | + file("fresh.txt", 1), |
| 333 | + }, |
| 334 | + ChangedPaths: []string{"fresh.txt"}, |
| 335 | + }) |
| 336 | + closed, _ := queuedNew.(Model).Update(tea.KeyMsg{Type: tea.KeyEsc}) |
| 337 | + got := closed.(Model) |
| 338 | + |
| 339 | + if len(got.Files) != 1 || got.Files[0].Path != "fresh.txt" { |
| 340 | + t.Fatalf("fresh overlay update should win after reset, got %#v", got.Files) |
| 341 | + } |
| 342 | +} |
| 343 | + |
284 | 344 | // TestModelViewSmallHeightDoesNotPanic verifies tiny terminal heights do not |
285 | 345 | // trigger negative content heights in overlay rendering. |
286 | 346 | func TestModelViewSmallHeightDoesNotPanic(t *testing.T) { |
@@ -321,6 +381,54 @@ func TestModelViewEmptyStateFitsViewport(t *testing.T) { |
321 | 381 | } |
322 | 382 | } |
323 | 383 |
|
| 384 | +// TestModelViewNarrowWidthKeepsChromeSingleLine verifies header and footer stay |
| 385 | +// within the viewport width instead of relying on terminal soft-wrap. |
| 386 | +func TestModelViewNarrowWidthKeepsChromeSingleLine(t *testing.T) { |
| 387 | + model := NewModel("repo", "/tmp/repo", "sha", []internal.FileDiff{{ |
| 388 | + Path: "very/long/path/to/current-file-name.ts", |
| 389 | + AddCount: 12, |
| 390 | + DelCount: 3, |
| 391 | + }}) |
| 392 | + model.Width = 20 |
| 393 | + model.Height = 4 |
| 394 | + |
| 395 | + view := model.View() |
| 396 | + lines := strings.Split(view, "\n") |
| 397 | + if len(lines) != 4 { |
| 398 | + t.Fatalf("line count = %d, want 4; view = %q", len(lines), view) |
| 399 | + } |
| 400 | + if lipgloss.Width(lines[0]) > model.Width { |
| 401 | + t.Fatalf("header width = %d, want <= %d; header = %q", lipgloss.Width(lines[0]), model.Width, lines[0]) |
| 402 | + } |
| 403 | + if lipgloss.Width(lines[3]) > model.Width { |
| 404 | + t.Fatalf("footer width = %d, want <= %d; footer = %q", lipgloss.Width(lines[3]), model.Width, lines[3]) |
| 405 | + } |
| 406 | +} |
| 407 | + |
| 408 | +// TestModelOverlayNarrowWidthKeepsEntriesSingleLine verifies overlay rows fit |
| 409 | +// the viewport width instead of depending on soft-wrap. |
| 410 | +func TestModelOverlayNarrowWidthKeepsEntriesSingleLine(t *testing.T) { |
| 411 | + model := NewModel("repo", "/tmp/repo", "sha", []internal.FileDiff{ |
| 412 | + {Path: "very/long/path/to/current-file-name.ts", AddCount: 12, DelCount: 3}, |
| 413 | + {Path: "another/extremely/long/path/to/second-file.ts", AddCount: 1}, |
| 414 | + }) |
| 415 | + model.Width = 20 |
| 416 | + model.Height = 4 |
| 417 | + model.OverlayOpen = true |
| 418 | + model.OverlaySnapshot = append([]internal.FileDiff(nil), model.Files...) |
| 419 | + |
| 420 | + view := model.View() |
| 421 | + lines := strings.Split(view, "\n") |
| 422 | + if len(lines) != 4 { |
| 423 | + t.Fatalf("line count = %d, want 4; view = %q", len(lines), view) |
| 424 | + } |
| 425 | + for i := 1; i <= 2; i++ { |
| 426 | + if lipgloss.Width(lines[i]) > model.Width { |
| 427 | + t.Fatalf("overlay line %d width = %d, want <= %d; line = %q", i, lipgloss.Width(lines[i]), model.Width, lines[i]) |
| 428 | + } |
| 429 | + } |
| 430 | +} |
| 431 | + |
324 | 432 | // TestModelScrollOffsetStaysWithinContent verifies repeated down-navigation does |
325 | 433 | // not grow scroll state beyond the visible diff content. |
326 | 434 | func TestModelScrollOffsetStaysWithinContent(t *testing.T) { |
|
0 commit comments