github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/tk/listbox_test.go (about) 1 package tk 2 3 import ( 4 "testing" 5 6 "github.com/markusbkk/elvish/pkg/cli/term" 7 "github.com/markusbkk/elvish/pkg/ui" 8 ) 9 10 var listBoxRenderVerticalTests = []renderTest{ 11 { 12 Name: "placeholder when Items is nil", 13 Given: NewListBox(ListBoxSpec{Placeholder: ui.T("nothing")}), 14 Width: 10, Height: 3, 15 Want: bb(10).Write("nothing"), 16 }, 17 { 18 Name: "placeholder when NItems is 0", 19 Given: NewListBox(ListBoxSpec{ 20 Placeholder: ui.T("nothing"), 21 State: ListBoxState{Items: TestItems{}}}), 22 Width: 10, Height: 3, 23 Want: bb(10).Write("nothing"), 24 }, 25 { 26 Name: "all items when there is enough height", 27 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 2}, Selected: 0}}), 28 Width: 10, Height: 3, 29 Want: bb(10). 30 Write("item 0 ", ui.Inverse). 31 Newline().Write("item 1"), 32 }, 33 { 34 Name: "long lines cropped", 35 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 2}, Selected: 0}}), 36 Width: 4, Height: 3, 37 Want: bb(4). 38 Write("item", ui.Inverse). 39 Newline().Write("item"), 40 }, 41 { 42 Name: "scrollbar when not showing all items", 43 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), 44 Width: 10, Height: 2, 45 Want: bb(10). 46 Write("item 0 ", ui.Inverse). 47 Write(" ", ui.Inverse, ui.FgMagenta). 48 Newline().Write("item 1 "). 49 Write("│", ui.FgMagenta), 50 }, 51 { 52 Name: "scrollbar when not showing last item in full", 53 Given: NewListBox(ListBoxSpec{ 54 State: ListBoxState{ 55 Items: TestItems{Prefix: "item\n", NItems: 2}, Selected: 0}}), 56 Width: 10, Height: 3, 57 Want: bb(10). 58 Write("item ", ui.Inverse). 59 Write(" ", ui.Inverse, ui.FgMagenta). 60 Newline().Write("0 ", ui.Inverse). 61 Write(" ", ui.Inverse, ui.FgMagenta). 62 Newline().Write("item "). 63 Write(" ", ui.Inverse, ui.FgMagenta), 64 }, 65 { 66 Name: "scrollbar when not showing only item in full", 67 Given: NewListBox(ListBoxSpec{ 68 State: ListBoxState{ 69 Items: TestItems{Prefix: "item\n", NItems: 1}, Selected: 0}}), 70 Width: 10, Height: 1, 71 Want: bb(10). 72 Write("item ", ui.Inverse). 73 Write(" ", ui.Inverse, ui.FgMagenta), 74 }, 75 { 76 Name: "padding", 77 Given: NewListBox( 78 ListBoxSpec{ 79 Padding: 1, 80 State: ListBoxState{ 81 Items: TestItems{Prefix: "item\n", NItems: 2}, Selected: 0}}), 82 Width: 4, Height: 4, 83 84 Want: bb(4). 85 Write(" it ", ui.Inverse).Newline(). 86 Write(" 0 ", ui.Inverse).Newline(). 87 Write(" it").Newline(). 88 Write(" 1").Buffer(), 89 }, 90 { 91 Name: "not extending style", 92 Given: NewListBox(ListBoxSpec{ 93 Padding: 1, 94 State: ListBoxState{ 95 Items: TestItems{ 96 Prefix: "x", NItems: 2, 97 Style: ui.Stylings(ui.FgBlue, ui.BgGreen)}}}), 98 Width: 6, Height: 2, 99 100 Want: bb(6). 101 Write(" ", ui.Inverse). 102 Write("x0", ui.FgBlue, ui.BgGreen, ui.Inverse). 103 Write(" ", ui.Inverse). 104 Newline(). 105 Write(" "). 106 Write("x1", ui.FgBlue, ui.BgGreen). 107 Buffer(), 108 }, 109 { 110 Name: "extending style", 111 Given: NewListBox(ListBoxSpec{ 112 Padding: 1, ExtendStyle: true, 113 State: ListBoxState{Items: TestItems{ 114 Prefix: "x", NItems: 2, 115 Style: ui.Stylings(ui.FgBlue, ui.BgGreen)}}}), 116 Width: 6, Height: 2, 117 118 Want: bb(6). 119 Write(" x0 ", ui.FgBlue, ui.BgGreen, ui.Inverse). 120 Newline(). 121 Write(" x1 ", ui.FgBlue, ui.BgGreen). 122 Buffer(), 123 }, 124 } 125 126 func TestListBox_Render_Vertical(t *testing.T) { 127 testRender(t, listBoxRenderVerticalTests) 128 } 129 130 func TestListBox_Render_Vertical_MutatesState(t *testing.T) { 131 // Calling Render alters the First field to reflect the first item rendered. 132 w := NewListBox(ListBoxSpec{ 133 State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 4, First: 0}}) 134 // Items shown will be 3, 4, 5 135 w.Render(10, 3) 136 state := w.CopyState() 137 if first := state.First; first != 3 { 138 t.Errorf("State.First = %d, want 3", first) 139 } 140 if height := state.Height; height != 3 { 141 t.Errorf("State.Height = %d, want 3", height) 142 } 143 } 144 145 var listBoxRenderHorizontalTests = []renderTest{ 146 { 147 Name: "placeholder when Items is nil", 148 Given: NewListBox(ListBoxSpec{Horizontal: true, Placeholder: ui.T("nothing")}), 149 Width: 10, Height: 3, 150 Want: bb(10).Write("nothing"), 151 }, 152 { 153 Name: "placeholder when NItems is 0", 154 Given: NewListBox(ListBoxSpec{ 155 Horizontal: true, Placeholder: ui.T("nothing"), 156 State: ListBoxState{Items: TestItems{}}}), 157 Width: 10, Height: 3, 158 Want: bb(10).Write("nothing"), 159 }, 160 { 161 Name: "all items when there is enough space, using minimal height", 162 Given: NewListBox(ListBoxSpec{ 163 Horizontal: true, 164 State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), 165 Width: 14, Height: 3, 166 // Available height is 3, but only need 2 lines. 167 Want: bb(14). 168 Write("item 0", ui.Inverse). 169 Write(" "). 170 Write("item 2"). 171 Newline().Write("item 1 item 3"), 172 }, 173 { 174 Name: "padding", 175 Given: NewListBox(ListBoxSpec{ 176 Horizontal: true, Padding: 1, 177 State: ListBoxState{Items: TestItems{NItems: 4, Prefix: "x"}, Selected: 0}}), 178 Width: 14, Height: 3, 179 Want: bb(14). 180 Write(" x0 ", ui.Inverse). 181 Write(" "). 182 Write(" x2"). 183 Newline().Write(" x1 x3"), 184 }, 185 { 186 Name: "extending style", 187 Given: NewListBox(ListBoxSpec{ 188 Horizontal: true, Padding: 1, ExtendStyle: true, 189 State: ListBoxState{Items: TestItems{ 190 NItems: 2, Prefix: "x", 191 Style: ui.Stylings(ui.FgBlue, ui.BgGreen)}}}), 192 Width: 14, Height: 3, 193 Want: bb(14). 194 Write(" x0 ", ui.FgBlue, ui.BgGreen, ui.Inverse). 195 Write(" "). 196 Write(" x1 ", ui.FgBlue, ui.BgGreen), 197 }, 198 { 199 Name: "long lines cropped, with full scrollbar", 200 Given: NewListBox(ListBoxSpec{ 201 Horizontal: true, 202 State: ListBoxState{Items: TestItems{NItems: 2}, Selected: 0}}), 203 Width: 4, Height: 3, 204 Want: bb(4). 205 Write("item", ui.Inverse). 206 Newline().Write("item"). 207 Newline().Write(" ", ui.FgMagenta, ui.Inverse), 208 }, 209 { 210 Name: "scrollbar when not showing all items", 211 Given: NewListBox(ListBoxSpec{ 212 Horizontal: true, 213 State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), 214 Width: 6, Height: 3, 215 Want: bb(6). 216 Write("item 0", ui.Inverse). 217 Newline().Write("item 1"). 218 Newline(). 219 Write(" ", ui.Inverse, ui.FgMagenta). 220 Write("━━━", ui.FgMagenta), 221 }, 222 { 223 Name: "scrollbar when not showing all items", 224 Given: NewListBox(ListBoxSpec{ 225 Horizontal: true, 226 State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), 227 Width: 10, Height: 3, 228 Want: bb(10). 229 Write("item 0", ui.Inverse).Write(" it"). 230 Newline().Write("item 1 it"). 231 Newline(). 232 Write(" ", ui.Inverse, ui.FgMagenta), 233 }, 234 } 235 236 func TestListBox_Render_Horizontal(t *testing.T) { 237 testRender(t, listBoxRenderHorizontalTests) 238 } 239 240 func TestListBox_Render_Horizontal_MutatesState(t *testing.T) { 241 // Calling Render alters the First field to reflect the first item rendered. 242 w := NewListBox(ListBoxSpec{ 243 Horizontal: true, 244 State: ListBoxState{ 245 Items: TestItems{Prefix: "x", NItems: 10}, Selected: 4, First: 0}}) 246 // Only a single column of 3 items shown: x3-x5 247 w.Render(2, 4) 248 state := w.CopyState() 249 if first := state.First; first != 3 { 250 t.Errorf("State.First = %d, want 3", first) 251 } 252 if height := state.Height; height != 3 { 253 t.Errorf("State.Height = %d, want 3", height) 254 } 255 } 256 257 var listBoxHandleTests = []handleTest{ 258 { 259 Name: "up moving selection up", 260 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 1}}), 261 Event: term.K(ui.Up), 262 263 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 264 }, 265 { 266 Name: "up stopping at 0", 267 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}}), 268 Event: term.K(ui.Up), 269 270 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 271 }, 272 { 273 Name: "up moving to last item when selecting after boundary", 274 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 11}}), 275 Event: term.K(ui.Up), 276 277 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}, 278 }, 279 { 280 Name: "down moving selection down", 281 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 1}}), 282 Event: term.K(ui.Down), 283 284 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 2}, 285 }, 286 { 287 Name: "down stopping at n-1", 288 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}}), 289 Event: term.K(ui.Down), 290 291 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}, 292 }, 293 { 294 Name: "down moving to first item when selecting before boundary", 295 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: -2}}), 296 Event: term.K(ui.Down), 297 298 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 299 }, 300 { 301 Name: "enter triggering default no-op accept", 302 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}), 303 Event: term.K(ui.Enter), 304 305 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}, 306 }, 307 { 308 Name: "other keys not handled", 309 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}), 310 Event: term.K('a'), 311 312 WantUnhandled: true, 313 }, 314 { 315 Name: "bindings", 316 Given: NewListBox(ListBoxSpec{ 317 State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}, 318 Bindings: MapBindings{ 319 term.K('a'): func(w Widget) { w.(*listBox).State.Selected = 0 }, 320 }, 321 }), 322 Event: term.K('a'), 323 324 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 325 }, 326 } 327 328 func TestListBox_Handle(t *testing.T) { 329 testHandle(t, listBoxHandleTests) 330 } 331 332 func TestListBox_Handle_EnterEmitsAccept(t *testing.T) { 333 var acceptedItems Items 334 var acceptedIndex int 335 w := NewListBox(ListBoxSpec{ 336 OnAccept: func(it Items, i int) { 337 acceptedItems = it 338 acceptedIndex = i 339 }, 340 State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}) 341 w.Handle(term.K(ui.Enter)) 342 343 if acceptedItems != (TestItems{NItems: 10}) { 344 t.Errorf("OnAccept not passed current Items") 345 } 346 if acceptedIndex != 5 { 347 t.Errorf("OnAccept not passed current selected index") 348 } 349 } 350 351 func TestListBox_Select_ChangeState(t *testing.T) { 352 // number of items = 10, height = 3 353 var tests = []struct { 354 name string 355 before int 356 f func(ListBoxState) int 357 after int 358 }{ 359 {"Next from -1", -1, Next, 0}, 360 {"Next from 0", 0, Next, 1}, 361 {"Next from 9", 9, Next, 9}, 362 {"Next from 10", 10, Next, 9}, 363 364 {"NextWrap from -1", -1, NextWrap, 0}, 365 {"NextWrap from 0", 0, NextWrap, 1}, 366 {"NextWrap from 9", 9, NextWrap, 0}, 367 {"NextWrap from 10", 10, NextWrap, 0}, 368 369 {"NextPage from -1", -1, NextPage, 2}, 370 {"NextPage from 0", 0, NextPage, 3}, 371 {"NextPage from 9", 9, NextPage, 9}, 372 {"NextPage from 10", 10, NextPage, 9}, 373 374 {"Prev from -1", -1, Prev, 0}, 375 {"Prev from 0", 0, Prev, 0}, 376 {"Prev from 9", 9, Prev, 8}, 377 {"Prev from 10", 10, Prev, 9}, 378 379 {"PrevWrap from -1", -1, PrevWrap, 9}, 380 {"PrevWrap from 0", 0, PrevWrap, 9}, 381 {"PrevWrap from 9", 9, PrevWrap, 8}, 382 {"PrevWrap from 10", 10, PrevWrap, 9}, 383 384 {"PrevPage from -1", -1, PrevPage, 0}, 385 {"PrevPage from 0", 0, PrevPage, 0}, 386 {"PrevPage from 9", 9, PrevPage, 6}, 387 {"PrevPage from 10", 10, PrevPage, 7}, 388 389 {"Left from -1", -1, Left, 0}, 390 {"Left from 0", 0, Left, 0}, 391 {"Left from 9", 9, Left, 6}, 392 {"Left from 10", 10, Left, 6}, 393 394 {"Right from -1", -1, Right, 3}, 395 {"Right from 0", 0, Right, 3}, 396 {"Right from 9", 9, Right, 9}, 397 {"Right from 10", 10, Right, 9}, 398 } 399 400 for _, test := range tests { 401 t.Run(test.name, func(t *testing.T) { 402 w := NewListBox(ListBoxSpec{ 403 State: ListBoxState{ 404 Items: TestItems{NItems: 10}, Height: 3, 405 Selected: test.before}}) 406 w.Select(test.f) 407 if selected := w.CopyState().Selected; selected != test.after { 408 t.Errorf("selected = %d, want %d", selected, test.after) 409 } 410 }) 411 } 412 } 413 414 func TestListBox_Select_CallOnSelect(t *testing.T) { 415 it := TestItems{NItems: 10} 416 gotItemsCh := make(chan Items, 10) 417 gotSelectedCh := make(chan int, 10) 418 w := NewListBox(ListBoxSpec{ 419 OnSelect: func(it Items, i int) { 420 gotItemsCh <- it 421 gotSelectedCh <- i 422 }, 423 State: ListBoxState{Items: it, Selected: 5}}) 424 425 verifyOnSelect := func(wantSelected int) { 426 if gotItems := <-gotItemsCh; gotItems != it { 427 t.Errorf("Got it = %v, want %v", gotItems, it) 428 } 429 if gotSelected := <-gotSelectedCh; gotSelected != wantSelected { 430 t.Errorf("Got selected = %v, want %v", gotSelected, wantSelected) 431 } 432 } 433 434 // Test that OnSelect is called during initialization. 435 verifyOnSelect(5) 436 // Test that OnSelect is called when changing selection. 437 w.Select(Next) 438 verifyOnSelect(6) 439 // Test that OnSelect is not called when index is invalid. Instead of 440 // waiting a fixed time to make sure that nothing is sent in the channel, we 441 // immediately does another Select with a valid index, and verify that only 442 // the valid index is sent. 443 w.Select(func(ListBoxState) int { return -1 }) 444 w.Select(func(ListBoxState) int { return 0 }) 445 verifyOnSelect(0) 446 } 447 448 func TestListBox_Accept_IndexCheck(t *testing.T) { 449 tests := []struct { 450 name string 451 nItems int 452 selected int 453 shouldAccept bool 454 }{ 455 {"index in range", 1, 0, true}, 456 {"index exceeds left boundary", 1, -1, false}, 457 {"index exceeds right boundary", 0, 0, false}, 458 } 459 for _, tt := range tests { 460 t.Run(tt.name, func(t *testing.T) { 461 w := NewListBox(ListBoxSpec{ 462 OnAccept: func(it Items, i int) { 463 if !tt.shouldAccept { 464 t.Error("should not accept this state") 465 } 466 }, 467 State: ListBoxState{ 468 Items: TestItems{NItems: tt.nItems}, 469 Selected: tt.selected, 470 }, 471 }) 472 w.Accept() 473 }) 474 } 475 }