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  }