package core import "testing" // TestCommandOutputHeight tests the Height() method for various configurations func TestCommandOutputHeight(t *testing.T) { t.Run("inline mode returns 1", func(t *testing.T) { co := &CommandOutput{ Title: "Test Title", Lines: []string{"line1", "line2", "line3"}, Inline: true, } if got := co.Height(); got != 1 { t.Errorf("Height() = %d, want 1 for inline mode", got) } }) t.Run("empty output with no title", func(t *testing.T) { co := &CommandOutput{ Lines: []string{}, Inline: false, } // 0 lines + 0 title + 2 padding = 2 want := 2 if got := co.Height(); got != want { t.Errorf("Height() = %d, want %d", got, want) } }) t.Run("one line with title", func(t *testing.T) { co := &CommandOutput{ Title: "Test", Lines: []string{"line1"}, Inline: false, } // 1 line + 1 title + 2 padding = 4 want := 4 if got := co.Height(); got != want { t.Errorf("Height() = %d, want %d", got, want) } }) t.Run("one line without title", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"line1"}, Inline: false, } // 1 line + 0 title + 2 padding = 3 want := 3 if got := co.Height(); got != want { t.Errorf("Height() = %d, want %d", got, want) } }) t.Run("multiple lines with title", func(t *testing.T) { co := &CommandOutput{ Title: "Output", Lines: []string{"line1", "line2", "line3", "line4", "line5"}, Inline: false, } // 5 lines + 1 title + 2 padding = 8 want := 8 if got := co.Height(); got != want { t.Errorf("Height() = %d, want %d", got, want) } }) t.Run("whitespace-only title counts as no title", func(t *testing.T) { co := &CommandOutput{ Title: " ", Lines: []string{"line1"}, Inline: false, } // 1 line + 0 title (whitespace) + 2 padding = 3 want := 3 if got := co.Height(); got != want { t.Errorf("Height() = %d, want %d", got, want) } }) t.Run("empty string title counts as no title", func(t *testing.T) { co := &CommandOutput{ Title: "", Lines: []string{"line1", "line2"}, Inline: false, } // 2 lines + 0 title + 2 padding = 4 want := 4 if got := co.Height(); got != want { t.Errorf("Height() = %d, want %d", got, want) } }) } // TestCommandOutputIsActive tests the IsActive() method func TestCommandOutputIsActive(t *testing.T) { t.Run("active with lines", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"line1"}, } if !co.IsActive() { t.Error("IsActive() = false, want true when lines present") } }) t.Run("inactive with no lines", func(t *testing.T) { co := &CommandOutput{ Lines: []string{}, } if co.IsActive() { t.Error("IsActive() = true, want false when no lines") } }) t.Run("inactive with nil lines", func(t *testing.T) { co := &CommandOutput{ Lines: nil, } if co.IsActive() { t.Error("IsActive() = true, want false when lines is nil") } }) t.Run("active with multiple lines", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"line1", "line2", "line3"}, } if !co.IsActive() { t.Error("IsActive() = false, want true when multiple lines present") } }) } // TestMaxOutputWindowHeight tests the maxOutputWindowHeight helper function func TestMaxOutputWindowHeight(t *testing.T) { tests := []struct { name string termHeight int want int }{ {"100 height terminal", 100, 75}, {"80 height terminal", 80, 60}, {"40 height terminal", 40, 30}, {"24 height terminal", 24, 18}, {"20 height terminal", 20, 15}, {"10 height terminal", 10, 7}, {"1 height terminal", 1, 0}, {"0 height terminal", 0, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := maxOutputWindowHeight(tt.termHeight) if got != tt.want { t.Errorf("maxOutputWindowHeight(%d) = %d, want %d", tt.termHeight, got, tt.want) } }) } } // TestCommandOutputViewport tests the Viewport() method func TestCommandOutputViewport(t *testing.T) { t.Run("viewport at start with small content", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"line1", "line2", "line3"}, ScrollOffset: 0, } // Terminal height 100 → max window = 75, viewport shows all 3 lines viewport := co.Viewport(100) if len(viewport) != 3 { t.Errorf("Viewport() returned %d lines, want 3", len(viewport)) } want := []string{"line1", "line2", "line3"} for i, line := range viewport { if line != want[i] { t.Errorf("Viewport()[%d] = %q, want %q", i, line, want[i]) } } }) t.Run("viewport at start with large content", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = string(rune('A' + (i % 26))) } co := &CommandOutput{ Lines: lines, ScrollOffset: 0, } // Terminal height 100 → max window = 75 viewport := co.Viewport(100) if len(viewport) != 75 { t.Errorf("Viewport() returned %d lines, want 75", len(viewport)) } // Should start with first line if viewport[0] != "A" { t.Errorf("Viewport()[0] = %q, want 'A'", viewport[0]) } }) t.Run("viewport scrolled down", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"}, ScrollOffset: 3, } // Terminal height 20 → max window = 15 viewport := co.Viewport(20) // Should start at offset 3 (line "D") if viewport[0] != "D" { t.Errorf("Viewport()[0] = %q, want 'D' (offset 3)", viewport[0]) } }) t.Run("viewport with different terminal heights", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = string(rune('0' + (i % 10))) } co := &CommandOutput{ Lines: lines, ScrollOffset: 0, } // Small terminal (40 lines) → max window = 30 viewport := co.Viewport(40) if len(viewport) != 30 { t.Errorf("Viewport(40) returned %d lines, want 30", len(viewport)) } // Large terminal (200 lines) → max window = 150, but only 100 lines available viewport = co.Viewport(200) if len(viewport) != 100 { t.Errorf("Viewport(200) returned %d lines, want 100 (all available)", len(viewport)) } }) t.Run("viewport at maximum scroll", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"A", "B", "C", "D", "E"}, ScrollOffset: 2, // Showing lines from index 2 onwards } // Terminal height 20 → max window = 15 (but only 3 lines available from offset 2) viewport := co.Viewport(20) want := []string{"C", "D", "E"} if len(viewport) != len(want) { t.Errorf("Viewport() returned %d lines, want %d", len(viewport), len(want)) } for i, line := range viewport { if line != want[i] { t.Errorf("Viewport()[%d] = %q, want %q", i, line, want[i]) } } }) } // TestCommandOutputScrollDown tests the ScrollDown() method func TestCommandOutputScrollDown(t *testing.T) { t.Run("scroll down with space available", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = string(rune('A' + (i % 26))) } co := &CommandOutput{ Lines: lines, ScrollOffset: 0, } // Terminal height 100 → max window = 75 // Can scroll since 100 lines > 75 viewport co.ScrollDown(100) if co.ScrollOffset != 1 { t.Errorf("ScrollOffset = %d after ScrollDown, want 1", co.ScrollOffset) } }) t.Run("scroll down multiple times", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = string(rune('A' + (i % 26))) } co := &CommandOutput{ Lines: lines, ScrollOffset: 0, } // Scroll down 5 times for i := 0; i < 5; i++ { co.ScrollDown(100) } if co.ScrollOffset != 5 { t.Errorf("ScrollOffset = %d after 5 ScrollDown calls, want 5", co.ScrollOffset) } }) t.Run("cannot scroll past end of content", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"A", "B", "C", "D", "E"}, ScrollOffset: 0, } // Terminal height 20 → max window = 15 // 5 lines fit entirely in viewport, should not scroll co.ScrollDown(20) if co.ScrollOffset != 0 { t.Errorf("ScrollOffset = %d, want 0 (should not scroll when content fits)", co.ScrollOffset) } }) t.Run("scroll down to maximum", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"}, ScrollOffset: 0, } // Terminal height 16 → max window = 12 // 10 lines total, can scroll down until offset + 12 >= 10 // Max offset = 10 - 12 = -2, but we can't go negative // Actually: can scroll while (offset + 12) < 10 // So max offset before stopping = 10 - 12 = can't scroll at all? Let me recalculate // offset=0: show lines 0-11 (but only 10 exist) → shows all 10 // Can't scroll since viewport > content // Let's use different numbers: 20 lines, window of 12 lines := make([]string, 20) for i := range lines { lines[i] = string(rune('A' + i)) } co.Lines = lines // Max scroll: offset + 12 < 20 → offset < 8 → max offset = 7 // But the function allows offset until (offset + 12) < 20 // So when offset = 8, offset + 12 = 20, not < 20, stops for i := 0; i < 20; i++ { co.ScrollDown(16) } // Maximum offset should be 8 (showing lines 8-19, which is 12 lines) want := 8 if co.ScrollOffset != want { t.Errorf("ScrollOffset = %d after scrolling to max, want %d", co.ScrollOffset, want) } }) t.Run("scroll down at maximum has no effect", func(t *testing.T) { lines := make([]string, 20) for i := range lines { lines[i] = string(rune('A' + i)) } co := &CommandOutput{ Lines: lines, ScrollOffset: 8, // Already at max for height 16 (window 12) } co.ScrollDown(16) if co.ScrollOffset != 8 { t.Errorf("ScrollOffset = %d, want 8 (should not scroll past end)", co.ScrollOffset) } }) t.Run("scroll with different terminal heights", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = string(rune('A' + (i % 26))) } co := &CommandOutput{ Lines: lines, ScrollOffset: 0, } // Small terminal (height 40 → window 30) co.ScrollDown(40) if co.ScrollOffset != 1 { t.Errorf("ScrollOffset = %d for height 40, want 1", co.ScrollOffset) } // Reset co.ScrollOffset = 0 // Large terminal (height 200 → window 150) co.ScrollDown(200) if co.ScrollOffset != 0 { t.Errorf("ScrollOffset = %d for height 200, want 0 (content fits entirely)", co.ScrollOffset) } }) } // TestCommandOutputScrollUp tests the ScrollUp() method func TestCommandOutputScrollUp(t *testing.T) { t.Run("scroll up from offset", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"A", "B", "C", "D", "E"}, ScrollOffset: 3, } co.ScrollUp() if co.ScrollOffset != 2 { t.Errorf("ScrollOffset = %d after ScrollUp, want 2", co.ScrollOffset) } }) t.Run("scroll up multiple times", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"A", "B", "C", "D", "E"}, ScrollOffset: 5, } for i := 0; i < 3; i++ { co.ScrollUp() } if co.ScrollOffset != 2 { t.Errorf("ScrollOffset = %d after 3 ScrollUp calls, want 2", co.ScrollOffset) } }) t.Run("cannot scroll up past zero", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"A", "B", "C", "D", "E"}, ScrollOffset: 0, } co.ScrollUp() if co.ScrollOffset != 0 { t.Errorf("ScrollOffset = %d, want 0 (should not go negative)", co.ScrollOffset) } }) t.Run("scroll up to zero", func(t *testing.T) { co := &CommandOutput{ Lines: []string{"A", "B", "C", "D", "E"}, ScrollOffset: 3, } // Scroll up 3 times to reach 0 for i := 0; i < 3; i++ { co.ScrollUp() } if co.ScrollOffset != 0 { t.Errorf("ScrollOffset = %d after scrolling to top, want 0", co.ScrollOffset) } // Try scrolling up one more time co.ScrollUp() if co.ScrollOffset != 0 { t.Errorf("ScrollOffset = %d after scrolling past top, want 0", co.ScrollOffset) } }) t.Run("scroll up from large offset", func(t *testing.T) { lines := make([]string, 100) for i := range lines { lines[i] = string(rune('A' + (i % 26))) } co := &CommandOutput{ Lines: lines, ScrollOffset: 50, } co.ScrollUp() if co.ScrollOffset != 49 { t.Errorf("ScrollOffset = %d after ScrollUp, want 49", co.ScrollOffset) } }) } // TestCommandOutputScrollingIntegration tests combined scrolling behavior func TestCommandOutputScrollingIntegration(t *testing.T) { t.Run("scroll down and up", func(t *testing.T) { lines := make([]string, 50) for i := range lines { lines[i] = string(rune('A' + (i % 26))) } co := &CommandOutput{ Lines: lines, ScrollOffset: 0, } termHeight := 40 // max window = 30 // Scroll down 5 times for i := 0; i < 5; i++ { co.ScrollDown(termHeight) } if co.ScrollOffset != 5 { t.Errorf("ScrollOffset = %d after 5 down, want 5", co.ScrollOffset) } // Scroll up 2 times for i := 0; i < 2; i++ { co.ScrollUp() } if co.ScrollOffset != 3 { t.Errorf("ScrollOffset = %d after 2 up, want 3", co.ScrollOffset) } // Scroll back to top for i := 0; i < 10; i++ { co.ScrollUp() } if co.ScrollOffset != 0 { t.Errorf("ScrollOffset = %d after scrolling to top, want 0", co.ScrollOffset) } }) t.Run("scroll to bottom and back to top", func(t *testing.T) { lines := make([]string, 30) for i := range lines { lines[i] = string(rune('A' + i)) } co := &CommandOutput{ Lines: lines, ScrollOffset: 0, } termHeight := 28 // max window = 21 // Scroll to bottom (max offset = 30 - 21 = 9) for i := 0; i < 20; i++ { co.ScrollDown(termHeight) } want := 9 if co.ScrollOffset != want { t.Errorf("ScrollOffset = %d after scrolling to bottom, want %d", co.ScrollOffset, want) } // Verify viewport shows last lines viewport := co.Viewport(termHeight) if len(viewport) != 21 { t.Errorf("Viewport length = %d, want 21", len(viewport)) } if viewport[len(viewport)-1] != string(rune('A'+29)) { t.Errorf("Last viewport line = %q, want %q", viewport[len(viewport)-1], string(rune('A'+29))) } // Scroll back to top for i := 0; i < 20; i++ { co.ScrollUp() } if co.ScrollOffset != 0 { t.Errorf("ScrollOffset = %d after scrolling back to top, want 0", co.ScrollOffset) } }) }