github.com/matthewdale/lab@v0.14.0/cmd/ci_view_test.go (about)

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