589 lines
14 KiB
Go
589 lines
14 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|