1package list
  2
  3import (
  4	"fmt"
  5	"strings"
  6	"testing"
  7
  8	tea "github.com/charmbracelet/bubbletea/v2"
  9	"github.com/stretchr/testify/assert"
 10	"github.com/stretchr/testify/require"
 11)
 12
 13// mockVariableHeightItem is a test item with configurable height
 14type mockVariableHeightItem struct {
 15	id      string
 16	height  int
 17	content string
 18}
 19
 20func (m *mockVariableHeightItem) ID() string {
 21	return m.id
 22}
 23
 24func (m *mockVariableHeightItem) Init() tea.Cmd {
 25	return nil
 26}
 27
 28func (m *mockVariableHeightItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 29	return m, nil
 30}
 31
 32func (m *mockVariableHeightItem) View() string {
 33	lines := make([]string, m.height)
 34	for i := 0; i < m.height; i++ {
 35		if i == 0 {
 36			lines[i] = m.content
 37		} else {
 38			lines[i] = fmt.Sprintf("  Line %d", i+1)
 39		}
 40	}
 41	return strings.Join(lines, "\n")
 42}
 43
 44func (m *mockVariableHeightItem) SetSize(width, height int) tea.Cmd {
 45	return nil
 46}
 47
 48func (m *mockVariableHeightItem) IsFocused() bool {
 49	return false
 50}
 51
 52func (m *mockVariableHeightItem) Focus() tea.Cmd {
 53	return nil
 54}
 55
 56func (m *mockVariableHeightItem) Blur() tea.Cmd {
 57	return nil
 58}
 59
 60func (m *mockVariableHeightItem) GetSize() (int, int) {
 61	return 0, m.height
 62}
 63
 64func TestArrowKeyNavigation(t *testing.T) {
 65	t.Run("should show full item when navigating with arrow keys", func(t *testing.T) {
 66		// Create items with varying heights
 67		items := []Item{
 68			&mockVariableHeightItem{id: "item1", height: 2, content: "Item 1 (2 lines)"},
 69			&mockVariableHeightItem{id: "item2", height: 3, content: "Item 2 (3 lines)"},
 70			&mockVariableHeightItem{id: "item3", height: 1, content: "Item 3 (1 line)"},
 71			&mockVariableHeightItem{id: "item4", height: 4, content: "Item 4 (4 lines)"},
 72			&mockVariableHeightItem{id: "item5", height: 2, content: "Item 5 (2 lines)"},
 73		}
 74
 75		// Create list with viewport height of 6, width of 40
 76		l := New(items, WithDirectionForward(), WithSize(40, 6)).(*list[Item])
 77		execCmdNav(l, l.Init())
 78
 79		// Initial state - first item should be selected
 80		assert.Equal(t, "item1", l.SelectedItemID())
 81		assert.Equal(t, 0, l.offset)
 82
 83		// Navigate down to item 2
 84		_, cmd := l.Update(tea.KeyPressMsg(tea.Key{
 85			Code: tea.KeyDown,
 86		}))
 87		execCmdNav(l, cmd)
 88
 89		assert.Equal(t, "item2", l.SelectedItemID())
 90		// Item 2 should be fully visible
 91		view := l.View()
 92		assert.Contains(t, view, "Item 2 (3 lines)")
 93		assert.Contains(t, view, "  Line 2")
 94		assert.Contains(t, view, "  Line 3")
 95
 96		// Navigate down to item 3
 97		_, cmd = l.Update(tea.KeyPressMsg(tea.Key{
 98			Code: tea.KeyDown,
 99		}))
100		execCmdNav(l, cmd)
101
102		assert.Equal(t, "item3", l.SelectedItemID())
103		view = l.View()
104		assert.Contains(t, view, "Item 3 (1 line)")
105
106		// Navigate down to item 4 (4 lines - might need scrolling)
107		_, cmd = l.Update(tea.KeyPressMsg(tea.Key{
108			Code: tea.KeyDown,
109		}))
110		execCmdNav(l, cmd)
111
112		assert.Equal(t, "item4", l.SelectedItemID())
113		view = l.View()
114		// All lines of item 4 should be visible
115		assert.Contains(t, view, "Item 4 (4 lines)")
116		assert.Contains(t, view, "  Line 2")
117		assert.Contains(t, view, "  Line 3")
118		assert.Contains(t, view, "  Line 4")
119
120		// Navigate back up to item 3
121		_, cmd = l.Update(tea.KeyPressMsg(tea.Key{
122			Code: tea.KeyUp,
123		}))
124		execCmdNav(l, cmd)
125
126		assert.Equal(t, "item3", l.SelectedItemID())
127		view = l.View()
128		assert.Contains(t, view, "Item 3 (1 line)")
129
130		// Navigate back up to item 2
131		_, cmd = l.Update(tea.KeyPressMsg(tea.Key{
132			Code: tea.KeyUp,
133		}))
134		execCmdNav(l, cmd)
135
136		assert.Equal(t, "item2", l.SelectedItemID())
137		view = l.View()
138		// All lines of item 2 should be visible
139		assert.Contains(t, view, "Item 2 (3 lines)")
140		assert.Contains(t, view, "  Line 2")
141		assert.Contains(t, view, "  Line 3")
142	})
143
144	t.Run("should not show partial items at viewport boundaries", func(t *testing.T) {
145		// Create items with specific heights to test boundary conditions
146		items := []Item{
147			&mockVariableHeightItem{id: "item1", height: 3, content: "Item 1"},
148			&mockVariableHeightItem{id: "item2", height: 3, content: "Item 2"},
149			&mockVariableHeightItem{id: "item3", height: 3, content: "Item 3"},
150			&mockVariableHeightItem{id: "item4", height: 3, content: "Item 4"},
151		}
152
153		// Create list with viewport height of 5, width of 40 (can't fit 2 full 3-line items)
154		l := New(items, WithDirectionForward(), WithSize(40, 5)).(*list[Item])
155		execCmdNav(l, l.Init())
156
157		// Navigate to item 2
158		_, cmd := l.Update(tea.KeyPressMsg(tea.Key{
159			Code: tea.KeyDown,
160		}))
161		execCmdNav(l, cmd)
162
163		view := l.View()
164		lines := strings.Split(view, "\n")
165
166		// Check that we have exactly 5 lines (viewport height)
167		require.Len(t, lines, 5)
168
169		// Item 2 should be fully visible
170		assert.Contains(t, view, "Item 2")
171
172		// Count how many lines of each item are visible
173		item1Lines := 0
174		item2Lines := 0
175		for _, line := range lines {
176			if strings.Contains(line, "Item 1") || (item1Lines > 0 && item1Lines < 3) {
177				item1Lines++
178			}
179			if strings.Contains(line, "Item 2") || (item2Lines > 0 && item2Lines < 3) {
180				item2Lines++
181			}
182		}
183
184		// Item 2 should have all 3 lines visible
185		assert.Equal(t, 3, item2Lines, "Item 2 should be fully visible")
186	})
187
188	t.Run("should handle items taller than viewport", func(t *testing.T) {
189		// Create an item taller than the viewport
190		items := []Item{
191			&mockVariableHeightItem{id: "item1", height: 2, content: "Item 1"},
192			&mockVariableHeightItem{id: "item2", height: 8, content: "Item 2 (tall)"},
193			&mockVariableHeightItem{id: "item3", height: 2, content: "Item 3"},
194		}
195
196		// Create list with viewport height of 5, width of 40
197		l := New(items, WithDirectionForward(), WithSize(40, 5)).(*list[Item])
198		execCmdNav(l, l.Init())
199
200		// Navigate to the tall item
201		_, cmd := l.Update(tea.KeyPressMsg(tea.Key{
202			Code: tea.KeyDown,
203		}))
204		execCmdNav(l, cmd)
205
206		assert.Equal(t, "item2", l.SelectedItemID())
207		view := l.View()
208
209		// Should show the item from the top
210		assert.Contains(t, view, "Item 2 (tall)")
211		lines := strings.Split(view, "\n")
212		assert.Len(t, lines, 5) // Should fill viewport
213	})
214}
215
216// Helper function to execute commands
217func execCmdNav(l *list[Item], cmd tea.Cmd) {
218	if cmd == nil {
219		return
220	}
221	msg := cmd()
222	if msg != nil {
223		l.Update(msg)
224	}
225}