list_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. package list
  2. import (
  3. "testing"
  4. tea "github.com/charmbracelet/bubbletea/v2"
  5. "github.com/sst/opencode/internal/styles"
  6. )
  7. // testItem is a simple test implementation of ListItem
  8. type testItem struct {
  9. value string
  10. }
  11. func (t testItem) Render(
  12. selected bool,
  13. width int,
  14. isFirstInViewport bool,
  15. baseStyle styles.Style,
  16. ) string {
  17. return t.value
  18. }
  19. func (t testItem) Selectable() bool {
  20. return true
  21. }
  22. // createTestList creates a list with test items for testing
  23. func createTestList() *listComponent[testItem] {
  24. items := []testItem{
  25. {value: "item1"},
  26. {value: "item2"},
  27. {value: "item3"},
  28. }
  29. list := NewListComponent(
  30. WithItems(items),
  31. WithMaxVisibleItems[testItem](5),
  32. WithFallbackMessage[testItem]("empty"),
  33. WithAlphaNumericKeys[testItem](false),
  34. WithRenderFunc(
  35. func(item testItem, selected bool, width int, baseStyle styles.Style) string {
  36. return item.Render(selected, width, false, baseStyle)
  37. },
  38. ),
  39. WithSelectableFunc(func(item testItem) bool {
  40. return item.Selectable()
  41. }),
  42. WithHeightFunc(func(item testItem, isFirstInViewport bool) int {
  43. return 1
  44. }),
  45. )
  46. return list.(*listComponent[testItem])
  47. }
  48. func TestArrowKeyNavigation(t *testing.T) {
  49. list := createTestList()
  50. // Test down arrow navigation
  51. downKey := tea.KeyPressMsg{Code: tea.KeyDown}
  52. updatedModel, _ := list.Update(downKey)
  53. list = updatedModel.(*listComponent[testItem])
  54. _, idx := list.GetSelectedItem()
  55. if idx != 1 {
  56. t.Errorf("Expected selected index 1 after down arrow, got %d", idx)
  57. }
  58. // Test up arrow navigation
  59. upKey := tea.KeyPressMsg{Code: tea.KeyUp}
  60. updatedModel, _ = list.Update(upKey)
  61. list = updatedModel.(*listComponent[testItem])
  62. _, idx = list.GetSelectedItem()
  63. if idx != 0 {
  64. t.Errorf("Expected selected index 0 after up arrow, got %d", idx)
  65. }
  66. }
  67. func TestJKKeyNavigation(t *testing.T) {
  68. items := []testItem{
  69. {value: "item1"},
  70. {value: "item2"},
  71. {value: "item3"},
  72. }
  73. // Create list with alpha keys enabled
  74. list := NewListComponent(
  75. WithItems(items),
  76. WithMaxVisibleItems[testItem](5),
  77. WithFallbackMessage[testItem]("empty"),
  78. WithAlphaNumericKeys[testItem](true),
  79. WithRenderFunc(
  80. func(item testItem, selected bool, width int, baseStyle styles.Style) string {
  81. return item.Render(selected, width, false, baseStyle)
  82. },
  83. ),
  84. WithSelectableFunc(func(item testItem) bool {
  85. return item.Selectable()
  86. }),
  87. WithHeightFunc(func(item testItem, isFirstInViewport bool) int {
  88. return 1
  89. }),
  90. )
  91. // Test j key (down)
  92. jKey := tea.KeyPressMsg{Code: 'j', Text: "j"}
  93. updatedModel, _ := list.Update(jKey)
  94. list = updatedModel.(*listComponent[testItem])
  95. _, idx := list.GetSelectedItem()
  96. if idx != 1 {
  97. t.Errorf("Expected selected index 1 after 'j' key, got %d", idx)
  98. }
  99. // Test k key (up)
  100. kKey := tea.KeyPressMsg{Code: 'k', Text: "k"}
  101. updatedModel, _ = list.Update(kKey)
  102. list = updatedModel.(*listComponent[testItem])
  103. _, idx = list.GetSelectedItem()
  104. if idx != 0 {
  105. t.Errorf("Expected selected index 0 after 'k' key, got %d", idx)
  106. }
  107. }
  108. func TestCtrlNavigation(t *testing.T) {
  109. list := createTestList()
  110. // Test Ctrl-N (down)
  111. ctrlN := tea.KeyPressMsg{Code: 'n', Mod: tea.ModCtrl}
  112. updatedModel, _ := list.Update(ctrlN)
  113. list = updatedModel.(*listComponent[testItem])
  114. _, idx := list.GetSelectedItem()
  115. if idx != 1 {
  116. t.Errorf("Expected selected index 1 after Ctrl-N, got %d", idx)
  117. }
  118. // Test Ctrl-P (up)
  119. ctrlP := tea.KeyPressMsg{Code: 'p', Mod: tea.ModCtrl}
  120. updatedModel, _ = list.Update(ctrlP)
  121. list = updatedModel.(*listComponent[testItem])
  122. _, idx = list.GetSelectedItem()
  123. if idx != 0 {
  124. t.Errorf("Expected selected index 0 after Ctrl-P, got %d", idx)
  125. }
  126. }
  127. func TestNavigationBoundaries(t *testing.T) {
  128. list := createTestList()
  129. // Test up arrow at first item (should stay at 0)
  130. upKey := tea.KeyPressMsg{Code: tea.KeyUp}
  131. updatedModel, _ := list.Update(upKey)
  132. list = updatedModel.(*listComponent[testItem])
  133. _, idx := list.GetSelectedItem()
  134. if idx != 0 {
  135. t.Errorf("Expected to stay at index 0 when pressing up at first item, got %d", idx)
  136. }
  137. // Move to last item
  138. downKey := tea.KeyPressMsg{Code: tea.KeyDown}
  139. updatedModel, _ = list.Update(downKey)
  140. list = updatedModel.(*listComponent[testItem])
  141. updatedModel, _ = list.Update(downKey)
  142. list = updatedModel.(*listComponent[testItem])
  143. _, idx = list.GetSelectedItem()
  144. if idx != 2 {
  145. t.Errorf("Expected to be at index 2, got %d", idx)
  146. }
  147. // Test down arrow at last item (should stay at 2)
  148. updatedModel, _ = list.Update(downKey)
  149. list = updatedModel.(*listComponent[testItem])
  150. _, idx = list.GetSelectedItem()
  151. if idx != 2 {
  152. t.Errorf("Expected to stay at index 2 when pressing down at last item, got %d", idx)
  153. }
  154. }
  155. func TestEmptyList(t *testing.T) {
  156. emptyList := NewListComponent(
  157. WithItems([]testItem{}),
  158. WithMaxVisibleItems[testItem](5),
  159. WithFallbackMessage[testItem]("empty"),
  160. WithAlphaNumericKeys[testItem](false),
  161. WithRenderFunc(
  162. func(item testItem, selected bool, width int, baseStyle styles.Style) string {
  163. return item.Render(selected, width, false, baseStyle)
  164. },
  165. ),
  166. WithSelectableFunc(func(item testItem) bool {
  167. return item.Selectable()
  168. }),
  169. WithHeightFunc(func(item testItem, isFirstInViewport bool) int {
  170. return 1
  171. }),
  172. )
  173. // Test navigation on empty list (should not crash)
  174. downKey := tea.KeyPressMsg{Code: tea.KeyDown}
  175. upKey := tea.KeyPressMsg{Code: tea.KeyUp}
  176. ctrlN := tea.KeyPressMsg{Code: 'n', Mod: tea.ModCtrl}
  177. ctrlP := tea.KeyPressMsg{Code: 'p', Mod: tea.ModCtrl}
  178. updatedModel, _ := emptyList.Update(downKey)
  179. emptyList = updatedModel.(*listComponent[testItem])
  180. updatedModel, _ = emptyList.Update(upKey)
  181. emptyList = updatedModel.(*listComponent[testItem])
  182. updatedModel, _ = emptyList.Update(ctrlN)
  183. emptyList = updatedModel.(*listComponent[testItem])
  184. updatedModel, _ = emptyList.Update(ctrlP)
  185. emptyList = updatedModel.(*listComponent[testItem])
  186. // Verify empty list behavior
  187. _, idx := emptyList.GetSelectedItem()
  188. if idx != -1 {
  189. t.Errorf("Expected index -1 for empty list, got %d", idx)
  190. }
  191. if !emptyList.IsEmpty() {
  192. t.Error("Expected IsEmpty() to return true for empty list")
  193. }
  194. }