1package list
2
3import (
4 "fmt"
5 "testing"
6
7 tea "github.com/charmbracelet/bubbletea/v2"
8 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
9 "github.com/charmbracelet/lipgloss/v2"
10 "github.com/charmbracelet/x/exp/golden"
11 "github.com/google/uuid"
12 "github.com/stretchr/testify/assert"
13)
14
15func TestBackwardList(t *testing.T) {
16 t.Run("within height", func(t *testing.T) {
17 t.Parallel()
18 l := New(WithDirection(Backward), WithGap(1)).(*list)
19 l.SetSize(10, 20)
20 items := []Item{}
21 for i := range 5 {
22 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
23 items = append(items, item)
24 }
25 cmd := l.SetItems(items)
26 if cmd != nil {
27 cmd()
28 }
29
30 // should select the last item
31 assert.Equal(t, l.selectedItem, items[len(items)-1].ID())
32
33 golden.RequireEqual(t, []byte(l.View()))
34 })
35 t.Run("should not change selected item", func(t *testing.T) {
36 t.Parallel()
37 items := []Item{}
38 for i := range 5 {
39 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
40 items = append(items, item)
41 }
42 l := New(WithDirection(Backward), WithGap(1), WithSelectedItem(items[2].ID())).(*list)
43 l.SetSize(10, 20)
44 cmd := l.SetItems(items)
45 if cmd != nil {
46 cmd()
47 }
48 // should select the last item
49 assert.Equal(t, l.selectedItem, items[2].ID())
50 })
51 t.Run("more than height", func(t *testing.T) {
52 t.Parallel()
53 l := New(WithDirection(Backward))
54 l.SetSize(10, 5)
55 items := []Item{}
56 for i := range 10 {
57 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
58 items = append(items, item)
59 }
60 cmd := l.SetItems(items)
61 if cmd != nil {
62 cmd()
63 }
64
65 golden.RequireEqual(t, []byte(l.View()))
66 })
67 t.Run("more than height multi line", func(t *testing.T) {
68 t.Parallel()
69 l := New(WithDirection(Backward))
70 l.SetSize(10, 5)
71 items := []Item{}
72 for i := range 10 {
73 item := NewSimpleItem(fmt.Sprintf("Item %d\nLine2", i))
74 items = append(items, item)
75 }
76 cmd := l.SetItems(items)
77 if cmd != nil {
78 cmd()
79 }
80
81 golden.RequireEqual(t, []byte(l.View()))
82 })
83 t.Run("should move up", func(t *testing.T) {
84 t.Parallel()
85 l := New(WithDirection(Backward)).(*list)
86 l.SetSize(10, 5)
87 items := []Item{}
88 for i := range 10 {
89 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
90 items = append(items, item)
91 }
92 cmd := l.SetItems(items)
93 if cmd != nil {
94 cmd()
95 }
96
97 l.MoveUp(1)
98 golden.RequireEqual(t, []byte(l.View()))
99 })
100 t.Run("should move at max to the top", func(t *testing.T) {
101 t.Parallel()
102 l := New(WithDirection(Backward)).(*list)
103 l.SetSize(10, 5)
104 items := []Item{}
105 for i := range 10 {
106 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
107 items = append(items, item)
108 }
109 cmd := l.SetItems(items)
110 if cmd != nil {
111 cmd()
112 }
113
114 l.MoveUp(100)
115 assert.Equal(t, l.offset, lipgloss.Height(l.rendered)-l.listHeight())
116 golden.RequireEqual(t, []byte(l.View()))
117 })
118 t.Run("should do nothing with wrong move number", func(t *testing.T) {
119 t.Parallel()
120 l := New(WithDirection(Backward)).(*list)
121 l.SetSize(10, 5)
122 items := []Item{}
123 for i := range 10 {
124 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
125 items = append(items, item)
126 }
127 cmd := l.SetItems(items)
128 if cmd != nil {
129 cmd()
130 }
131
132 l.MoveUp(-10)
133 golden.RequireEqual(t, []byte(l.View()))
134 })
135 t.Run("should move to the top", func(t *testing.T) {
136 t.Parallel()
137 l := New(WithDirection(Backward)).(*list)
138 l.SetSize(10, 5)
139 items := []Item{}
140 for i := range 10 {
141 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
142 items = append(items, item)
143 }
144 cmd := l.SetItems(items)
145 if cmd != nil {
146 cmd()
147 }
148
149 l.GoToTop()
150 assert.Equal(t, l.direction, Forward)
151 golden.RequireEqual(t, []byte(l.View()))
152 })
153 t.Run("should select the item above", func(t *testing.T) {
154 t.Parallel()
155 l := New(WithDirection(Backward)).(*list)
156 l.SetSize(10, 5)
157 items := []Item{}
158 for i := range 10 {
159 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
160 items = append(items, item)
161 }
162 cmd := l.SetItems(items)
163 if cmd != nil {
164 cmd()
165 }
166
167 selectedInx := len(l.items) - 2
168 currentItem := items[len(l.items)-1]
169 nextItem := items[selectedInx]
170 assert.False(t, nextItem.(SimpleItem).IsFocused())
171 assert.True(t, currentItem.(SimpleItem).IsFocused())
172 cmd = l.SelectItemAbove()
173 if cmd != nil {
174 cmd()
175 }
176
177 assert.Equal(t, l.selectedItem, l.items[selectedInx].ID())
178 assert.True(t, l.items[selectedInx].(SimpleItem).IsFocused())
179
180 golden.RequireEqual(t, []byte(l.View()))
181 })
182 t.Run("should move the view to be able to see the selected item", func(t *testing.T) {
183 t.Parallel()
184 l := New(WithDirection(Backward)).(*list)
185 l.SetSize(10, 5)
186 items := []Item{}
187 for i := range 10 {
188 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
189 items = append(items, item)
190 }
191 cmd := l.SetItems(items)
192 if cmd != nil {
193 cmd()
194 }
195
196 for range 5 {
197 cmd = l.SelectItemAbove()
198 if cmd != nil {
199 cmd()
200 }
201 }
202 golden.RequireEqual(t, []byte(l.View()))
203 })
204}
205
206func TestForwardList(t *testing.T) {
207 t.Run("within height", func(t *testing.T) {
208 t.Parallel()
209 l := New(WithDirection(Forward), WithGap(1)).(*list)
210 l.SetSize(10, 20)
211 items := []Item{}
212 for i := range 5 {
213 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
214 items = append(items, item)
215 }
216 cmd := l.SetItems(items)
217 if cmd != nil {
218 cmd()
219 }
220
221 // should select the last item
222 assert.Equal(t, l.selectedItem, items[0].ID())
223
224 golden.RequireEqual(t, []byte(l.View()))
225 })
226 t.Run("should not change selected item", func(t *testing.T) {
227 t.Parallel()
228 items := []Item{}
229 for i := range 5 {
230 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
231 items = append(items, item)
232 }
233 l := New(WithDirection(Forward), WithGap(1), WithSelectedItem(items[2].ID())).(*list)
234 l.SetSize(10, 20)
235 cmd := l.SetItems(items)
236 if cmd != nil {
237 cmd()
238 }
239 // should select the last item
240 assert.Equal(t, l.selectedItem, items[2].ID())
241 })
242 t.Run("more than height", func(t *testing.T) {
243 t.Parallel()
244 l := New(WithDirection(Forward))
245 l.SetSize(10, 5)
246 items := []Item{}
247 for i := range 10 {
248 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
249 items = append(items, item)
250 }
251 cmd := l.SetItems(items)
252 if cmd != nil {
253 cmd()
254 }
255
256 golden.RequireEqual(t, []byte(l.View()))
257 })
258 t.Run("more than height multi line", func(t *testing.T) {
259 t.Parallel()
260 l := New(WithDirection(Forward))
261 l.SetSize(10, 5)
262 items := []Item{}
263 for i := range 10 {
264 item := NewSimpleItem(fmt.Sprintf("Item %d\nLine2", i))
265 items = append(items, item)
266 }
267 cmd := l.SetItems(items)
268 if cmd != nil {
269 cmd()
270 }
271
272 golden.RequireEqual(t, []byte(l.View()))
273 })
274 t.Run("should move down", func(t *testing.T) {
275 t.Parallel()
276 l := New(WithDirection(Forward)).(*list)
277 l.SetSize(10, 5)
278 items := []Item{}
279 for i := range 10 {
280 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
281 items = append(items, item)
282 }
283 cmd := l.SetItems(items)
284 if cmd != nil {
285 cmd()
286 }
287
288 l.MoveDown(1)
289 golden.RequireEqual(t, []byte(l.View()))
290 })
291 t.Run("should move at max to the top", func(t *testing.T) {
292 t.Parallel()
293 l := New(WithDirection(Forward)).(*list)
294 l.SetSize(10, 5)
295 items := []Item{}
296 for i := range 10 {
297 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
298 items = append(items, item)
299 }
300 cmd := l.SetItems(items)
301 if cmd != nil {
302 cmd()
303 }
304
305 l.MoveDown(100)
306 assert.Equal(t, l.offset, lipgloss.Height(l.rendered)-l.listHeight())
307 golden.RequireEqual(t, []byte(l.View()))
308 })
309 t.Run("should do nothing with wrong move number", func(t *testing.T) {
310 t.Parallel()
311 l := New(WithDirection(Forward)).(*list)
312 l.SetSize(10, 5)
313 items := []Item{}
314 for i := range 10 {
315 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
316 items = append(items, item)
317 }
318 cmd := l.SetItems(items)
319 if cmd != nil {
320 cmd()
321 }
322
323 l.MoveDown(-10)
324 golden.RequireEqual(t, []byte(l.View()))
325 })
326 t.Run("should move to the bottom", func(t *testing.T) {
327 t.Parallel()
328 l := New(WithDirection(Forward)).(*list)
329 l.SetSize(10, 5)
330 items := []Item{}
331 for i := range 10 {
332 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
333 items = append(items, item)
334 }
335 cmd := l.SetItems(items)
336 if cmd != nil {
337 cmd()
338 }
339
340 l.GoToBottom()
341 assert.Equal(t, l.direction, Backward)
342 golden.RequireEqual(t, []byte(l.View()))
343 })
344 t.Run("should select the item below", func(t *testing.T) {
345 t.Parallel()
346 l := New(WithDirection(Forward)).(*list)
347 l.SetSize(10, 5)
348 items := []Item{}
349 for i := range 10 {
350 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
351 items = append(items, item)
352 }
353 cmd := l.SetItems(items)
354 if cmd != nil {
355 cmd()
356 }
357
358 selectedInx := 1
359 currentItem := items[0]
360 nextItem := items[selectedInx]
361 assert.False(t, nextItem.(SimpleItem).IsFocused())
362 assert.True(t, currentItem.(SimpleItem).IsFocused())
363 cmd = l.SelectItemBelow()
364 if cmd != nil {
365 cmd()
366 }
367
368 assert.Equal(t, l.selectedItem, l.items[selectedInx].ID())
369 assert.True(t, l.items[selectedInx].(SimpleItem).IsFocused())
370
371 golden.RequireEqual(t, []byte(l.View()))
372 })
373 t.Run("should move the view to be able to see the selected item", func(t *testing.T) {
374 t.Parallel()
375 l := New(WithDirection(Backward)).(*list)
376 l.SetSize(10, 5)
377 items := []Item{}
378 for i := range 10 {
379 item := NewSimpleItem(fmt.Sprintf("Item %d", i))
380 items = append(items, item)
381 }
382 cmd := l.SetItems(items)
383 if cmd != nil {
384 cmd()
385 }
386
387 for range 5 {
388 cmd = l.SelectItemBelow()
389 if cmd != nil {
390 cmd()
391 }
392 }
393 golden.RequireEqual(t, []byte(l.View()))
394 })
395}
396
397type SimpleItem interface {
398 Item
399 layout.Focusable
400}
401
402type simpleItem struct {
403 width int
404 content string
405 id string
406 focused bool
407}
408
409func NewSimpleItem(content string) SimpleItem {
410 return &simpleItem{
411 width: 0,
412 content: content,
413 focused: false,
414 id: uuid.NewString(),
415 }
416}
417
418func (s *simpleItem) ID() string {
419 return s.id
420}
421
422func (s *simpleItem) Init() tea.Cmd {
423 return nil
424}
425
426func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
427 return s, nil
428}
429
430func (s *simpleItem) View() string {
431 if s.focused {
432 return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
433 }
434 return lipgloss.NewStyle().Width(s.width).Render(s.content)
435}
436
437func (l *simpleItem) GetSize() (int, int) {
438 return l.width, 0
439}
440
441// SetSize implements Item.
442func (s *simpleItem) SetSize(width int, height int) tea.Cmd {
443 s.width = width
444 return nil
445}
446
447// Blur implements SimpleItem.
448func (s *simpleItem) Blur() tea.Cmd {
449 s.focused = false
450 return nil
451}
452
453// Focus implements SimpleItem.
454func (s *simpleItem) Focus() tea.Cmd {
455 s.focused = true
456 return nil
457}
458
459// IsFocused implements SimpleItem.
460func (s *simpleItem) IsFocused() bool {
461 return s.focused
462}