diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index 673d9d726..096534e96 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -69,6 +69,10 @@ func (self *GuiDriver) MouseMove(x, y int) { self.replayMouseEvent(x, y, tcell.ButtonPrimary) } +func (self *GuiDriver) ScrollWheelDown(x, y int) { + self.replayMouseEvent(x, y, tcell.WheelDown) +} + func (self *GuiDriver) MouseRelease(x, y int) { self.replayMouseEvent(x, y, tcell.ButtonNone) } @@ -128,6 +132,16 @@ func (self *GuiDriver) FocusInAndClick(x, y int) { self.waitTillIdle() } +// RefreshInBackground performs the refresh that the background routines perform +// on a timer (see BackgroundRoutineMgr). Tests drive it directly rather than +// turning those routines on, so that they neither wait for a timer nor depend on +// one firing at a particular moment. +func (self *GuiDriver) RefreshInBackground() { + self.gui.c.RefreshFromWorker(types.RefreshOptions{Background: true}) + + self.waitTillIdle() +} + func (self *GuiDriver) PretendMergeOrRebaseStartedInLazygit() { self.gui.onUIThread(func() error { self.gui.State.SetMergeOrRebaseStartedInLazygit(true) diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go index d65caee5d..afd55a845 100644 --- a/pkg/integration/components/test_driver.go +++ b/pkg/integration/components/test_driver.go @@ -78,6 +78,12 @@ func (self *TestDriver) repeatMouseMove() { self.mouseMove(self.mouseX, self.mouseY) } +func (self *TestDriver) scrollWheelDown(x, y int) { + self.SetCaption(fmt.Sprintf("Scrolling down at %d, %d", x, y)) + self.gui.ScrollWheelDown(x, y) + self.Wait(self.inputDelay) +} + func (self *TestDriver) mouseRelease() { self.SetCaption(fmt.Sprintf("Releasing mouse at %d, %d", self.mouseX, self.mouseY)) self.gui.MouseRelease(self.mouseX, self.mouseY) @@ -136,6 +142,15 @@ func (self *TestDriver) Log(message string) { self.gui.LogUI(message) } +// RefreshInBackground performs the refresh that lazygit's background routines +// perform on a timer, e.g. to pick up changes made by RunCommand. Tests use this +// rather than turning those routines on and waiting for them. +func (self *TestDriver) RefreshInBackground() { + self.SetCaption("Refreshing in the background") + self.gui.RefreshInBackground() + self.Wait(self.inputDelay) +} + // allows the user to run shell commands during the test to emulate background activity func (self *TestDriver) Shell() *Shell { return self.shell diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index cf0338dec..bda63e3fb 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -56,6 +56,12 @@ func (self *fakeGuiDriver) MouseRelease(x, y int) { self.releasedCoordinates = append(self.releasedCoordinates, coordinate{x: x, y: y}) } +func (self *fakeGuiDriver) ScrollWheelDown(x, y int) { +} + +func (self *fakeGuiDriver) RefreshInBackground() { +} + func (self *fakeGuiDriver) OnUIThreadAndWait(f func()) { f() } diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go index bc0969f24..9b895e462 100644 --- a/pkg/integration/components/view_driver.go +++ b/pkg/integration/components/view_driver.go @@ -544,6 +544,15 @@ func (self *ViewDriver) MouseMoveToBottom(x int) *ViewDriver { return self.MouseMove(x, self.getView().InnerHeight()-1) } +// scrolls the view down by one notch of the mouse wheel, i.e. by +// gui.scrollHeight lines. This moves the scroll position without moving the +// selection. +func (self *ViewDriver) ScrollWheelDown() *ViewDriver { + offsetX, offsetY, _, _ := self.getView().Dimensions() + self.t.scrollWheelDown(offsetX+1, offsetY+1) + return self +} + func (self *ViewDriver) RepeatMouseMove() *ViewDriver { self.t.repeatMouseMove() return self diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 2d88de8e9..c9f4f975d 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -496,6 +496,7 @@ var tests = []*components.IntegrationTest{ tag.Reset, tag.ResetToDuplicateNamedBranch, ui.Accordion, + ui.BackgroundRefreshKeepsScrollPosition, ui.BranchesNotFirstTab, ui.CommitsNotFirstTab, ui.DisableSwitchTabWithPanelJumpKeys, diff --git a/pkg/integration/tests/ui/background_refresh_keeps_scroll_position.go b/pkg/integration/tests/ui/background_refresh_keeps_scroll_position.go new file mode 100644 index 000000000..e2cf6ee1e --- /dev/null +++ b/pkg/integration/tests/ui/background_refresh_keeps_scroll_position.go @@ -0,0 +1,41 @@ +package ui + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var BackgroundRefreshKeepsScrollPosition = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A background refresh doesn't scroll the selection back into view", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("initial commit") + for i := range 20 { + shell.CreateFile(fmt.Sprintf("file%02d", i), "") + } + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + Focus(). + SelectNextItem(). + SelectedLine(Contains("file00")). + // Scroll the selection out of view with the mouse wheel + ScrollWheelDown(). + ScrollWheelDown(). + OriginY(4). + Tap(func() { + t.Shell().CreateFile("aaa", "") + t.RefreshInBackground() + }). + // The new file sorts before the selected one, so the selection has + // moved down a line; the view must stay where the user left it though + SelectedLineIdx(2). + OriginY(4) + }, +}) diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index 4d2da7602..db9068ad9 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -31,6 +31,9 @@ type GuiDriver interface { ClickAndHold(int, int) MouseMove(int, int) MouseRelease(int, int) + ScrollWheelDown(int, int) + // Perform the refresh that a background routine would perform on a timer + RefreshInBackground() // Can be used to avoid data races with the UI thread in the uncommon cases that // the test driver needs to assert state while the gui is not idle. OnUIThreadAndWait(func())