list_test.go

  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}