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  }