ai-agent/internal/tui/width_test.go
admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

370 lines
13 KiB
Go

package tui
import (
"strings"
"testing"
)
// TestViewportWidthCalculation tests that viewport width calculations are consistent
// across all components to prevent horizontal scrolling.
func TestViewportWidthCalculation(t *testing.T) {
tests := []struct {
name string
screenWidth int
panelVisible bool
panelWidth int
wantViewWidth int
wantContentW int
wantMarkdownW int
}{
{
name: "small screen with panel",
screenWidth: 80,
panelVisible: true,
panelWidth: 25,
wantViewWidth: 80 - 25 - 2, // screen - panel - separator
wantContentW: 80 - 25 - 5, // screen - panel - separator - padding
wantMarkdownW: 80 - 25 - 5,
},
{
name: "medium screen with panel",
screenWidth: 120,
panelVisible: true,
panelWidth: 30,
wantViewWidth: 120 - 30 - 2,
wantContentW: 120 - 30 - 5,
wantMarkdownW: 120 - 30 - 5,
},
{
name: "large screen with panel",
screenWidth: 160,
panelVisible: true,
panelWidth: 40,
wantViewWidth: 160 - 40 - 2,
wantContentW: 160 - 40 - 5,
wantMarkdownW: 160 - 40 - 5,
},
{
name: "small screen without panel",
screenWidth: 80,
panelVisible: false,
panelWidth: 0,
wantViewWidth: 80 - 1, // just separator
wantContentW: 80 - 1 - 3, // viewport - padding for markdown
wantMarkdownW: 80 - 1 - 3,
},
{
name: "large screen without panel",
screenWidth: 200,
panelVisible: false,
panelWidth: 0,
wantViewWidth: 200 - 1,
wantContentW: 200 - 1 - 3,
wantMarkdownW: 200 - 1 - 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Simulate the width calculation from model.go:WindowSizeMsg handler
panelWidth := tt.panelWidth
if panelWidth == 0 && tt.panelVisible {
// Calculate panel width based on screen width (from model.go:365-371)
panelWidth = 30
if tt.screenWidth < 100 {
panelWidth = 25
} else if tt.screenWidth > 160 {
panelWidth = 40
}
}
// Viewport width (from model.go:373-380)
viewportWidth := tt.screenWidth - 1
if tt.panelVisible {
viewportWidth = tt.screenWidth - panelWidth - 2
}
if viewportWidth < 20 {
viewportWidth = 20
}
// Content/markdown width (from model.go:382-386)
markdownWidth := viewportWidth - 3
if markdownWidth < 20 {
markdownWidth = 20
}
// Content width for rendering (from view.go:422-429)
contentW := tt.screenWidth - 4
if tt.panelVisible {
contentW = tt.screenWidth - panelWidth - 5
}
if contentW < 20 {
contentW = 20
}
// Verify consistency
if markdownWidth != tt.wantMarkdownW {
t.Errorf("markdown width = %d, want %d", markdownWidth, tt.wantMarkdownW)
}
if viewportWidth != tt.wantViewWidth {
t.Errorf("viewport width = %d, want %d", viewportWidth, tt.wantViewWidth)
}
if contentW != tt.wantContentW {
t.Errorf("content width = %d, want %d", contentW, tt.wantContentW)
}
// CRITICAL: viewport width should never exceed screen width minus panel
maxAllowedWidth := tt.screenWidth - 1
if tt.panelVisible {
maxAllowedWidth = tt.screenWidth - panelWidth - 1
}
if viewportWidth > maxAllowedWidth {
t.Errorf("viewport width %d exceeds max allowed %d - will cause horizontal scroll",
viewportWidth, maxAllowedWidth)
}
// Content width should be <= viewport width
if contentW > viewportWidth {
t.Errorf("content width %d > viewport width %d - will cause horizontal scroll",
contentW, viewportWidth)
}
// Markdown width should be <= viewport width
if markdownWidth > viewportWidth {
t.Errorf("markdown width %d > viewport width %d - will cause horizontal scroll",
markdownWidth, viewportWidth)
}
})
}
}
// TestResponsiveWidthToggle tests that toggling the side panel maintains proper widths
func TestResponsiveWidthToggle(t *testing.T) {
screenWidth := 120
panelWidth := 30
// Panel visible
viewportWithPanel := screenWidth - panelWidth - 2
contentWithPanel := screenWidth - panelWidth - 5
// Panel hidden
viewportWithoutPanel := screenWidth - 1
contentWithoutPanel := screenWidth - 4
// Widths should increase when panel is hidden
if viewportWithoutPanel <= viewportWithPanel {
t.Errorf("viewport should be wider when panel is hidden: %d <= %d",
viewportWithoutPanel, viewportWithPanel)
}
if contentWithoutPanel <= contentWithPanel {
t.Errorf("content should be wider when panel is hidden: %d <= %d",
contentWithoutPanel, contentWithPanel)
}
// Neither should exceed screen width
if viewportWithoutPanel > screenWidth {
t.Errorf("viewport without panel %d exceeds screen width %d",
viewportWithoutPanel, screenWidth)
}
if viewportWithPanel > screenWidth-panelWidth {
t.Errorf("viewport with panel %d exceeds available space %d",
viewportWithPanel, screenWidth-panelWidth)
}
}
// TestMinimumWidthConstraints tests that minimum width constraints prevent negative layouts
func TestMinimumWidthConstraints(t *testing.T) {
tests := []struct {
name string
screenWidth int
}{
{"tiny screen", 40},
{"very small screen", 60},
{"small screen", 80},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
panelWidth := 25 // minimum panel width
minWidth := 20 // minimum content width
// Calculate viewport width with panel
viewportWidth := tt.screenWidth - panelWidth - 2
if viewportWidth < minWidth {
viewportWidth = minWidth
}
// Calculate content width with panel
contentWidth := tt.screenWidth - panelWidth - 5
if contentWidth < minWidth {
contentWidth = minWidth
}
// Verify minimums are respected
if viewportWidth < minWidth {
t.Errorf("viewport width %d below minimum %d", viewportWidth, minWidth)
}
if contentWidth < minWidth {
t.Errorf("content width %d below minimum %d", contentWidth, minWidth)
}
// Even with minimum constraints, total shouldn't exceed screen
totalWidth := panelWidth + 1 + viewportWidth
if totalWidth > tt.screenWidth && tt.screenWidth >= minWidth+panelWidth+1 {
t.Errorf("total width %d exceeds screen width %d", totalWidth, tt.screenWidth)
}
})
}
}
// TestRenderedTextWidth simulates actual rendered text to ensure it fits within viewport
func TestRenderedTextWidth(t *testing.T) {
tests := []struct {
name string
screenWidth int
panelWidth int
text string
}{
{
name: "long line with panel",
screenWidth: 120,
panelWidth: 30,
text: strings.Repeat("x", 100),
},
{
name: "long line without panel",
screenWidth: 120,
panelWidth: 0,
text: strings.Repeat("x", 120),
},
{
name: "short line",
screenWidth: 80,
panelWidth: 25,
text: "short text",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Calculate available content width
availableWidth := tt.screenWidth - 4
if tt.panelWidth > 0 {
availableWidth = tt.screenWidth - tt.panelWidth - 5
}
if availableWidth < 20 {
availableWidth = 20
}
// Simulate wrapText behavior
wrapped := wrapText(tt.text, availableWidth)
// Check each line fits
lines := strings.Split(wrapped, "\n")
for i, line := range lines {
if len(line) > availableWidth {
t.Errorf("line %d length %d exceeds available width %d",
i, len(line), availableWidth)
}
}
})
}
}
// TestLayoutConsistency verifies that all width calculations are consistent
func TestLayoutConsistency(t *testing.T) {
// Test various screen sizes
for screenWidth := 40; screenWidth <= 200; screenWidth += 10 {
t.Run("screen_width", func(t *testing.T) {
// Determine panel width (from model.go logic)
panelWidth := 30
if screenWidth < 100 {
panelWidth = 25
} else if screenWidth > 160 {
panelWidth = 40
}
// Test with panel visible
t.Run("with_panel", func(t *testing.T) {
// Viewport width calculation (from model.go:373-380)
viewportWidth := screenWidth - panelWidth - 2
if viewportWidth < 20 {
viewportWidth = 20
}
// Content width calculation (from model.go:382-386)
markdownWidth := viewportWidth - 3
if markdownWidth < 20 {
markdownWidth = 20
}
contentWidth := screenWidth - panelWidth - 5
if contentWidth < 20 {
contentWidth = 20
}
// All widths should be consistent
if contentWidth > viewportWidth {
t.Errorf("content %d > viewport %d", contentWidth, viewportWidth)
}
if markdownWidth > viewportWidth {
t.Errorf("markdown %d > viewport %d", markdownWidth, viewportWidth)
}
// For very small screens, the layout might exceed screen width
// This is expected - the minimum viewport width takes precedence
minRequiredWidth := panelWidth + 1 + 20 // panel + separator + min viewport
if screenWidth >= minRequiredWidth {
// Only check total width if screen is large enough
totalWidth := panelWidth + 1 + viewportWidth
if totalWidth > screenWidth {
t.Errorf("total layout %d exceeds screen %d", totalWidth, screenWidth)
}
}
})
// Test without panel
t.Run("without_panel", func(t *testing.T) {
// Viewport width calculation
viewportWidth := screenWidth - 1
if viewportWidth < 20 {
viewportWidth = 20
}
// Content width calculation
markdownWidth := viewportWidth - 3
if markdownWidth < 20 {
markdownWidth = 20
}
contentWidth := screenWidth - 4
if contentWidth < 20 {
contentWidth = 20
}
// All widths should be consistent
if contentWidth > viewportWidth {
t.Errorf("content %d > viewport %d", contentWidth, viewportWidth)
}
if markdownWidth > viewportWidth {
t.Errorf("markdown %d > viewport %d", markdownWidth, viewportWidth)
}
// For very small screens, minimum width takes precedence
if screenWidth >= 21 { // 1 + min viewport (20)
if viewportWidth > screenWidth {
t.Errorf("viewport %d exceeds screen %d", viewportWidth, screenWidth)
}
}
})
})
}
}