mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	Add integration test for accordion mode
This commit is contained in:
		
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -18,7 +18,7 @@ require ( | ||||
| 	github.com/integrii/flaggy v1.4.0 | ||||
| 	github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 | ||||
| 	github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d | ||||
| 	github.com/jesseduffield/gocui v0.3.1-0.20230719103719-ea5c8b64cf35 | ||||
| 	github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f | ||||
| 	github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 | ||||
| 	github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 | ||||
| 	github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e | ||||
|   | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T | ||||
| github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk= | ||||
| github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE= | ||||
| github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o= | ||||
| github.com/jesseduffield/gocui v0.3.1-0.20230719103719-ea5c8b64cf35 h1:ZWI/Tkg89iTpZDuxWYsR/f6xu+U827N8L1YnSvqug88= | ||||
| github.com/jesseduffield/gocui v0.3.1-0.20230719103719-ea5c8b64cf35/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s= | ||||
| github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f h1:w/pxI34XepTAx4HwxUu8ipimbVRgSTS+7ahmgFQwH80= | ||||
| github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s= | ||||
| github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0= | ||||
| github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= | ||||
| github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY= | ||||
|   | ||||
| @@ -522,9 +522,29 @@ var RuneReplacements = map[rune]string{ | ||||
| } | ||||
|  | ||||
| func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest) (*gocui.Gui, error) { | ||||
| 	playRecording := test != nil && os.Getenv(components.SANDBOX_ENV_VAR) != "true" | ||||
| 	runInSandbox := os.Getenv(components.SANDBOX_ENV_VAR) == "true" | ||||
| 	playRecording := test != nil && !runInSandbox | ||||
|  | ||||
| 	g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playRecording, headless, RuneReplacements) | ||||
| 	width, height := 0, 0 | ||||
| 	if test != nil { | ||||
| 		if test.RequiresHeadless() { | ||||
| 			if runInSandbox { | ||||
| 				panic("Test requires headless, can't run in sandbox") | ||||
| 			} | ||||
| 			headless = true | ||||
| 		} | ||||
| 		width, height = test.HeadlessDimensions() | ||||
| 	} | ||||
|  | ||||
| 	g, err := gocui.NewGui(gocui.NewGuiOpts{ | ||||
| 		OutputMode:       gocui.OutputTrue, | ||||
| 		SupportOverlaps:  OverlappingEdges, | ||||
| 		PlayRecording:    playRecording, | ||||
| 		Headless:         headless, | ||||
| 		RuneReplacements: RuneReplacements, | ||||
| 		Width:            width, | ||||
| 		Height:           height, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|   | ||||
| @@ -28,7 +28,10 @@ func RunTUI() { | ||||
| 	app := newApp(testDir) | ||||
| 	app.loadTests() | ||||
|  | ||||
| 	g, err := gocui.NewGui(gocui.OutputTrue, false, false, false, gui.RuneReplacements) | ||||
| 	g, err := gocui.NewGui(gocui.NewGuiOpts{ | ||||
| 		OutputMode:       gocui.OutputTrue, | ||||
| 		RuneReplacements: gui.RuneReplacements, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Panicln(err) | ||||
| 	} | ||||
|   | ||||
| @@ -19,6 +19,11 @@ import ( | ||||
| // to get the test's name via it's file's path. | ||||
| const unitTestDescription = "test test" | ||||
|  | ||||
| const ( | ||||
| 	defaultWidth  = 100 | ||||
| 	defaultHeight = 100 | ||||
| ) | ||||
|  | ||||
| type IntegrationTest struct { | ||||
| 	name         string | ||||
| 	description  string | ||||
| @@ -32,6 +37,8 @@ type IntegrationTest struct { | ||||
| 		keys config.KeybindingConfig, | ||||
| 	) | ||||
| 	gitVersion GitVersionRestriction | ||||
| 	width      int | ||||
| 	height     int | ||||
| } | ||||
|  | ||||
| var _ integrationTypes.IntegrationTest = &IntegrationTest{} | ||||
| @@ -52,6 +59,11 @@ type NewIntegrationTestArgs struct { | ||||
| 	Skip         bool | ||||
| 	// to run a test only on certain git versions | ||||
| 	GitVersion GitVersionRestriction | ||||
| 	// width and height when running in headless mode, for testing | ||||
| 	// the UI in different sizes. | ||||
| 	// If these are set, the test must be run in headless mode | ||||
| 	Width  int | ||||
| 	Height int | ||||
| } | ||||
|  | ||||
| type GitVersionRestriction struct { | ||||
| @@ -120,6 +132,8 @@ func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest { | ||||
| 		setupConfig:  args.SetupConfig, | ||||
| 		run:          args.Run, | ||||
| 		gitVersion:   args.GitVersion, | ||||
| 		width:        args.Width, | ||||
| 		height:       args.Height, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -172,6 +186,18 @@ func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (self *IntegrationTest) HeadlessDimensions() (int, int) { | ||||
| 	if self.width == 0 && self.height == 0 { | ||||
| 		return defaultWidth, defaultHeight | ||||
| 	} | ||||
|  | ||||
| 	return self.width, self.height | ||||
| } | ||||
|  | ||||
| func (self *IntegrationTest) RequiresHeadless() bool { | ||||
| 	return self.width != 0 && self.height != 0 | ||||
| } | ||||
|  | ||||
| func testNameFromCurrentFilePath() string { | ||||
| 	path := utils.FilePath(3) | ||||
| 	return TestNameFromFilePath(path) | ||||
|   | ||||
| @@ -82,6 +82,20 @@ func (self *ViewDriver) TopLines(matchers ...*TextMatcher) *ViewDriver { | ||||
| 	return self.assertLines(0, matchers...) | ||||
| } | ||||
|  | ||||
| // Asserts on the visible lines of the view. | ||||
| // Note, this assumes that the view's viewport is filled with lines | ||||
| func (self *ViewDriver) VisibleLines(matchers ...*TextMatcher) *ViewDriver { | ||||
| 	self.validateMatchersPassed(matchers) | ||||
| 	self.validateVisibleLineCount(matchers) | ||||
|  | ||||
| 	// Get the origin of the view and offset that. | ||||
| 	// Note that we don't do any retrying here so if we want to bring back retry logic | ||||
| 	// we'll need to update this. | ||||
| 	originY := self.getView().OriginY() | ||||
|  | ||||
| 	return self.assertLines(originY, matchers...) | ||||
| } | ||||
|  | ||||
| // asserts that somewhere in the view there are consequetive lines matching the given matchers. | ||||
| func (self *ViewDriver) ContainsLines(matchers ...*TextMatcher) *ViewDriver { | ||||
| 	self.validateMatchersPassed(matchers) | ||||
| @@ -212,6 +226,16 @@ func (self *ViewDriver) validateEnoughLines(matchers []*TextMatcher) { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // assumes the view's viewport is filled with lines | ||||
| func (self *ViewDriver) validateVisibleLineCount(matchers []*TextMatcher) { | ||||
| 	view := self.getView() | ||||
|  | ||||
| 	self.t.assertWithRetries(func() (bool, string) { | ||||
| 		count := view.InnerHeight() + 1 | ||||
| 		return count == len(matchers), fmt.Sprintf("unexpected number of visible lines in view '%s'. Expected exactly %d, got %d", view.Name(), len(matchers), count) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (self *ViewDriver) assertLines(offset int, matchers ...*TextMatcher) *ViewDriver { | ||||
| 	view := self.getView() | ||||
|  | ||||
|   | ||||
| @@ -209,6 +209,7 @@ var tests = []*components.IntegrationTest{ | ||||
| 	tag.CrudAnnotated, | ||||
| 	tag.CrudLightweight, | ||||
| 	tag.Reset, | ||||
| 	ui.Accordion, | ||||
| 	ui.DoublePopup, | ||||
| 	ui.SwitchTabFromMenu, | ||||
| 	undo.UndoCheckoutAndDrop, | ||||
|   | ||||
							
								
								
									
										61
									
								
								pkg/integration/tests/ui/accordion.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								pkg/integration/tests/ui/accordion.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| package ui | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/config" | ||||
| 	. "github.com/jesseduffield/lazygit/pkg/integration/components" | ||||
| ) | ||||
|  | ||||
| // When in acccordion mode, Lazygit looks like this: | ||||
| // | ||||
| // ╶─Status─────────────────────────╴┌─Patch──────────────────────────────────────────────────────────┐ | ||||
| // ╶─Files - Submodules──────0 of 0─╴│commit 6e56dd04b70e548976f7f2928c4d9c359574e2bc                 ▲ | ||||
| // ╶─Local branches - Remotes1 of 1─╴│Author: CI <CI@example.com>                                     █ | ||||
| // ┌─Commits - Reflog───────────────┐│Date:   Wed Jul 19 22:00:03 2023 +1000                          │ | ||||
| // │7fe02805 CI commit 12           ▲│                                                                ▼ | ||||
| // │6e56dd04 CI commit 11           █└────────────────────────────────────────────────────────────────┘ | ||||
| // │a35c687d CI commit 10           ▼┌─Command log────────────────────────────────────────────────────┐ | ||||
| // └───────────────────────10 of 20─┘│Random tip: To filter commits by path, press '<c-s>'            │ | ||||
| // ╶─Stash───────────────────0 of 0─╴└────────────────────────────────────────────────────────────────┘ | ||||
| //  <pgup>/<pgdown>: Scroll, <esc>: Cancel, q: Quit, ?: Keybindings, 1-Donate Ask Question unversioned | ||||
|  | ||||
| var Accordion = NewIntegrationTest(NewIntegrationTestArgs{ | ||||
| 	Description:  "Verify accordion mode kicks in when the screen height is too small", | ||||
| 	ExtraCmdArgs: []string{}, | ||||
| 	Width:        100, | ||||
| 	Height:       10, | ||||
| 	Skip:         false, | ||||
| 	SetupConfig:  func(config *config.AppConfig) {}, | ||||
| 	SetupRepo: func(shell *Shell) { | ||||
| 		shell.CreateNCommits(20) | ||||
| 	}, | ||||
| 	Run: func(t *TestDriver, keys config.KeybindingConfig) { | ||||
| 		t.Views().Commits(). | ||||
| 			Focus(). | ||||
| 			VisibleLines( | ||||
| 				Contains("commit 20").IsSelected(), | ||||
| 				Contains("commit 19"), | ||||
| 				Contains("commit 18"), | ||||
| 			). | ||||
| 			// go past commit 11, then come back, so that it ends up in the centre of the viewport | ||||
| 			NavigateToLine(Contains("commit 11")). | ||||
| 			NavigateToLine(Contains("commit 10")). | ||||
| 			NavigateToLine(Contains("commit 11")). | ||||
| 			VisibleLines( | ||||
| 				Contains("commit 12"), | ||||
| 				Contains("commit 11").IsSelected(), | ||||
| 				Contains("commit 10"), | ||||
| 			) | ||||
|  | ||||
| 		t.Views().Files(). | ||||
| 			Focus() | ||||
|  | ||||
| 		// ensure we retain the same viewport upon re-focus | ||||
| 		t.Views().Commits(). | ||||
| 			Focus(). | ||||
| 			VisibleLines( | ||||
| 				Contains("commit 12"), | ||||
| 				Contains("commit 11").IsSelected(), | ||||
| 				Contains("commit 10"), | ||||
| 			) | ||||
| 	}, | ||||
| }) | ||||
| @@ -13,6 +13,9 @@ import ( | ||||
| type IntegrationTest interface { | ||||
| 	Run(GuiDriver) | ||||
| 	SetupConfig(config *config.AppConfig) | ||||
| 	RequiresHeadless() bool | ||||
| 	// width and height when running headless | ||||
| 	HeadlessDimensions() (int, int) | ||||
| } | ||||
|  | ||||
| // this is the interface through which our integration tests interact with the lazygit gui | ||||
|   | ||||
							
								
								
									
										29
									
								
								vendor/github.com/jesseduffield/gocui/gui.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								vendor/github.com/jesseduffield/gocui/gui.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -177,13 +177,26 @@ type Gui struct { | ||||
| 	taskManager *TaskManager | ||||
| } | ||||
|  | ||||
| type NewGuiOpts struct { | ||||
| 	OutputMode      OutputMode | ||||
| 	SupportOverlaps bool | ||||
| 	PlayRecording   bool | ||||
| 	Headless        bool | ||||
| 	// only applicable when Headless is true | ||||
| 	Width int | ||||
| 	// only applicable when Headless is true | ||||
| 	Height int | ||||
|  | ||||
| 	RuneReplacements map[rune]string | ||||
| } | ||||
|  | ||||
| // NewGui returns a new Gui object with a given output mode. | ||||
| func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless bool, runeReplacements map[rune]string) (*Gui, error) { | ||||
| func NewGui(opts NewGuiOpts) (*Gui, error) { | ||||
| 	g := &Gui{} | ||||
|  | ||||
| 	var err error | ||||
| 	if headless { | ||||
| 		err = g.tcellInitSimulation() | ||||
| 	if opts.Headless { | ||||
| 		err = g.tcellInitSimulation(opts.Width, opts.Height) | ||||
| 	} else { | ||||
| 		err = g.tcellInit(runeReplacements) | ||||
| 	} | ||||
| @@ -191,7 +204,7 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if headless || runtime.GOOS == "windows" { | ||||
| 	if opts.Headless || runtime.GOOS == "windows" { | ||||
| 		g.maxX, g.maxY = g.screen.Size() | ||||
| 	} else { | ||||
| 		// TODO: find out if we actually need this bespoke logic for linux | ||||
| @@ -201,7 +214,7 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	g.outputMode = mode | ||||
| 	g.outputMode = opts.OutputMode | ||||
|  | ||||
| 	g.stop = make(chan struct{}) | ||||
|  | ||||
| @@ -209,7 +222,7 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless | ||||
| 	g.userEvents = make(chan userEvent, 20) | ||||
| 	g.taskManager = newTaskManager() | ||||
|  | ||||
| 	if playRecording { | ||||
| 	if opts.PlayRecording { | ||||
| 		g.ReplayedEvents = replayedEvents{ | ||||
| 			Keys:    make(chan *TcellKeyEventWrapper), | ||||
| 			Resizes: make(chan *TcellResizeEventWrapper), | ||||
| @@ -221,14 +234,14 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless | ||||
|  | ||||
| 	// SupportOverlaps is true when we allow for view edges to overlap with other | ||||
| 	// view edges | ||||
| 	g.SupportOverlaps = supportOverlaps | ||||
| 	g.SupportOverlaps = opts.SupportOverlaps | ||||
|  | ||||
| 	// default keys for when searching strings in a view | ||||
| 	g.SearchEscapeKey = KeyEsc | ||||
| 	g.NextSearchMatchKey = 'n' | ||||
| 	g.PrevSearchMatchKey = 'N' | ||||
|  | ||||
| 	g.playRecording = playRecording | ||||
| 	g.playRecording = opts.PlayRecording | ||||
|  | ||||
| 	return g, nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								vendor/github.com/jesseduffield/gocui/tcell_driver.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/github.com/jesseduffield/gocui/tcell_driver.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -81,7 +81,7 @@ func registerRuneFallbacks(s tcell.Screen, additional map[rune]string) { | ||||
| } | ||||
|  | ||||
| // tcellInitSimulation initializes tcell screen for use. | ||||
| func (g *Gui) tcellInitSimulation() error { | ||||
| func (g *Gui) tcellInitSimulation(width int, height int) error { | ||||
| 	s := tcell.NewSimulationScreen("") | ||||
| 	if e := s.Init(); e != nil { | ||||
| 		return e | ||||
| @@ -90,7 +90,7 @@ func (g *Gui) tcellInitSimulation() error { | ||||
| 		Screen = s | ||||
| 		// setting to a larger value than the typical terminal size | ||||
| 		// so that during a test we're more likely to see an item to select in a view. | ||||
| 		s.SetSize(100, 100) | ||||
| 		s.SetSize(width, height) | ||||
| 		s.Sync() | ||||
| 		return nil | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										2
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							| @@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem | ||||
| github.com/jesseduffield/go-git/v5/utils/merkletrie/index | ||||
| github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame | ||||
| github.com/jesseduffield/go-git/v5/utils/merkletrie/noder | ||||
| # github.com/jesseduffield/gocui v0.3.1-0.20230719103719-ea5c8b64cf35 | ||||
| # github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f | ||||
| ## explicit; go 1.12 | ||||
| github.com/jesseduffield/gocui | ||||
| # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user