From ba024ed6c9e70e4bd49942052353479d47731ec6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 22 Jul 2026 09:12:49 +0200 Subject: [PATCH] Add ViewDriver methods for mouse drag gestures in integration tests Add press/move/release primitives next to the existing Click. The test driver remembers the last reported position so a release doesn't have to repeat the coordinates, and RepeatMouseMove lets a test verify that a held-button motion event within the same cell has no effect. --- pkg/gui/gui_driver.go | 16 ++++++++++++ pkg/integration/components/test_driver.go | 26 ++++++++++++++++++ pkg/integration/components/test_test.go | 28 +++++++++++++++++--- pkg/integration/components/view_driver.go | 32 +++++++++++++++++++++++ pkg/integration/types/types.go | 3 +++ 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index 2b035a855..66fb3c659 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -57,6 +57,22 @@ func (self *GuiDriver) Click(x, y int) { self.replayMouseEvent(x, y, tcell.ButtonNone) } +func (self *GuiDriver) ClickAndHold(x, y int) { + self.CheckAllToastsAcknowledged() + self.replayMouseEvent(x, y, tcell.ButtonPrimary) +} + +// MouseMove reports the mouse at a new position with the left button still +// held down, i.e. a drag movement. (No test needs pointer motion without a +// button held, so that variant doesn't exist.) +func (self *GuiDriver) MouseMove(x, y int) { + self.replayMouseEvent(x, y, tcell.ButtonPrimary) +} + +func (self *GuiDriver) MouseRelease(x, y int) { + self.replayMouseEvent(x, y, tcell.ButtonNone) +} + func (self *GuiDriver) replayMouseEvent(x, y int, buttons tcell.ButtonMask) { self.gui.g.ReplayMouseEvent(gocui.NewTcellMouseEventWrapper( tcell.NewEventMouse(x, y, buttons, 0), diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go index 19219707a..d65caee5d 100644 --- a/pkg/integration/components/test_driver.go +++ b/pkg/integration/components/test_driver.go @@ -13,6 +13,8 @@ type TestDriver struct { gui integrationTypes.GuiDriver keys config.KeybindingConfig inputDelay int + mouseX int + mouseY int *assertionHelper shell *Shell } @@ -58,6 +60,30 @@ func (self *TestDriver) click(x, y int) { self.Wait(self.inputDelay) } +func (self *TestDriver) clickAndHold(x, y int) { + self.SetCaption(fmt.Sprintf("Clicking and holding %d, %d", x, y)) + self.mouseX, self.mouseY = x, y + self.gui.ClickAndHold(x, y) + self.Wait(self.inputDelay) +} + +func (self *TestDriver) mouseMove(x, y int) { + self.SetCaption(fmt.Sprintf("Moving mouse to %d, %d", x, y)) + self.mouseX, self.mouseY = x, y + self.gui.MouseMove(x, y) + self.Wait(self.inputDelay) +} + +func (self *TestDriver) repeatMouseMove() { + self.mouseMove(self.mouseX, self.mouseY) +} + +func (self *TestDriver) mouseRelease() { + self.SetCaption(fmt.Sprintf("Releasing mouse at %d, %d", self.mouseX, self.mouseY)) + self.gui.MouseRelease(self.mouseX, self.mouseY) + self.Wait(self.inputDelay) +} + // Should only be used in specific cases where you're doing something weird! // E.g. invoking a global keybinding from within a popup. // You probably shouldn't use this function, and should instead go through a view like t.Views().Commit().Focus().Press(...) diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index 7196779eb..624c71c6a 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -19,9 +19,12 @@ type coordinate struct { } type fakeGuiDriver struct { - failureMessage string - pressedKeys []string - clickedCoordinates []coordinate + failureMessage string + pressedKeys []string + clickedCoordinates []coordinate + heldCoordinates []coordinate + movedCoordinates []coordinate + releasedCoordinates []coordinate } var _ integrationTypes.GuiDriver = &fakeGuiDriver{} @@ -38,6 +41,18 @@ func (self *fakeGuiDriver) Click(x, y int) { self.clickedCoordinates = append(self.clickedCoordinates, coordinate{x: x, y: y}) } +func (self *fakeGuiDriver) ClickAndHold(x, y int) { + self.heldCoordinates = append(self.heldCoordinates, coordinate{x: x, y: y}) +} + +func (self *fakeGuiDriver) MouseMove(x, y int) { + self.movedCoordinates = append(self.movedCoordinates, coordinate{x: x, y: y}) +} + +func (self *fakeGuiDriver) MouseRelease(x, y int) { + self.releasedCoordinates = append(self.releasedCoordinates, coordinate{x: x, y: y}) +} + func (self *fakeGuiDriver) FocusIn() { } @@ -123,12 +138,19 @@ func TestSuccess(t *testing.T) { t.press("b") t.click(0, 1) t.click(2, 3) + t.clickAndHold(0, 1) + t.mouseMove(2, 3) + t.repeatMouseMove() + t.mouseRelease() }, }) driver := &fakeGuiDriver{} test.Run(driver) assert.EqualValues(t, []string{"a", "b"}, driver.pressedKeys) assert.EqualValues(t, []coordinate{{0, 1}, {2, 3}}, driver.clickedCoordinates) + assert.EqualValues(t, []coordinate{{0, 1}}, driver.heldCoordinates) + assert.EqualValues(t, []coordinate{{2, 3}, {2, 3}}, driver.movedCoordinates) + assert.EqualValues(t, []coordinate{{2, 3}}, driver.releasedCoordinates) assert.Equal(t, "", driver.failureMessage) } diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go index 2cfaba338..656a93fe6 100644 --- a/pkg/integration/components/view_driver.go +++ b/pkg/integration/components/view_driver.go @@ -483,6 +483,38 @@ func (self *ViewDriver) FocusInAndClick(x, y int) *ViewDriver { return self } +func (self *ViewDriver) MouseMoveToView(target *ViewDriver, x, y int) *ViewDriver { + offsetX, offsetY, _, _ := target.getView().Dimensions() + self.t.mouseMove(offsetX+1+x, offsetY+1+y) + return self +} + +func (self *ViewDriver) Drag(fromX, fromY, toX, toY int) *ViewDriver { + return self.ClickAndHold(fromX, fromY).MouseMove(toX, toY).MouseRelease() +} + +func (self *ViewDriver) ClickAndHold(x, y int) *ViewDriver { + offsetX, offsetY, _, _ := self.getView().Dimensions() + self.t.clickAndHold(offsetX+1+x, offsetY+1+y) + return self +} + +func (self *ViewDriver) MouseMove(x, y int) *ViewDriver { + offsetX, offsetY, _, _ := self.getView().Dimensions() + self.t.mouseMove(offsetX+1+x, offsetY+1+y) + return self +} + +func (self *ViewDriver) RepeatMouseMove() *ViewDriver { + self.t.repeatMouseMove() + return self +} + +func (self *ViewDriver) MouseRelease() *ViewDriver { + self.t.mouseRelease() + return self +} + // i.e. pressing down arrow func (self *ViewDriver) SelectNextItem() *ViewDriver { return self.PressFast(self.t.keys.Universal.NextItem) diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index ea76c45be..bc92c6982 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -28,6 +28,9 @@ type GuiDriver interface { // user typing faster than lazygit processes the input. PressKeysRapidly(...string) Click(int, int) + ClickAndHold(int, int) + MouseMove(int, int) + MouseRelease(int, int) // Simulate the terminal window regaining focus (which triggers a reload of // changed config files) FocusIn()