diff options
Diffstat (limited to 'pkg/integration/components/test_driver.go')
-rw-r--r-- | pkg/integration/components/test_driver.go | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go new file mode 100644 index 000000000..7b3fb0889 --- /dev/null +++ b/pkg/integration/components/test_driver.go @@ -0,0 +1,237 @@ +package components + +import ( + "fmt" + "strings" + "time" + + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/types" + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" + "github.com/samber/lo" +) + +type TestDriver struct { + gui integrationTypes.GuiDriver + keys config.KeybindingConfig + pushKeyDelay int + *assertionHelper +} + +func NewTestController(gui integrationTypes.GuiDriver, keys config.KeybindingConfig, pushKeyDelay int) *TestDriver { + return &TestDriver{ + gui: gui, + keys: keys, + pushKeyDelay: pushKeyDelay, + assertionHelper: &assertionHelper{gui: gui}, + } +} + +// key is something like 'w' or '<space>'. It's best not to pass a direct value, +// but instead to go through the default user config to get a more meaningful key name +func (self *TestDriver) press(keyStr string) { + self.Wait(self.pushKeyDelay) + + self.gui.PressKey(keyStr) +} + +func (self *TestDriver) typeContent(content string) { + for _, char := range content { + self.press(string(char)) + } +} + +func (self *TestDriver) ContinueMerge() { + self.Views().current().Press(self.keys.Universal.CreateRebaseOptionsMenu) + + self.ExpectMenu(). + Title(Equals("Rebase Options")). + Select(Contains("continue")). + Confirm() +} + +func (self *TestDriver) ContinueRebase() { + self.ContinueMerge() +} + +// for when you want to allow lazygit to process something before continuing +func (self *TestDriver) Wait(milliseconds int) { + time.Sleep(time.Duration(milliseconds) * time.Millisecond) +} + +func (self *TestDriver) LogUI(message string) { + self.gui.LogUI(message) +} + +func (self *TestDriver) Log(message string) { + self.gui.LogUI(message) +} + +// this will look for a list item in the current panel and if it finds it, it will +// enter the keypresses required to navigate to it. +// The test will fail if: +// - the user is not in a list item +// - no list item is found containing the given text +// - multiple list items are found containing the given text in the initial page of items +// +// NOTE: this currently assumes that ViewBufferLines returns all the lines that can be accessed. +// If this changes in future, we'll need to update this code to first attempt to find the item +// in the current page and failing that, jump to the top of the view and iterate through all of it, +// looking for the item. +func (self *TestDriver) navigateToListItem(matcher *matcher) { + self.inListContext() + + currentContext := self.gui.CurrentContext().(types.IListContext) + + view := currentContext.GetView() + + var matchIndex int + + self.assertWithRetries(func() (bool, string) { + matchIndex = -1 + var matches []string + lines := view.ViewBufferLines() + // first we look for a duplicate on the current screen. We won't bother looking beyond that though. + for i, line := range lines { + ok, _ := matcher.test(line) + if ok { + matches = append(matches, line) + matchIndex = i + } + } + if len(matches) > 1 { + return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Matching lines:\n%s", len(matches), matcher.name(), strings.Join(matches, "\n")) + } else if len(matches) == 0 { + return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n")) + } else { + return true, "" + } + }) + + selectedLineIdx := view.SelectedLineIdx() + if selectedLineIdx == matchIndex { + self.Views().current().SelectedLine(matcher) + return + } + if selectedLineIdx < matchIndex { + for i := selectedLineIdx; i < matchIndex; i++ { + self.Views().current().SelectNextItem() + } + self.Views().current().SelectedLine(matcher) + return + } else { + for i := selectedLineIdx; i > matchIndex; i-- { + self.Views().current().SelectPreviousItem() + } + self.Views().current().SelectedLine(matcher) + return + } +} + +func (self *TestDriver) inListContext() { + self.assertWithRetries(func() (bool, string) { + currentContext := self.gui.CurrentContext() + _, ok := currentContext.(types.IListContext) + return ok, fmt.Sprintf("Expected current context to be a list context, but got %s", currentContext.GetKey()) + }) +} + +func (self *TestDriver) ExpectConfirmation() *ConfirmationAsserter { + self.inConfirm() + + return &ConfirmationAsserter{t: self} +} + +func (self *TestDriver) inConfirm() { + self.assertWithRetries(func() (bool, string) { + currentView := self.gui.CurrentContext().GetView() + return currentView.Name() == "confirmation" && !currentView.Editable, "Expected confirmation popup to be focused" + }) +} + +func (self *TestDriver) ExpectPrompt() *PromptAsserter { + self.inPrompt() + + return &PromptAsserter{t: self} +} + +func (self *TestDriver) inPrompt() { + self.assertWithRetries(func() (bool, string) { + currentView := self.gui.CurrentContext().GetView() + return currentView.Name() == "confirmation" && currentView.Editable, "Expected prompt popup to be focused" + }) +} + +func (self *TestDriver) ExpectAlert() *AlertAsserter { + self.inAlert() + + return &AlertAsserter{t: self} +} + +func (self *TestDriver) inAlert() { + // basically the same thing as a confirmation popup with the current implementation + self.assertWithRetries(func() (bool, string) { + currentView := self.gui.CurrentContext().GetView() + return currentView.Name() == "confirmation" && !currentView.Editable, "Expected alert popup to be focused" + }) +} + +func (self *TestDriver) ExpectMenu() *MenuAsserter { + self.inMenu() + + return &MenuAsserter{t: self} +} + +func (self *TestDriver) inMenu() { + self.assertWithRetries(func() (bool, string) { + return self.gui.CurrentContext().GetView().Name() == "menu", "Expected popup menu to be focused" + }) +} + +func (self *TestDriver) ExpectCommitMessagePanel() *CommitMessagePanelAsserter { + self.inCommitMessagePanel() + + return &CommitMessagePanelAsserter{t: self} +} + +func (self *TestDriver) inCommitMessagePanel() { + self.assertWithRetries(func() (bool, string) { + currentView := self.gui.CurrentContext().GetView() + return currentView.Name() == "commitMessage", "Expected commit message panel to be focused" + }) +} + +func (self *TestDriver) currentWindowName(expectedWindowName string) { + self.assertWithRetries(func() (bool, string) { + actual := self.gui.CurrentContext().GetView().Name() + return actual == expectedWindowName, fmt.Sprintf("Expected current window name to be '%s', but got '%s'", expectedWindowName, actual) + }) +} + +// for making assertions on lazygit views +func (self *TestDriver) Views() *Views { + return &Views{t: self} +} + +// for making assertions on the lazygit model +func (self *TestDriver) Model() *Model { + return &Model{assertionHelper: self.assertionHelper, gui: self.gui} +} + +// for making assertions on the file system +func (self *TestDriver) FileSystem() *FileSystem { + return &FileSystem{assertionHelper: self.assertionHelper} +} + +// for when you just want to fail the test yourself. +// This runs callbacks to ensure we render the error after closing the gui. +func (self *TestDriver) Fail(message string) { + self.assertionHelper.fail(message) +} + +func (self *TestDriver) NotInPopup() { + self.assertWithRetries(func() (bool, string) { + viewName := self.gui.CurrentContext().GetView().Name() + return !lo.Contains([]string{"menu", "confirmation", "commitMessage"}, viewName), fmt.Sprintf("Unexpected popup view present: %s view", viewName) + }) +} |