github.com/elves/elvish@v0.15.0/pkg/cli/listbox_test.go (about) 1 package cli 2 3 import ( 4 "testing" 5 6 "github.com/elves/elvish/pkg/cli/term" 7 "github.com/elves/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: "overlay handler", 316 Given: listBoxWithOverlay( 317 ListBoxSpec{State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}, 318 func(w *listBox) Handler { 319 return MapHandler{ 320 term.K('a'): func() { w.State.Selected = 0 }, 321 } 322 }), 323 Event: term.K('a'), 324 325 WantNewState: ListBoxState{Items: TestItems{NItems: 10}, Selected: 0}, 326 }, 327 } 328 329 func listBoxWithOverlay(spec ListBoxSpec, overlay func(*listBox) Handler) *listBox { 330 w := NewListBox(spec) 331 ww := w.(*listBox) 332 ww.OverlayHandler = overlay(ww) 333 return ww 334 } 335 336 func TestListBox_Handle(t *testing.T) { 337 TestHandle(t, listBoxHandleTests) 338 } 339 340 func TestListBox_Handle_EnterEmitsAccept(t *testing.T) { 341 var acceptedItems Items 342 var acceptedIndex int 343 w := NewListBox(ListBoxSpec{ 344 OnAccept: func(it Items, i int) { 345 acceptedItems = it 346 acceptedIndex = i 347 }, 348 State: ListBoxState{Items: TestItems{NItems: 10}, Selected: 5}}) 349 w.Handle(term.K(ui.Enter)) 350 351 if acceptedItems != (TestItems{NItems: 10}) { 352 t.Errorf("OnAccept not passed current Items") 353 } 354 if acceptedIndex != 5 { 355 t.Errorf("OnAccept not passed current selected index") 356 } 357 } 358 359 func TestListBox_Select_ChangeState(t *testing.T) { 360 // number of items = 10, height = 3 361 var tests = []struct { 362 name string 363 before int 364 f func(ListBoxState) int 365 after int 366 }{ 367 {"Next from -1", -1, Next, 0}, 368 {"Next from 0", 0, Next, 1}, 369 {"Next from 9", 9, Next, 9}, 370 {"Next from 10", 10, Next, 9}, 371 372 {"NextWrap from -1", -1, NextWrap, 0}, 373 {"NextWrap from 0", 0, NextWrap, 1}, 374 {"NextWrap from 9", 9, NextWrap, 0}, 375 {"NextWrap from 10", 10, NextWrap, 0}, 376 377 {"NextPage from -1", -1, NextPage, 2}, 378 {"NextPage from 0", 0, NextPage, 3}, 379 {"NextPage from 9", 9, NextPage, 9}, 380 {"NextPage from 10", 10, NextPage, 9}, 381 382 {"Prev from -1", -1, Prev, 0}, 383 {"Prev from 0", 0, Prev, 0}, 384 {"Prev from 9", 9, Prev, 8}, 385 {"Prev from 10", 10, Prev, 9}, 386 387 {"PrevWrap from -1", -1, PrevWrap, 9}, 388 {"PrevWrap from 0", 0, PrevWrap, 9}, 389 {"PrevWrap from 9", 9, PrevWrap, 8}, 390 {"PrevWrap from 10", 10, PrevWrap, 9}, 391 392 {"PrevPage from -1", -1, PrevPage, 0}, 393 {"PrevPage from 0", 0, PrevPage, 0}, 394 {"PrevPage from 9", 9, PrevPage, 6}, 395 {"PrevPage from 10", 10, PrevPage, 7}, 396 397 {"Left from -1", -1, Left, 0}, 398 {"Left from 0", 0, Left, 0}, 399 {"Left from 9", 9, Left, 6}, 400 {"Left from 10", 10, Left, 6}, 401 402 {"Right from -1", -1, Right, 3}, 403 {"Right from 0", 0, Right, 3}, 404 {"Right from 9", 9, Right, 9}, 405 {"Right from 10", 10, Right, 9}, 406 } 407 408 for _, test := range tests { 409 t.Run(test.name, func(t *testing.T) { 410 w := NewListBox(ListBoxSpec{ 411 State: ListBoxState{ 412 Items: TestItems{NItems: 10}, Height: 3, 413 Selected: test.before}}) 414 w.Select(test.f) 415 if selected := w.CopyState().Selected; selected != test.after { 416 t.Errorf("selected = %d, want %d", selected, test.after) 417 } 418 }) 419 } 420 } 421 422 func TestListBox_Select_CallOnSelect(t *testing.T) { 423 it := TestItems{NItems: 10} 424 gotItemsCh := make(chan Items, 10) 425 gotSelectedCh := make(chan int, 10) 426 w := NewListBox(ListBoxSpec{ 427 OnSelect: func(it Items, i int) { 428 gotItemsCh <- it 429 gotSelectedCh <- i 430 }, 431 State: ListBoxState{Items: it, Selected: 5}}) 432 433 verifyOnSelect := func(wantSelected int) { 434 if gotItems := <-gotItemsCh; gotItems != it { 435 t.Errorf("Got it = %v, want %v", gotItems, it) 436 } 437 if gotSelected := <-gotSelectedCh; gotSelected != wantSelected { 438 t.Errorf("Got selected = %v, want %v", gotSelected, wantSelected) 439 } 440 } 441 442 // Test that OnSelect is called during initialization. 443 verifyOnSelect(5) 444 // Test that OnSelect is called when changing selection. 445 w.Select(Next) 446 verifyOnSelect(6) 447 // Test that OnSelect is not called when index is invalid. Instead of 448 // waiting a fixed time to make sure that nothing is sent in the channel, we 449 // immediately does another Select with a valid index, and verify that only 450 // the valid index is sent. 451 w.Select(func(ListBoxState) int { return -1 }) 452 w.Select(func(ListBoxState) int { return 0 }) 453 verifyOnSelect(0) 454 } 455 456 func TestListBox_Accept_IndexCheck(t *testing.T) { 457 tests := []struct { 458 name string 459 nItems int 460 selected int 461 shouldAccept bool 462 }{ 463 {"index in range", 1, 0, true}, 464 {"index exceeds left boundary", 1, -1, false}, 465 {"index exceeds right boundary", 0, 0, false}, 466 } 467 for _, tt := range tests { 468 t.Run(tt.name, func(t *testing.T) { 469 w := NewListBox(ListBoxSpec{ 470 OnAccept: func(it Items, i int) { 471 if !tt.shouldAccept { 472 t.Error("should not accept this state") 473 } 474 }, 475 State: ListBoxState{ 476 Items: TestItems{NItems: tt.nItems}, 477 Selected: tt.selected, 478 }, 479 }) 480 w.Accept() 481 }) 482 } 483 }