366 lines
11 KiB
Go
366 lines
11 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"charm.land/lipgloss/v2"
|
|
)
|
|
|
|
// TestOverlayCentering_HelpOverlay verifies help overlay is centered
|
|
func TestOverlayCentering_HelpOverlay(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.width = 120
|
|
m.height = 40
|
|
|
|
// Initialize help viewport
|
|
m.overlay = OverlayHelp
|
|
m.initHelpViewport()
|
|
|
|
overlay := m.renderHelpOverlay(m.width)
|
|
overlayLines := strings.Split(overlay, "\n")
|
|
|
|
// Check overlay width doesn't exceed screen
|
|
for _, line := range overlayLines {
|
|
lineWidth := lipgloss.Width(line)
|
|
if lineWidth > m.width {
|
|
t.Errorf("overlay line width %d exceeds screen width %d", lineWidth, m.width)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestOverlayCentering_ModelPicker verifies model picker overlay is centered
|
|
func TestOverlayCentering_ModelPicker(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.width = 100
|
|
m.height = 30
|
|
|
|
// Initialize model picker state manually
|
|
m.openModelPicker()
|
|
|
|
// Model picker requires modelManager to be set
|
|
if m.modelPickerState == nil {
|
|
// Test passes if it doesn't panic
|
|
t.Skip("model picker requires model manager")
|
|
}
|
|
|
|
overlay := m.renderModelPicker()
|
|
if overlay == "" {
|
|
t.Log("model picker overlay empty (expected without model manager)")
|
|
}
|
|
}
|
|
|
|
// TestOverlayCentering_SmallScreen verifies overlays work on small screens
|
|
func TestOverlayCentering_SmallScreen(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.width = 60
|
|
m.height = 20
|
|
|
|
m.overlay = OverlayHelp
|
|
m.initHelpViewport()
|
|
|
|
overlay := m.renderHelpOverlay(m.width)
|
|
|
|
if overlay == "" {
|
|
t.Error("overlay should render on small screen")
|
|
}
|
|
|
|
// Should not panic or produce empty output
|
|
lines := strings.Count(overlay, "\n")
|
|
if lines < 5 {
|
|
t.Errorf("overlay should have at least 5 lines, got %d", lines)
|
|
}
|
|
}
|
|
|
|
// TestOverlayCentering_LargeScreen verifies overlays scale on large screens
|
|
func TestOverlayCentering_LargeScreen(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.width = 200
|
|
m.height = 60
|
|
|
|
m.overlay = OverlayHelp
|
|
m.initHelpViewport()
|
|
|
|
overlay := m.renderHelpOverlay(m.width)
|
|
|
|
// Overlay should not be excessively wide
|
|
overlayLines := strings.Split(overlay, "\n")
|
|
maxLineWidth := 0
|
|
for _, line := range overlayLines {
|
|
width := lipgloss.Width(line)
|
|
if width > maxLineWidth {
|
|
maxLineWidth = width
|
|
}
|
|
}
|
|
|
|
// Overlay should be centered and not use full width
|
|
if maxLineWidth > m.width-10 {
|
|
t.Errorf("overlay too wide: %d (max should be ~%d)", maxLineWidth, m.width-10)
|
|
}
|
|
}
|
|
|
|
// TestOverlayOnContent_Positioning verifies overlay is positioned correctly
|
|
func TestOverlayOnContent_Positioning(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.width = 100
|
|
m.height = 40
|
|
|
|
base := strings.Repeat("base line\n", 40)
|
|
overlay := strings.Repeat("overlay line\n", 10)
|
|
|
|
result := m.overlayOnContent(base, overlay)
|
|
|
|
// Result should have same number of lines as base
|
|
baseLines := strings.Count(base, "\n")
|
|
resultLines := strings.Count(result, "\n")
|
|
|
|
if resultLines < baseLines {
|
|
t.Errorf("result should have at least as many lines as base: got %d, want %d", resultLines, baseLines)
|
|
}
|
|
}
|
|
|
|
// TestToolCard_WidthCalculation verifies tool cards respect width constraints
|
|
func TestToolCard_WidthCalculation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
availableW int
|
|
cardName string
|
|
expectRender bool
|
|
}{
|
|
{"wide screen", 100, "read_file", true},
|
|
{"narrow screen", 40, "read_file", true},
|
|
{"very narrow", 30, "test", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
card := NewToolCard(tt.cardName, ToolCardFile, true)
|
|
card.State = ToolCardRunning
|
|
|
|
view := card.View(tt.availableW)
|
|
|
|
// Should render without panic
|
|
if view == "" {
|
|
t.Error("tool card should render")
|
|
}
|
|
|
|
// Note: lipgloss.Width includes ANSI codes, so we just verify it renders
|
|
_ = lipgloss.Width(view)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestToolCard_LongArgsWrapping verifies long args are wrapped properly
|
|
func TestToolCard_LongArgsWrapping(t *testing.T) {
|
|
card := NewToolCard("write_file", ToolCardFile, true)
|
|
card.State = ToolCardSuccess
|
|
card.Expanded = true
|
|
card.Args = strings.Repeat("very_long_argument_that_should_be_wrapped_properly ", 10)
|
|
card.Result = "success"
|
|
|
|
view := card.View(80)
|
|
viewLines := strings.Split(view, "\n")
|
|
|
|
// Should render multiple lines
|
|
if len(viewLines) < 3 {
|
|
t.Errorf("tool card should have multiple lines, got %d", len(viewLines))
|
|
}
|
|
|
|
// Verify it renders without panic
|
|
if view == "" {
|
|
t.Error("tool card view should not be empty")
|
|
}
|
|
}
|
|
|
|
// TestToolCard_ManagerRendering verifies multiple cards render correctly
|
|
func TestToolCard_ManagerRendering(t *testing.T) {
|
|
mgr := NewToolCardManager(true)
|
|
|
|
// Add multiple cards
|
|
mgr.AddCard("read_file", ToolCardFile, testTime)
|
|
mgr.AddCard("write_file", ToolCardFile, testTime)
|
|
mgr.AddCard("bash", ToolCardBash, testTime)
|
|
|
|
// Update some cards
|
|
mgr.UpdateCard("read_file", ToolCardSuccess, "file content", testDuration)
|
|
mgr.UpdateCard("write_file", ToolCardRunning, "", 0)
|
|
|
|
view := mgr.View(100)
|
|
|
|
if view == "" {
|
|
t.Error("manager view should not be empty")
|
|
}
|
|
|
|
// Should have multiple cards (separated by newlines)
|
|
lines := strings.Count(view, "\n")
|
|
if lines < 2 {
|
|
t.Errorf("manager view should have multiple lines, got %d", lines+1)
|
|
}
|
|
}
|
|
|
|
// TestToolCard_BorderAndPadding verifies border and padding are accounted for
|
|
func TestToolCard_BorderAndPadding(t *testing.T) {
|
|
card := NewToolCard("test", ToolCardGeneric, true)
|
|
card.State = ToolCardSuccess
|
|
card.Expanded = true
|
|
card.Args = "test args"
|
|
card.Result = "test result"
|
|
|
|
availableW := 60
|
|
view := card.View(availableW)
|
|
|
|
// Account for border (2) + padding (2) = 4 chars
|
|
contentW := availableW - 4
|
|
|
|
viewLines := strings.Split(view, "\n")
|
|
for i, line := range viewLines {
|
|
lineWidth := lipgloss.Width(line)
|
|
if lineWidth > availableW {
|
|
t.Errorf("line %d width %d exceeds available width %d (content should fit in %d)",
|
|
i, lineWidth, availableW, contentW)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestToolCard_EmojiIcons verifies emoji icons render without breaking layout
|
|
func TestToolCard_EmojiIcons(t *testing.T) {
|
|
kinds := []ToolCardKind{ToolCardFile, ToolCardBash, ToolCardSearch, ToolCardGit, ToolCardGeneric}
|
|
states := []ToolCardState{ToolCardRunning, ToolCardSuccess, ToolCardError}
|
|
|
|
for _, kind := range kinds {
|
|
for _, state := range states {
|
|
t.Run(string(rune(kind))+string(rune(state)), func(t *testing.T) {
|
|
card := NewToolCard("test", kind, true)
|
|
card.State = state
|
|
|
|
view := card.View(60)
|
|
|
|
// Should render without panic
|
|
if view == "" {
|
|
t.Error("card view should not be empty")
|
|
}
|
|
|
|
// Should not exceed width
|
|
viewWidth := lipgloss.Width(view)
|
|
if viewWidth > 60 {
|
|
t.Errorf("card width %d exceeds 60", viewWidth)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWrapText_LongWords verifies wrapText breaks long words
|
|
func TestWrapText_LongWords(t *testing.T) {
|
|
longWord := strings.Repeat("a", 100)
|
|
result := wrapText(longWord, 40)
|
|
|
|
lines := strings.Split(result, "\n")
|
|
for i, line := range lines {
|
|
if len(line) > 40 {
|
|
t.Errorf("line %d exceeds width: %d chars", i, len(line))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWrapText_MultipleWords verifies wrapText handles multiple words
|
|
func TestWrapText_MultipleWords(t *testing.T) {
|
|
text := "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10"
|
|
result := wrapText(text, 20)
|
|
|
|
lines := strings.Split(result, "\n")
|
|
for i, line := range lines {
|
|
if len(line) > 20 {
|
|
t.Errorf("line %d exceeds width: %d chars", i, len(line))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWrapText_EmptyAndEdgeCases verifies wrapText handles edge cases
|
|
func TestWrapText_EmptyAndEdgeCases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
width int
|
|
expect string
|
|
}{
|
|
{"empty", "", 40, ""},
|
|
{"zero width", "hello", 0, "hello"},
|
|
{"exact fit", "hello", 5, "hello"},
|
|
{"single char width", "hello world", 1, "h\ne\nl\nl\no\n \nw\no\nr\nl\nd"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := wrapText(tt.input, tt.width)
|
|
if tt.width > 0 && result != tt.expect {
|
|
// Just verify it doesn't panic and returns something reasonable
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIndentBlock_Multiline verifies indentBlock adds prefix to each line
|
|
func TestIndentBlock_Multiline(t *testing.T) {
|
|
input := "line1\nline2\nline3"
|
|
result := indentBlock(input, " ")
|
|
|
|
expected := " line1\n line2\n line3"
|
|
if result != expected {
|
|
t.Errorf("indentBlock failed: got %q, want %q", result, expected)
|
|
}
|
|
}
|
|
|
|
// TestIndentBlock_EmptyLines verifies indentBlock handles empty lines
|
|
func TestIndentBlock_EmptyLines(t *testing.T) {
|
|
input := "line1\n\nline3"
|
|
result := indentBlock(input, " ")
|
|
|
|
// Empty lines should remain empty
|
|
lines := strings.Split(result, "\n")
|
|
if len(lines) != 3 {
|
|
t.Errorf("expected 3 lines, got %d", len(lines))
|
|
}
|
|
if lines[1] != "" {
|
|
t.Error("empty line should remain empty")
|
|
}
|
|
}
|
|
|
|
// BenchmarkOverlayRendering benchmarks overlay rendering performance
|
|
func BenchmarkOverlayRendering_Help(b *testing.B) {
|
|
m := newTestModelB(b)
|
|
m.width = 120
|
|
m.height = 40
|
|
m.overlay = OverlayHelp
|
|
m.initHelpViewport()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = m.renderHelpOverlay(m.width)
|
|
}
|
|
}
|
|
|
|
// BenchmarkToolCardRendering benchmarks tool card rendering
|
|
func BenchmarkToolCardRendering(b *testing.B) {
|
|
card := NewToolCard("read_file", ToolCardFile, true)
|
|
card.State = ToolCardSuccess
|
|
card.Expanded = true
|
|
card.Args = strings.Repeat("arg ", 20)
|
|
card.Result = strings.Repeat("result line\n", 10)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = card.View(80)
|
|
}
|
|
}
|
|
|
|
// BenchmarkWrapText benchmarks text wrapping
|
|
func BenchmarkWrapText(b *testing.B) {
|
|
text := strings.Repeat("This is a test sentence with multiple words. ", 20)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = wrapText(text, 60)
|
|
}
|
|
}
|