src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/tk/listbox_test.go (about) 1 package tk 2 3 import ( 4 "testing" 5 6 "src.elv.sh/pkg/cli/term" 7 "src.elv.sh/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.ContentHeight; 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 Name: "not showing scrollbar with height = 1", 236 Given: NewListBox(ListBoxSpec{ 237 Horizontal: true, 238 State: ListBoxState{Items: TestItems{NItems: 4}, Selected: 0}}), 239 Width: 10, Height: 1, 240 Want: bb(10). 241 Write("item 0", ui.Inverse).Write(" it"), 242 }, 243 } 244 245 func TestListBox_Render_Horizontal(t *testing.T) { 246 testRender(t, listBoxRenderHorizontalTests) 247 } 248 249 func TestListBox_Render_Horizontal_MutatesState(t *testing.T) { 250 // Calling Render alters the First field to reflect the first item rendered. 251 w := NewListBox(ListBoxSpec{ 252 Horizontal: true, 253 State: ListBoxState{ 254 Items: TestItems{Prefix: "x", NItems: 10}, Selected: 4, First: 0}}) 255 // Only a single column of 3 items shown: x3-x5 256 w.Render(2, 4) 257 state := w.CopyState() 258 if first := state.First; first != 3 { 259 t.Errorf("State.First = %d, want 3", first) 260 } 261 if height := state.ContentHeight; height != 3 { 262 t.Errorf("State.Height = %d, want 3", height) 263 } 264 } 265 266 var listBoxHandleTests = []handleTest{ 267 { 268 Name: "up moving selection up", 269 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 1}}), 270 Event: term.K(ui.Up), 271 272 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 273 }, 274 { 275 Name: "up stopping at 0", 276 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}}), 277 Event: term.K(ui.Up), 278 279 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 280 }, 281 { 282 Name: "up moving to last item when selecting after boundary", 283 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 11}}), 284 Event: term.K(ui.Up), 285 286 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}, 287 }, 288 { 289 Name: "down moving selection down", 290 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 1}}), 291 Event: term.K(ui.Down), 292 293 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 2}, 294 }, 295 { 296 Name: "down stopping at n-1", 297 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}}), 298 Event: term.K(ui.Down), 299 300 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 9}, 301 }, 302 { 303 Name: "down moving to first item when selecting before boundary", 304 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: -2}}), 305 Event: term.K(ui.Down), 306 307 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 308 }, 309 { 310 Name: "enter triggering default no-op accept", 311 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}), 312 Event: term.K(ui.Enter), 313 314 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}, 315 }, 316 { 317 Name: "other keys not handled", 318 Given: NewListBox(ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}), 319 Event: term.K('a'), 320 321 WantUnhandled: true, 322 }, 323 { 324 Name: "bindings", 325 Given: NewListBox(ListBoxSpec{ 326 State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}, 327 Bindings: MapBindings{ 328 term.K('a'): func(w Widget) { w.(*listBox).State.Selected = 0 }, 329 }, 330 }), 331 Event: term.K('a'), 332 333 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 334 }, 335 } 336 337 func TestListBox_Handle(t *testing.T) { 338 testHandle(t, listBoxHandleTests) 339 } 340 341 func TestListBox_Handle_EnterEmitsAccept(t *testing.T) { 342 var acceptedItems Items 343 var acceptedIndex int 344 w := NewListBox(ListBoxSpec{ 345 OnAccept: func(it Items, i int) { 346 acceptedItems = it 347 acceptedIndex = i 348 }, 349 State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}) 350 w.Handle(term.K(ui.Enter)) 351 352 if acceptedItems != (TestItems{NItems: 10}) { 353 t.Errorf("OnAccept not passed current Items") 354 } 355 if acceptedIndex != 5 { 356 t.Errorf("OnAccept not passed current selected index") 357 } 358 } 359 360 func TestListBox_Select_ChangeState(t *testing.T) { 361 // number of items = 10, height = 3 362 var tests = []struct { 363 name string 364 before int 365 f func(ListBoxState) int 366 after int 367 }{ 368 {"Next from -1", -1, Next, 0}, 369 {"Next from 0", 0, Next, 1}, 370 {"Next from 9", 9, Next, 9}, 371 {"Next from 10", 10, Next, 9}, 372 373 {"NextWrap from -1", -1, NextWrap, 0}, 374 {"NextWrap from 0", 0, NextWrap, 1}, 375 {"NextWrap from 9", 9, NextWrap, 0}, 376 {"NextWrap from 10", 10, NextWrap, 0}, 377 378 {"NextPage from -1", -1, NextPage, 2}, 379 {"NextPage from 0", 0, NextPage, 3}, 380 {"NextPage from 9", 9, NextPage, 9}, 381 {"NextPage from 10", 10, NextPage, 9}, 382 383 {"Prev from -1", -1, Prev, 0}, 384 {"Prev from 0", 0, Prev, 0}, 385 {"Prev from 9", 9, Prev, 8}, 386 {"Prev from 10", 10, Prev, 9}, 387 388 {"PrevWrap from -1", -1, PrevWrap, 9}, 389 {"PrevWrap from 0", 0, PrevWrap, 9}, 390 {"PrevWrap from 9", 9, PrevWrap, 8}, 391 {"PrevWrap from 10", 10, PrevWrap, 9}, 392 393 {"PrevPage from -1", -1, PrevPage, 0}, 394 {"PrevPage from 0", 0, PrevPage, 0}, 395 {"PrevPage from 9", 9, PrevPage, 6}, 396 {"PrevPage from 10", 10, PrevPage, 7}, 397 398 {"Left from -1", -1, Left, 0}, 399 {"Left from 0", 0, Left, 0}, 400 {"Left from 9", 9, Left, 6}, 401 {"Left from 10", 10, Left, 6}, 402 403 {"Right from -1", -1, Right, 3}, 404 {"Right from 0", 0, Right, 3}, 405 {"Right from 9", 9, Right, 9}, 406 {"Right from 10", 10, Right, 9}, 407 } 408 409 for _, test := range tests { 410 t.Run(test.name, func(t *testing.T) { 411 w := NewListBox(ListBoxSpec{ 412 State: ListBoxState{ 413 Items: TestItems{NItems: 10}, ContentHeight: 3, 414 Selected: test.before}}) 415 w.Select(test.f) 416 if selected := w.CopyState().Selected; selected != test.after { 417 t.Errorf("selected = %d, want %d", selected, test.after) 418 } 419 }) 420 } 421 } 422 423 func TestListBox_Select_CallOnSelect(t *testing.T) { 424 it := TestItems{NItems: 10} 425 gotItemsCh := make(chan Items, 10) 426 gotSelectedCh := make(chan int, 10) 427 w := NewListBox(ListBoxSpec{ 428 OnSelect: func(it Items, i int) { 429 gotItemsCh <- it 430 gotSelectedCh <- i 431 }, 432 State: ListBoxState{Items: it, Selected: 5}}) 433 434 verifyOnSelect := func(wantSelected int) { 435 if gotItems := <-gotItemsCh; gotItems != it { 436 t.Errorf("Got it = %v, want %v", gotItems, it) 437 } 438 if gotSelected := <-gotSelectedCh; gotSelected != wantSelected { 439 t.Errorf("Got selected = %v, want %v", gotSelected, wantSelected) 440 } 441 } 442 443 // Test that OnSelect is called during initialization. 444 verifyOnSelect(5) 445 // Test that OnSelect is called when changing selection. 446 w.Select(Next) 447 verifyOnSelect(6) 448 // Test that OnSelect is not called when index is invalid. Instead of 449 // waiting a fixed time to make sure that nothing is sent in the channel, we 450 // immediately does another Select with a valid index, and verify that only 451 // the valid index is sent. 452 w.Select(func(ListBoxState) int { return -1 }) 453 w.Select(func(ListBoxState) int { return 0 }) 454 verifyOnSelect(0) 455 } 456 457 func TestListBox_Accept_IndexCheck(t *testing.T) { 458 tests := []struct { 459 name string 460 nItems int 461 selected int 462 shouldAccept bool 463 }{ 464 {"index in range", 1, 0, true}, 465 {"index exceeds left boundary", 1, -1, false}, 466 {"index exceeds right boundary", 0, 0, false}, 467 } 468 for _, tt := range tests { 469 t.Run(tt.name, func(t *testing.T) { 470 w := NewListBox(ListBoxSpec{ 471 OnAccept: func(it Items, i int) { 472 if !tt.shouldAccept { 473 t.Error("should not accept this state") 474 } 475 }, 476 State: ListBoxState{ 477 Items: TestItems{NItems: tt.nItems}, 478 Selected: tt.selected, 479 }, 480 }) 481 w.Accept() 482 }) 483 } 484 }