list_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. WithMaxVisibleHeight[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. )
  43. return list.(*listComponent[testItem])
  44. }
  45. func TestArrowKeyNavigation(t *testing.T) {
  46. list := createTestList()
  47. // Test down arrow navigation
  48. downKey := tea.KeyPressMsg{Code: tea.KeyDown}
  49. updatedModel, _ := list.Update(downKey)
  50. list = updatedModel.(*listComponent[testItem])
  51. _, idx := list.GetSelectedItem()
  52. if idx != 1 {
  53. t.Errorf("Expected selected index 1 after down arrow, got %d", idx)
  54. }
  55. // Test up arrow navigation
  56. upKey := tea.KeyPressMsg{Code: tea.KeyUp}
  57. updatedModel, _ = list.Update(upKey)
  58. list = updatedModel.(*listComponent[testItem])
  59. _, idx = list.GetSelectedItem()
  60. if idx != 0 {
  61. t.Errorf("Expected selected index 0 after up arrow, got %d", idx)
  62. }
  63. }
  64. func TestJKKeyNavigation(t *testing.T) {
  65. items := []testItem{
  66. {value: "item1"},
  67. {value: "item2"},
  68. {value: "item3"},
  69. }
  70. // Create list with alpha keys enabled
  71. list := NewListComponent(
  72. WithItems(items),
  73. WithMaxVisibleHeight[testItem](5),
  74. WithFallbackMessage[testItem]("empty"),
  75. WithAlphaNumericKeys[testItem](true),
  76. WithRenderFunc(
  77. func(item testItem, selected bool, width int, baseStyle styles.Style) string {
  78. return item.Render(selected, width, false, baseStyle)
  79. },
  80. ),
  81. WithSelectableFunc(func(item testItem) bool {
  82. return item.Selectable()
  83. }),
  84. )
  85. // Test j key (down)
  86. jKey := tea.KeyPressMsg{Code: 'j', Text: "j"}
  87. updatedModel, _ := list.Update(jKey)
  88. list = updatedModel.(*listComponent[testItem])
  89. _, idx := list.GetSelectedItem()
  90. if idx != 1 {
  91. t.Errorf("Expected selected index 1 after 'j' key, got %d", idx)
  92. }
  93. // Test k key (up)
  94. kKey := tea.KeyPressMsg{Code: 'k', Text: "k"}
  95. updatedModel, _ = list.Update(kKey)
  96. list = updatedModel.(*listComponent[testItem])
  97. _, idx = list.GetSelectedItem()
  98. if idx != 0 {
  99. t.Errorf("Expected selected index 0 after 'k' key, got %d", idx)
  100. }
  101. }
  102. func TestCtrlNavigation(t *testing.T) {
  103. list := createTestList()
  104. // Test Ctrl-N (down)
  105. ctrlN := tea.KeyPressMsg{Code: 'n', Mod: tea.ModCtrl}
  106. updatedModel, _ := list.Update(ctrlN)
  107. list = updatedModel.(*listComponent[testItem])
  108. _, idx := list.GetSelectedItem()
  109. if idx != 1 {
  110. t.Errorf("Expected selected index 1 after Ctrl-N, got %d", idx)
  111. }
  112. // Test Ctrl-P (up)
  113. ctrlP := tea.KeyPressMsg{Code: 'p', Mod: tea.ModCtrl}
  114. updatedModel, _ = list.Update(ctrlP)
  115. list = updatedModel.(*listComponent[testItem])
  116. _, idx = list.GetSelectedItem()
  117. if idx != 0 {
  118. t.Errorf("Expected selected index 0 after Ctrl-P, got %d", idx)
  119. }
  120. }
  121. func TestNavigationBoundaries(t *testing.T) {
  122. list := createTestList()
  123. // Test up arrow at first item (should wrap to last item)
  124. upKey := tea.KeyPressMsg{Code: tea.KeyUp}
  125. updatedModel, _ := list.Update(upKey)
  126. list = updatedModel.(*listComponent[testItem])
  127. _, idx := list.GetSelectedItem()
  128. if idx != 2 {
  129. t.Errorf("Expected to wrap to index 2 when pressing up at first item, got %d", idx)
  130. }
  131. // Move to first item
  132. list.SetSelectedIndex(0)
  133. // Move to last item
  134. downKey := tea.KeyPressMsg{Code: tea.KeyDown}
  135. updatedModel, _ = list.Update(downKey)
  136. list = updatedModel.(*listComponent[testItem])
  137. updatedModel, _ = list.Update(downKey)
  138. list = updatedModel.(*listComponent[testItem])
  139. _, idx = list.GetSelectedItem()
  140. if idx != 2 {
  141. t.Errorf("Expected to be at index 2, got %d", idx)
  142. }
  143. // Test down arrow at last item (should wrap to first item)
  144. updatedModel, _ = list.Update(downKey)
  145. list = updatedModel.(*listComponent[testItem])
  146. _, idx = list.GetSelectedItem()
  147. if idx != 0 {
  148. t.Errorf("Expected to wrap to index 0 when pressing down at last item, got %d", idx)
  149. }
  150. }
  151. func TestEmptyList(t *testing.T) {
  152. emptyList := NewListComponent(
  153. WithItems([]testItem{}),
  154. WithMaxVisibleHeight[testItem](5),
  155. WithFallbackMessage[testItem]("empty"),
  156. WithAlphaNumericKeys[testItem](false),
  157. WithRenderFunc(
  158. func(item testItem, selected bool, width int, baseStyle styles.Style) string {
  159. return item.Render(selected, width, false, baseStyle)
  160. },
  161. ),
  162. WithSelectableFunc(func(item testItem) bool {
  163. return item.Selectable()
  164. }),
  165. )
  166. // Test navigation on empty list (should not crash)
  167. downKey := tea.KeyPressMsg{Code: tea.KeyDown}
  168. upKey := tea.KeyPressMsg{Code: tea.KeyUp}
  169. ctrlN := tea.KeyPressMsg{Code: 'n', Mod: tea.ModCtrl}
  170. ctrlP := tea.KeyPressMsg{Code: 'p', Mod: tea.ModCtrl}
  171. updatedModel, _ := emptyList.Update(downKey)
  172. emptyList = updatedModel.(*listComponent[testItem])
  173. updatedModel, _ = emptyList.Update(upKey)
  174. emptyList = updatedModel.(*listComponent[testItem])
  175. updatedModel, _ = emptyList.Update(ctrlN)
  176. emptyList = updatedModel.(*listComponent[testItem])
  177. updatedModel, _ = emptyList.Update(ctrlP)
  178. emptyList = updatedModel.(*listComponent[testItem])
  179. // Verify empty list behavior
  180. _, idx := emptyList.GetSelectedItem()
  181. if idx != -1 {
  182. t.Errorf("Expected index -1 for empty list, got %d", idx)
  183. }
  184. if !emptyList.IsEmpty() {
  185. t.Error("Expected IsEmpty() to return true for empty list")
  186. }
  187. }
  188. func TestWrapAroundNavigation(t *testing.T) {
  189. list := createTestList()
  190. // Start at first item (index 0)
  191. _, idx := list.GetSelectedItem()
  192. if idx != 0 {
  193. t.Errorf("Expected to start at index 0, got %d", idx)
  194. }
  195. // Press up arrow - should wrap to last item (index 2)
  196. upKey := tea.KeyPressMsg{Code: tea.KeyUp}
  197. updatedModel, _ := list.Update(upKey)
  198. list = updatedModel.(*listComponent[testItem])
  199. _, idx = list.GetSelectedItem()
  200. if idx != 2 {
  201. t.Errorf("Expected to wrap to index 2 when pressing up from first item, got %d", idx)
  202. }
  203. // Press down arrow - should wrap to first item (index 0)
  204. downKey := tea.KeyPressMsg{Code: tea.KeyDown}
  205. updatedModel, _ = list.Update(downKey)
  206. list = updatedModel.(*listComponent[testItem])
  207. _, idx = list.GetSelectedItem()
  208. if idx != 0 {
  209. t.Errorf("Expected to wrap to index 0 when pressing down from last item, got %d", idx)
  210. }
  211. // Navigate to middle and verify normal navigation still works
  212. updatedModel, _ = list.Update(downKey)
  213. list = updatedModel.(*listComponent[testItem])
  214. _, idx = list.GetSelectedItem()
  215. if idx != 1 {
  216. t.Errorf("Expected to move to index 1, got %d", idx)
  217. }
  218. }