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}