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  }