From 68355862c1162f386aede13b676099c9b9b6bc73 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 25 Aug 2026 09:36:03 +0200 Subject: [PATCH] Add test helpers for a menu's filter row Its footer, the hint in the menu's subtitle, where the row sits in relation to the menu and the tooltip, and whether the text cursor is showing are all things the tests for it need to look at. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gui/gui_driver.go | 4 ++ pkg/integration/components/test_driver.go | 18 +++++++++ pkg/integration/components/test_test.go | 4 ++ pkg/integration/components/view_driver.go | 47 +++++++++++++++++++++++ pkg/integration/components/views.go | 8 ++++ pkg/integration/types/types.go | 2 + 6 files changed, 83 insertions(+) diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index b922f0705..07a0b7d4c 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -181,6 +181,10 @@ func (self *GuiDriver) CurrentContext() types.Context { return self.gui.State.ContextMgr.Current() } +func (self *GuiDriver) CursorVisible() bool { + return self.gui.g.Cursor +} + func (self *GuiDriver) ContextForView(viewName string) types.Context { context, ok := self.gui.helpers.View.ContextForView(viewName) if !ok { diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go index afd55a845..bd5bbfc24 100644 --- a/pkg/integration/components/test_driver.go +++ b/pkg/integration/components/test_driver.go @@ -97,6 +97,24 @@ func (self *TestDriver) GlobalPress(key config.Keybinding) { self.press(key[0]) } +// asserts that the terminal's text cursor is shown, i.e. that there is a text +// field to type into +func (self *TestDriver) CursorIsVisible() *TestDriver { + self.assertWithRetries(func() (bool, string) { + return self.gui.CursorVisible(), "Expected the cursor to be visible" + }) + + return self +} + +func (self *TestDriver) CursorIsHidden() *TestDriver { + self.assertWithRetries(func() (bool, string) { + return !self.gui.CursorVisible(), "Expected the cursor to be hidden" + }) + + return self +} + // FocusIn simulates the terminal window regaining focus, which causes lazygit // to reload any config files that changed while it was in the background. func (self *TestDriver) FocusIn() { diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index 2495a07c5..cce96105a 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -88,6 +88,10 @@ func (self *fakeGuiDriver) CurrentContext() types.Context { return nil } +func (self *fakeGuiDriver) CursorVisible() bool { + return false +} + func (self *fakeGuiDriver) ContextForView(viewName string) types.Context { return nil } diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go index b743afde7..102a8562a 100644 --- a/pkg/integration/components/view_driver.go +++ b/pkg/integration/components/view_driver.go @@ -41,6 +41,53 @@ func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver { return self } +// asserts that the view has the expected footer, i.e. the "x of y" text on its +// bottom border +func (self *ViewDriver) Footer(expected *TextMatcher) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + actual := self.getView().Footer + return expected.context(fmt.Sprintf("%s footer", self.context)).test(actual) + }) + + return self +} + +// asserts that the view has the expected subtitle +func (self *ViewDriver) Subtitle(expected *TextMatcher) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + actual := self.getView().Subtitle + return expected.context(fmt.Sprintf("%s subtitle", self.context)).test(actual) + }) + + return self +} + +// asserts that the view hangs off the bottom of the given one, sharing a border +// with it +func (self *ViewDriver) SharesTopBorderWithBottomOf(upper *ViewDriver) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + _, _, _, upperY1 := upper.getView().Dimensions() + _, y0, _, _ := self.getView().Dimensions() + return y0 == upperY1, fmt.Sprintf( + "%s: Expected view to start on row %d, where the view above it ends, but it starts on row %d", + self.context, upperY1, y0) + }) + + return self +} + +// asserts that the view starts on the row below the given one +func (self *ViewDriver) IsImmediatelyBelow(upper *ViewDriver) *ViewDriver { + self.t.assertWithRetries(func() (bool, string) { + _, _, _, upperY1 := upper.getView().Dimensions() + _, y0, _, _ := self.getView().Dimensions() + return y0 == upperY1+1, fmt.Sprintf( + "%s: Expected view to start on row %d, but it starts on row %d", self.context, upperY1+1, y0) + }) + + return self +} + func (self *ViewDriver) Clear() *ViewDriver { // clearing multiple times in case there's multiple lines // (the clear button only clears a single line at a time) diff --git a/pkg/integration/components/views.go b/pkg/integration/components/views.go index 90795d942..5c91b6937 100644 --- a/pkg/integration/components/views.go +++ b/pkg/integration/components/views.go @@ -124,6 +124,14 @@ func (self *Views) Menu() *ViewDriver { return self.regularView("menu") } +func (self *Views) MenuFilter() *ViewDriver { + return self.regularView("menuFilter") +} + +func (self *Views) MenuFilterFrame() *ViewDriver { + return self.regularView("menuFilterFrame") +} + func (self *Views) Confirmation() *ViewDriver { return self.regularView("confirmation") } diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index db9068ad9..325f4ea38 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -45,6 +45,8 @@ type GuiDriver interface { FocusInAndClick(int, int) Keys() config.KeybindingConfig CurrentContext() types.Context + // Whether the terminal's text cursor is currently shown + CursorVisible() bool ContextForView(viewName string) types.Context Fail(message string) // These two log methods are for the sake of debugging while testing. There's no need to actually