github.com/zaquestion/lab@v0.25.1/cmd/ci_view_test.go (about)

     1  package cmd
     2  
     3  import (
     4  	"strings"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/gdamore/tcell/v2"
     9  	"github.com/rivo/tview"
    10  	"github.com/stretchr/testify/assert"
    11  	gitlab "github.com/xanzy/go-gitlab"
    12  )
    13  
    14  func assertScreen(t *testing.T, screen tcell.Screen, expected []string) {
    15  	sx, sy := screen.Size()
    16  	assert.Equal(t, len(expected), sy)
    17  	assert.Equal(t, len([]rune(expected[0])), sx)
    18  	actual := make([]string, sy)
    19  	for y, str := range expected {
    20  		runes := make([]rune, len(str))
    21  		row := []rune(str)
    22  		for x, expectedRune := range row {
    23  			r, _, _, _ := screen.GetContent(x, y)
    24  			runes[x] = r
    25  			_ = expectedRune
    26  			//assert.Equal(t, expectedRune, r, "%s != %s at (%d,%d)",
    27  			//	strconv.QuoteRune(expectedRune), strconv.QuoteRune(r), x, y)
    28  		}
    29  
    30  		actual[y] = strings.TrimRight(string(runes), string('\x00'))
    31  		assert.Equal(t, str, actual[y])
    32  	}
    33  	t.Logf("Expected w: %d l: %d", len([]rune(expected[0])), len(expected))
    34  	for _, str := range expected {
    35  		t.Log(str)
    36  	}
    37  	t.Logf("Actual w: %d l: %d", sx, sy)
    38  	for _, str := range actual {
    39  		t.Log(str)
    40  	}
    41  }
    42  
    43  func Test_line(t *testing.T) {
    44  	tests := []struct {
    45  		desc     string
    46  		lineF    func(screen tcell.Screen, x, y, l int)
    47  		x, y, l  int
    48  		expected []string
    49  	}{
    50  		{
    51  			"hline",
    52  			hline,
    53  			2, 2, 5,
    54  			[]string{
    55  				"          ",
    56  				"          ",
    57  				"  ━━━━━   ",
    58  				"          ",
    59  				"          ",
    60  				"          ",
    61  				"          ",
    62  				"          ",
    63  				"          ",
    64  				"          ",
    65  			},
    66  		},
    67  		{
    68  			"hline overflow",
    69  			hline,
    70  			2, 2, 10,
    71  			[]string{
    72  				"          ",
    73  				"          ",
    74  				"  ━━━━━━━━",
    75  				"          ",
    76  				"          ",
    77  				"          ",
    78  				"          ",
    79  				"          ",
    80  				"          ",
    81  				"          ",
    82  			},
    83  		},
    84  		{
    85  			"vline",
    86  			vline,
    87  			2, 2, 5,
    88  			[]string{
    89  				"          ",
    90  				"          ",
    91  				"  ┃       ",
    92  				"  ┃       ",
    93  				"  ┃       ",
    94  				"  ┃       ",
    95  				"  ┃       ",
    96  				"          ",
    97  				"          ",
    98  				"          ",
    99  			},
   100  		},
   101  		{
   102  			"vline overflow",
   103  			vline,
   104  			2, 2, 10,
   105  			[]string{
   106  				"          ",
   107  				"          ",
   108  				"  ┃       ",
   109  				"  ┃       ",
   110  				"  ┃       ",
   111  				"  ┃       ",
   112  				"  ┃       ",
   113  				"  ┃       ",
   114  				"  ┃       ",
   115  				"  ┃       ",
   116  			},
   117  		},
   118  	}
   119  
   120  	for _, test := range tests {
   121  		screen := tcell.NewSimulationScreen("UTF-8")
   122  		err := screen.Init()
   123  		if err != nil {
   124  			t.Fatal(err)
   125  		}
   126  		// Set screen to matrix size
   127  		screen.SetSize(len(test.expected), len(test.expected[0]))
   128  
   129  		test := test
   130  		t.Run(test.desc, func(t *testing.T) {
   131  			t.Parallel()
   132  			test.lineF(screen, test.x, test.y, test.l)
   133  			screen.Show()
   134  			assertScreen(t, screen, test.expected)
   135  		})
   136  	}
   137  }
   138  
   139  func testbox(x, y, w, h int) *tview.TextView {
   140  	b := tview.NewTextView()
   141  	b.SetBorder(true)
   142  	b.SetRect(x, y, w, h)
   143  	return b
   144  }
   145  
   146  func Test_connect(t *testing.T) {
   147  	tests := []struct {
   148  		desc        string
   149  		b1, b2      *tview.Box
   150  		first, last bool
   151  		expected    []string
   152  	}{
   153  		{
   154  			"first stage",
   155  			testbox(2, 1, 3, 3).Box, testbox(2, 5, 3, 3).Box,
   156  			true, false,
   157  			[]string{
   158  				"          ",
   159  				"  ┌─┐     ",
   160  				"  │ │     ",
   161  				"  └─┘ ┃   ",
   162  				"      ┃   ",
   163  				"  ┌─┐ ┃   ",
   164  				"  │ │━┛   ",
   165  				"  └─┘     ",
   166  				"          ",
   167  				"          ",
   168  			},
   169  		},
   170  		{
   171  			"last stage",
   172  			testbox(5, 1, 3, 3).Box, testbox(5, 5, 3, 3).Box,
   173  			false, true,
   174  			[]string{
   175  				"          ",
   176  				"     ┌─┐  ",
   177  				"   ┳ │ │  ",
   178  				"   ┃ └─┘  ",
   179  				"   ┃      ",
   180  				"   ┃ ┌─┐  ",
   181  				"   ┗━│ │  ",
   182  				"     └─┘  ",
   183  				"          ",
   184  				"          ",
   185  			},
   186  		},
   187  		{
   188  			"cross stage",
   189  			testbox(1, 1, 3, 3).Box, testbox(7, 1, 3, 3).Box,
   190  			false, false,
   191  			[]string{
   192  				"          ",
   193  				" ┌─┐   ┌─┐",
   194  				" │ │━━━│ │",
   195  				" └─┘   └─┘",
   196  				"          ",
   197  				"          ",
   198  				"          ",
   199  				"          ",
   200  				"          ",
   201  				"          ",
   202  			},
   203  		},
   204  	}
   205  
   206  	for _, test := range tests {
   207  		screen := tcell.NewSimulationScreen("UTF-8")
   208  		err := screen.Init()
   209  		if err != nil {
   210  			t.Fatal(err)
   211  		}
   212  		// Set screen to matrix size
   213  		screen.SetSize(len(test.expected), len(test.expected[0]))
   214  
   215  		test.b1.Draw(screen)
   216  		test.b2.Draw(screen)
   217  
   218  		test := test
   219  		t.Run(test.desc, func(t *testing.T) {
   220  			t.Parallel()
   221  			connect(screen, test.b1, test.b2, 2, test.first, test.last)
   222  			screen.Show()
   223  			assertScreen(t, screen, test.expected)
   224  		})
   225  	}
   226  }
   227  
   228  func Test_connectJobs(t *testing.T) {
   229  	expected := []string{
   230  		"                 ",
   231  		" ┌─┐   ┌─┐   ┌─┐ ",
   232  		" │ │┳━┳│ │┳━┳│ │ ",
   233  		" └─┘┃ ┃└─┘┃ ┃└─┘ ",
   234  		"    ┃ ┃   ┃ ┃    ",
   235  		" ┌─┐┃ ┃┌─┐┃ ┃┌─┐ ",
   236  		" │ │┫ ┣│ │┫ ┗│ │ ",
   237  		" └─┘┃ ┃└─┘┃  └─┘ ",
   238  		"    ┃ ┃   ┃      ",
   239  		" ┌─┐┃ ┃┌─┐┃      ",
   240  		" │ │┫ ┗│ │┛      ",
   241  		" └─┘┃  └─┘       ",
   242  		"    ┃            ",
   243  		" ┌─┐┃            ",
   244  		" │ │┛            ",
   245  		" └─┘             ",
   246  		"                 ",
   247  	}
   248  	jobs := []*gitlab.Job{
   249  		{
   250  			Name:  "stage1-job1",
   251  			Stage: "stage1",
   252  		},
   253  		{
   254  			Name:  "stage1-job2",
   255  			Stage: "stage1",
   256  		},
   257  		{
   258  			Name:  "stage1-job3",
   259  			Stage: "stage1",
   260  		},
   261  		{
   262  			Name:  "stage1-job4",
   263  			Stage: "stage1",
   264  		},
   265  		{
   266  			Name:  "stage2-job1",
   267  			Stage: "stage2",
   268  		},
   269  		{
   270  			Name:  "stage2-job2",
   271  			Stage: "stage2",
   272  		},
   273  		{
   274  			Name:  "stage2-job3",
   275  			Stage: "stage2",
   276  		},
   277  		{
   278  			Name:  "stage3-job1",
   279  			Stage: "stage3",
   280  		},
   281  		{
   282  			Name:  "stage3-job2",
   283  			Stage: "stage3",
   284  		},
   285  	}
   286  	boxes := map[string]*tview.TextView{
   287  		"jobs-stage1-job1": testbox(1, 1, 3, 3),
   288  		"jobs-stage1-job2": testbox(1, 5, 3, 3),
   289  		"jobs-stage1-job3": testbox(1, 9, 3, 3),
   290  		"jobs-stage1-job4": testbox(1, 13, 3, 3),
   291  
   292  		"jobs-stage2-job1": testbox(7, 1, 3, 3),
   293  		"jobs-stage2-job2": testbox(7, 5, 3, 3),
   294  		"jobs-stage2-job3": testbox(7, 9, 3, 3),
   295  
   296  		"jobs-stage3-job1": testbox(13, 1, 3, 3),
   297  		"jobs-stage3-job2": testbox(13, 5, 3, 3),
   298  	}
   299  
   300  	screen := tcell.NewSimulationScreen("UTF-8")
   301  	err := screen.Init()
   302  	if err != nil {
   303  		t.Fatal(err)
   304  	}
   305  	// Set screen to matrix size
   306  	screen.SetSize(len(expected), len(expected[0]))
   307  
   308  	for _, b := range boxes {
   309  		b.Draw(screen)
   310  	}
   311  
   312  	err = connectJobs(screen, jobs, boxes)
   313  	if err != nil {
   314  		t.Fatal(err)
   315  	}
   316  
   317  	screen.Show()
   318  	assertScreen(t, screen, expected)
   319  }
   320  
   321  func Test_connectJobsNegative(t *testing.T) {
   322  	tests := []struct {
   323  		desc  string
   324  		jobs  []*gitlab.Job
   325  		boxes map[string]*tview.TextView
   326  	}{
   327  		{
   328  			"determinePadding -- first job missing",
   329  			[]*gitlab.Job{
   330  				{
   331  					Name:  "stage1-job1",
   332  					Stage: "stage1",
   333  				},
   334  			},
   335  			map[string]*tview.TextView{
   336  				"jobs-stage2-job1": testbox(1, 5, 3, 3),
   337  				"jobs-stage2-job2": testbox(1, 9, 3, 3),
   338  			},
   339  		},
   340  		{
   341  			"determinePadding -- second job missing",
   342  			[]*gitlab.Job{
   343  				{
   344  					Name:  "stage1-job1",
   345  					Stage: "stage1",
   346  				},
   347  				{
   348  					Name:  "stage2-job1",
   349  					Stage: "stage2",
   350  				},
   351  				{
   352  					Name:  "stage2-job2",
   353  					Stage: "stage2",
   354  				},
   355  			},
   356  			map[string]*tview.TextView{
   357  				"jobs-stage1-job1": testbox(1, 1, 3, 3),
   358  				"jobs-stage2-job2": testbox(1, 9, 3, 3),
   359  			},
   360  		},
   361  		{
   362  			"connect -- third job missing",
   363  			[]*gitlab.Job{
   364  				{
   365  					Name:  "stage1-job1",
   366  					Stage: "stage1",
   367  				},
   368  				{
   369  					Name:  "stage2-job1",
   370  					Stage: "stage2",
   371  				},
   372  				{
   373  					Name:  "stage2-job2",
   374  					Stage: "stage2",
   375  				},
   376  			},
   377  			map[string]*tview.TextView{
   378  				"jobs-stage1-job1": testbox(1, 1, 3, 3),
   379  				"jobs-stage2-job1": testbox(1, 5, 3, 3),
   380  			},
   381  		},
   382  		{
   383  			"connect -- third job missing",
   384  			[]*gitlab.Job{
   385  				{
   386  					Name:  "stage1-job1",
   387  					Stage: "stage1",
   388  				},
   389  				{
   390  					Name:  "stage2-job1",
   391  					Stage: "stage2",
   392  				},
   393  				{
   394  					Name:  "stage2-job2",
   395  					Stage: "stage2",
   396  				},
   397  			},
   398  			map[string]*tview.TextView{
   399  				"jobs-stage1-job1": testbox(1, 1, 3, 3),
   400  				"jobs-stage2-job1": testbox(1, 5, 3, 3),
   401  			},
   402  		},
   403  	}
   404  	for _, test := range tests {
   405  		screen := tcell.NewSimulationScreen("UTF-8")
   406  		err := screen.Init()
   407  		if err != nil {
   408  			t.Fatal(err)
   409  		}
   410  		test := test
   411  		t.Run(test.desc, func(t *testing.T) {
   412  			t.Parallel()
   413  			assert.Error(t, connectJobs(screen, test.jobs, test.boxes))
   414  
   415  		})
   416  	}
   417  }
   418  
   419  func Test_jobsView(t *testing.T) {
   420  	expected := []string{
   421  		"  ┌────────────────────┐      ┌────────────────────┐      ┌────────────────────┐        ",
   422  		"  │       Stage1       │      │       Stage2       │      │       Stage3       │        ",
   423  		"  └────────────────────┘      └────────────────────┘      └────────────────────┘        ",
   424  		"                                                                                        ",
   425  		"  ╔✔ stage1-job1-reall…╗      ┌───● stage2-job1────┐      ┌───● stage3-job1────┐        ",
   426  		"  ║                    ║      │                    │      │                    │        ",
   427  		"  ║             01m 01s║━┳━━┳━│                    │━┳━━┳━│                    │        ",
   428  		"  ╚════════════════════╝ ┃  ┃ └────────────────────┘ ┃  ┃ └────────────────────┘        ",
   429  		"                         ┃  ┃                        ┃  ┃                               ",
   430  		"  ┌───✔ stage1-job2────┐ ┃  ┃ ┌───● stage2-job2────┐ ┃  ┃ ┌───● stage3-job2────┐        ",
   431  		"  │                    │ ┃  ┃ │                    │ ┃  ┃ │                    │        ",
   432  		"  │                    │━┫  ┣━│                    │━┫  ┗━│                    │        ",
   433  		"  └────────────────────┘ ┃  ┃ └────────────────────┘ ┃    └────────────────────┘        ",
   434  		"                         ┃  ┃                        ┃                                  ",
   435  		"  ┌───✔ stage1-job3────┐ ┃  ┃ ┌───● stage2-job3────┐ ┃                                  ",
   436  		"  │                    │ ┃  ┃ │                    │ ┃                                  ",
   437  		"  │                    │━┫  ┗━│                    │━┛                                  ",
   438  		"  └────────────────────┘ ┃    └────────────────────┘                                    ",
   439  		"                         ┃                                                              ",
   440  		"  ┌───✘ stage1-job4────┐ ┃                                                              ",
   441  		"  │                    │ ┃                                                              ",
   442  		"  │                    │━┛                                                              ",
   443  		"  └────────────────────┘                                                                ",
   444  		"                                                                                        ",
   445  		"                                                                                        ",
   446  		"                                                                                        ",
   447  	}
   448  	now := time.Now()
   449  	past := now.Add(time.Second * -61)
   450  	jobs := []*gitlab.Job{
   451  		{
   452  			Name:       "stage1-job1-really-long",
   453  			Stage:      "stage1",
   454  			Status:     "success",
   455  			StartedAt:  &past, // relies on test running in <1s we'll see how it goes
   456  			FinishedAt: &now,
   457  		},
   458  		{
   459  			Name:   "stage1-job2",
   460  			Stage:  "stage1",
   461  			Status: "success",
   462  		},
   463  		{
   464  			Name:   "stage1-job3",
   465  			Stage:  "stage1",
   466  			Status: "success",
   467  		},
   468  		{
   469  			Name:   "stage1-job4",
   470  			Stage:  "stage1",
   471  			Status: "failed",
   472  		},
   473  		{
   474  			Name:   "stage2-job1",
   475  			Stage:  "stage2",
   476  			Status: "running",
   477  		},
   478  		{
   479  			Name:   "stage2-job2",
   480  			Stage:  "stage2",
   481  			Status: "running",
   482  		},
   483  		{
   484  			Name:   "stage2-job3",
   485  			Stage:  "stage2",
   486  			Status: "pending",
   487  		},
   488  		{
   489  			Name:   "stage3-job1",
   490  			Stage:  "stage3",
   491  			Status: "manual",
   492  		},
   493  		{
   494  			Name:   "stage3-job2",
   495  			Stage:  "stage3",
   496  			Status: "manual",
   497  		},
   498  	}
   499  
   500  	boxes = make(map[string]*tview.TextView)
   501  	jobsCh := make(chan []*gitlab.Job)
   502  	inputCh := make(chan struct{})
   503  	root := tview.NewPages()
   504  	root.SetBorderPadding(1, 1, 2, 2)
   505  
   506  	screen := tcell.NewSimulationScreen("UTF-8")
   507  	err := screen.Init()
   508  	if err != nil {
   509  		t.Fatal(err)
   510  	}
   511  	// Set screen to matrix size
   512  	screen.SetSize(len([]rune(expected[0])), len(expected))
   513  	w, h := screen.Size()
   514  	root.SetRect(0, 0, w, h)
   515  
   516  	go func() {
   517  		jobsCh <- jobs
   518  	}()
   519  	root.Box.Focus(nil)
   520  	jobsView(nil, jobsCh, inputCh, root)
   521  	root.Focus(func(p tview.Primitive) { p.Focus(nil) })
   522  	root.Draw(screen)
   523  	connectJobsView(nil)(screen)
   524  	screen.Sync()
   525  	assertScreen(t, screen, expected)
   526  }
   527  
   528  func Test_latestJobs(t *testing.T) {
   529  	tests := []struct {
   530  		desc     string
   531  		jobs     []*gitlab.Job
   532  		expected []*gitlab.Job
   533  	}{
   534  		{
   535  			desc: "no newer jobs",
   536  			jobs: []*gitlab.Job{
   537  				{
   538  					ID:    1,
   539  					Name:  "stage1-job1",
   540  					Stage: "stage1",
   541  				},
   542  				{
   543  					ID:    2,
   544  					Name:  "stage1-job2",
   545  					Stage: "stage1",
   546  				},
   547  				{
   548  					ID:    3,
   549  					Name:  "stage1-job3",
   550  					Stage: "stage1",
   551  				},
   552  			},
   553  			expected: []*gitlab.Job{
   554  				{
   555  					ID:    1,
   556  					Name:  "stage1-job1",
   557  					Stage: "stage1",
   558  				},
   559  				{
   560  					ID:    2,
   561  					Name:  "stage1-job2",
   562  					Stage: "stage1",
   563  				},
   564  				{
   565  					ID:    3,
   566  					Name:  "stage1-job3",
   567  					Stage: "stage1",
   568  				},
   569  			},
   570  		},
   571  		{
   572  			desc: "1 newer",
   573  			jobs: []*gitlab.Job{
   574  				{
   575  					ID:    1,
   576  					Name:  "stage1-job1",
   577  					Stage: "stage1",
   578  				},
   579  				{
   580  					ID:    2,
   581  					Name:  "stage1-job2",
   582  					Stage: "stage1",
   583  				},
   584  				{
   585  					ID:    3,
   586  					Name:  "stage1-job3",
   587  					Stage: "stage1",
   588  				},
   589  				{
   590  					ID:    4,
   591  					Name:  "stage1-job1",
   592  					Stage: "stage1",
   593  				},
   594  			},
   595  			expected: []*gitlab.Job{
   596  				{
   597  					ID:    4,
   598  					Name:  "stage1-job1",
   599  					Stage: "stage1",
   600  				},
   601  				{
   602  					ID:    2,
   603  					Name:  "stage1-job2",
   604  					Stage: "stage1",
   605  				},
   606  				{
   607  					ID:    3,
   608  					Name:  "stage1-job3",
   609  					Stage: "stage1",
   610  				},
   611  			},
   612  		},
   613  		{
   614  			desc: "2 newer",
   615  			jobs: []*gitlab.Job{
   616  				{
   617  					ID:    1,
   618  					Name:  "stage1-job1",
   619  					Stage: "stage1",
   620  				},
   621  				{
   622  					ID:    2,
   623  					Name:  "stage1-job2",
   624  					Stage: "stage1",
   625  				},
   626  				{
   627  					ID:    3,
   628  					Name:  "stage1-job3",
   629  					Stage: "stage1",
   630  				},
   631  				{
   632  					ID:    4,
   633  					Name:  "stage1-job3",
   634  					Stage: "stage1",
   635  				},
   636  				{
   637  					ID:    5,
   638  					Name:  "stage1-job1",
   639  					Stage: "stage1",
   640  				},
   641  			},
   642  			expected: []*gitlab.Job{
   643  				{
   644  					ID:    5,
   645  					Name:  "stage1-job1",
   646  					Stage: "stage1",
   647  				},
   648  				{
   649  					ID:    2,
   650  					Name:  "stage1-job2",
   651  					Stage: "stage1",
   652  				},
   653  				{
   654  					ID:    4,
   655  					Name:  "stage1-job3",
   656  					Stage: "stage1",
   657  				},
   658  			},
   659  		},
   660  	}
   661  
   662  	for _, test := range tests {
   663  		test := test
   664  		t.Run(test.desc, func(t *testing.T) {
   665  			t.Parallel()
   666  			jobs := latestJobs(test.jobs)
   667  			assert.Equal(t, test.expected, jobs)
   668  		})
   669  	}
   670  }
   671  
   672  func Test_adjacentStages(t *testing.T) {
   673  	tests := []struct {
   674  		desc                       string
   675  		stage                      string
   676  		jobs                       []*gitlab.Job
   677  		expectedPrev, expectedNext string
   678  	}{
   679  		{
   680  			"no jobs",
   681  			"1",
   682  			[]*gitlab.Job{},
   683  			"", "",
   684  		},
   685  		{
   686  			"first stage",
   687  			"1",
   688  			[]*gitlab.Job{
   689  				{
   690  					Stage: "1",
   691  				},
   692  				{
   693  					Stage: "1",
   694  				},
   695  				{
   696  					Stage: "1",
   697  				},
   698  				{
   699  					Stage: "2",
   700  				},
   701  			},
   702  			"1", "2",
   703  		},
   704  		{
   705  			"mid stage",
   706  			"2",
   707  			[]*gitlab.Job{
   708  				{
   709  					Stage: "1",
   710  				},
   711  				{
   712  					Stage: "1",
   713  				},
   714  				{
   715  					Stage: "1",
   716  				},
   717  				{
   718  					Stage: "2",
   719  				},
   720  				{
   721  					Stage: "2",
   722  				},
   723  				{
   724  					Stage: "3",
   725  				},
   726  			},
   727  			"1", "3",
   728  		},
   729  		{
   730  			"last stage",
   731  			"3",
   732  			[]*gitlab.Job{
   733  				{
   734  					Stage: "1",
   735  				},
   736  				{
   737  					Stage: "1",
   738  				},
   739  				{
   740  					Stage: "1",
   741  				},
   742  				{
   743  					Stage: "2",
   744  				},
   745  				{
   746  					Stage: "2",
   747  				},
   748  				{
   749  					Stage: "3",
   750  				},
   751  			},
   752  			"2", "3",
   753  		},
   754  	}
   755  
   756  	for _, test := range tests {
   757  		test := test
   758  		t.Run(test.desc, func(t *testing.T) {
   759  			t.Parallel()
   760  			prev, next := adjacentStages(test.jobs, test.stage)
   761  			assert.Equal(t, test.expectedPrev, prev)
   762  			assert.Equal(t, test.expectedNext, next)
   763  		})
   764  	}
   765  }
   766  
   767  func Test_stageBounds(t *testing.T) {
   768  	tests := []struct {
   769  		desc                         string
   770  		stage                        string
   771  		jobs                         []*gitlab.Job
   772  		expectedLower, expectedUpper int
   773  	}{
   774  		{
   775  			"no jobs",
   776  			"1",
   777  			[]*gitlab.Job{},
   778  			0, 0,
   779  		},
   780  		{
   781  			"first stage",
   782  			"1",
   783  			[]*gitlab.Job{
   784  				{
   785  					Stage: "1",
   786  				},
   787  				{
   788  					Stage: "1",
   789  				},
   790  				{
   791  					Stage: "1",
   792  				},
   793  				{
   794  					Stage: "2",
   795  				},
   796  			},
   797  			0, 2,
   798  		},
   799  		{
   800  			"mid stage",
   801  			"2",
   802  			[]*gitlab.Job{
   803  				{
   804  					Stage: "1",
   805  				},
   806  				{
   807  					Stage: "1",
   808  				},
   809  				{
   810  					Stage: "1",
   811  				},
   812  				{
   813  					Stage: "2",
   814  				},
   815  				{
   816  					Stage: "2",
   817  				},
   818  				{
   819  					Stage: "3",
   820  				},
   821  			},
   822  			3, 4,
   823  		},
   824  		{
   825  			"last stage",
   826  			"3",
   827  			[]*gitlab.Job{
   828  				{
   829  					Stage: "1",
   830  				},
   831  				{
   832  					Stage: "1",
   833  				},
   834  				{
   835  					Stage: "1",
   836  				},
   837  				{
   838  					Stage: "2",
   839  				},
   840  				{
   841  					Stage: "2",
   842  				},
   843  				{
   844  					Stage: "3",
   845  				},
   846  			},
   847  			5, 5,
   848  		},
   849  	}
   850  
   851  	for _, test := range tests {
   852  		test := test
   853  		t.Run(test.desc, func(t *testing.T) {
   854  			t.Parallel()
   855  			lower, upper := stageBounds(test.jobs, test.stage)
   856  			assert.Equal(t, test.expectedLower, lower)
   857  			assert.Equal(t, test.expectedUpper, upper)
   858  		})
   859  	}
   860  }
   861  
   862  func Test_handleNavigation(t *testing.T) {
   863  	jobs := []*gitlab.Job{
   864  		{
   865  			Name:   "stage1-job1-really-long",
   866  			Stage:  "stage1",
   867  			Status: "success",
   868  		},
   869  		{
   870  			Name:   "stage1-job2",
   871  			Stage:  "stage1",
   872  			Status: "success",
   873  		},
   874  		{
   875  			Name:   "stage1-job3",
   876  			Stage:  "stage1",
   877  			Status: "success",
   878  		},
   879  		{
   880  			Name:   "stage1-job4",
   881  			Stage:  "stage1",
   882  			Status: "failed",
   883  		},
   884  		{
   885  			Name:   "stage2-job1",
   886  			Stage:  "stage2",
   887  			Status: "running",
   888  		},
   889  		{
   890  			Name:   "stage2-job2",
   891  			Stage:  "stage2",
   892  			Status: "running",
   893  		},
   894  		{
   895  			Name:   "stage2-job3",
   896  			Stage:  "stage2",
   897  			Status: "pending",
   898  		},
   899  		{
   900  			Name:   "stage3-job1",
   901  			Stage:  "stage3",
   902  			Status: "manual",
   903  		},
   904  		{
   905  			Name:   "stage3-job2",
   906  			Stage:  "stage3",
   907  			Status: "manual",
   908  		},
   909  	}
   910  	tests := []struct {
   911  		desc     string
   912  		input    []*tcell.EventKey
   913  		expected int
   914  	}{
   915  		{
   916  			"do nothing",
   917  			[]*tcell.EventKey{},
   918  			0,
   919  		},
   920  		{
   921  			"arrows down",
   922  			[]*tcell.EventKey{
   923  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   924  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   925  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   926  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   927  			},
   928  			3,
   929  		},
   930  		{
   931  			"arrows down bottom boundary",
   932  			[]*tcell.EventKey{
   933  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   934  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   935  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   936  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   937  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   938  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   939  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   940  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   941  			},
   942  			3,
   943  		},
   944  		{
   945  			"arrows down bottom middle boundary",
   946  			[]*tcell.EventKey{
   947  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   948  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   949  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   950  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   951  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   952  				tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone),
   953  			},
   954  			6,
   955  		},
   956  		{
   957  			"arrows down last (3rd) column bottom boundary",
   958  			[]*tcell.EventKey{
   959  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   960  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   961  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   962  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   963  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   964  				tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone),
   965  				tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone),
   966  			},
   967  			8,
   968  		},
   969  		{
   970  			"arrows down persistent depth bottom boundary",
   971  			[]*tcell.EventKey{
   972  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   973  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   974  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   975  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   976  				tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone),
   977  				tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone),
   978  				tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone),
   979  				tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone),
   980  				tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone),
   981  			},
   982  			3,
   983  		},
   984  		{
   985  			"arrows left boundary",
   986  			[]*tcell.EventKey{
   987  				tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone),
   988  				tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone),
   989  			},
   990  			0,
   991  		},
   992  		{
   993  			"arrows up boundary",
   994  			[]*tcell.EventKey{
   995  				tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone),
   996  				tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone),
   997  			},
   998  			0,
   999  		},
  1000  		{
  1001  			"arrows right boundary",
  1002  			[]*tcell.EventKey{
  1003  				tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone),
  1004  				tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone),
  1005  				tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone),
  1006  			},
  1007  			7,
  1008  		},
  1009  		{
  1010  			"hjkl down",
  1011  			[]*tcell.EventKey{
  1012  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1013  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1014  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1015  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1016  			},
  1017  			3,
  1018  		},
  1019  		{
  1020  			"hjkl down bottom boundary",
  1021  			[]*tcell.EventKey{
  1022  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1023  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1024  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1025  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1026  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1027  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1028  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1029  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1030  			},
  1031  			3,
  1032  		},
  1033  		{
  1034  			"hjkl down bottom middle boundary",
  1035  			[]*tcell.EventKey{
  1036  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1037  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1038  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1039  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1040  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1041  				tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone),
  1042  			},
  1043  			6,
  1044  		},
  1045  		{
  1046  			"hjkl down last (3rd) column bottom boundary",
  1047  			[]*tcell.EventKey{
  1048  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1049  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1050  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1051  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1052  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1053  				tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone),
  1054  				tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone),
  1055  			},
  1056  			8,
  1057  		},
  1058  		{
  1059  			"hjkl down persistent depth bottom boundary",
  1060  			[]*tcell.EventKey{
  1061  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1062  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1063  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1064  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1065  				tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone),
  1066  				tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone),
  1067  				tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone),
  1068  				tcell.NewEventKey(tcell.KeyRune, 'h', tcell.ModNone),
  1069  				tcell.NewEventKey(tcell.KeyRune, 'h', tcell.ModNone),
  1070  			},
  1071  			3,
  1072  		},
  1073  		{
  1074  			"hjkl left boundary",
  1075  			[]*tcell.EventKey{
  1076  				tcell.NewEventKey(tcell.KeyRune, 'h', tcell.ModNone),
  1077  				tcell.NewEventKey(tcell.KeyRune, 'h', tcell.ModNone),
  1078  			},
  1079  			0,
  1080  		},
  1081  		{
  1082  			"hjkl up boundary",
  1083  			[]*tcell.EventKey{
  1084  				tcell.NewEventKey(tcell.KeyRune, 'k', tcell.ModNone),
  1085  				tcell.NewEventKey(tcell.KeyRune, 'k', tcell.ModNone),
  1086  			},
  1087  			0,
  1088  		},
  1089  		{
  1090  			"hjkl right boundary",
  1091  			[]*tcell.EventKey{
  1092  				tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone),
  1093  				tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone),
  1094  				tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone),
  1095  			},
  1096  			7,
  1097  		},
  1098  		{
  1099  			"G boundary",
  1100  			[]*tcell.EventKey{
  1101  				tcell.NewEventKey(tcell.KeyRune, 'G', tcell.ModNone),
  1102  			},
  1103  			3,
  1104  		},
  1105  		{
  1106  			"Gg boundary",
  1107  			[]*tcell.EventKey{
  1108  				tcell.NewEventKey(tcell.KeyRune, 'G', tcell.ModNone),
  1109  				tcell.NewEventKey(tcell.KeyRune, 'g', tcell.ModNone),
  1110  			},
  1111  			0,
  1112  		},
  1113  	}
  1114  
  1115  	for _, test := range tests {
  1116  		test := test
  1117  		t.Run(test.desc, func(t *testing.T) {
  1118  			t.Parallel()
  1119  			var navi navigator
  1120  			for _, e := range test.input {
  1121  				navi.Navigate(jobs, e)
  1122  			}
  1123  			assert.Equal(t, test.expected, navi.idx)
  1124  		})
  1125  	}
  1126  }