diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2022-12-24 17:48:57 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2022-12-24 19:15:59 +1100 |
commit | b623ecf898e1ac35b0a6093978203a7b5c13c42e (patch) | |
tree | 670c2ecdc78e84fbb6dc9988195a6f3a991df217 /pkg | |
parent | aedfce28459bc59865b516dba2ee480a5d0972f1 (diff) |
add helper functions for popups in tests
Diffstat (limited to 'pkg')
40 files changed, 378 insertions, 400 deletions
diff --git a/pkg/integration/README.md b/pkg/integration/README.md index 3c75792ed..96b7f0089 100644 --- a/pkg/integration/README.md +++ b/pkg/integration/README.md @@ -41,10 +41,28 @@ The run step has four arguments passed in: Try to do as much setup work as possible in your setup step. For example, if all you're testing is that the user is able to resolve merge conflicts, create the merge conflicts in the setup step. On the other hand, if you're testing to see that lazygit can warn the user about merge conflicts after an attempted merge, it's fine to wait until the run step to actually create the conflicts. If the run step is focused on the thing you're trying to test, the test will run faster and its intent will be clearer. -Use assertions to ensure that lazygit has processed all your keybindings so far. For example, if you press 'n' on a branch to create a new branch, assert that the confirmation view is now focused. +Use assertions to ensure that lazygit has processed all your keybindings so far. Each time you press a key, something should happen on the screen, so you should assert that that thing has happened. This means we won't get into trouble from keys being entered two quickly because at each stage we ensure the key has been processed. This also makes tests more readable because they help explain what we expect to be happening on-screen. For example: + +```go +input.Press(keys.Files.CommitChanges) +assert.InCommitMessagePanel() +``` If you find yourself doing something frequently in a test, consider making it a method in one of the helper arguments. For example, instead of calling `input.PressKey(keys.Universal.Confirm)` in 100 places, it's better to have a method `input.Confirm()`. This is not to say that everything should be made into a method on the input struct: just things that are particularly common in tests. +Also, given how often we need to select a menu item or type into a prompt panel, there are some helper functions for that. For example: + +```go +// asserts that a prompt opens with the title 'Enter a file name', and then types 'my file' and confirms +input.Prompt(Equals("Enter a file name"), "my file") + +// asserts that a menu opens with the title: 'Choose file content', and then selects the option which contains 'bar' +input.Menu(Equals("Choose file content"), Contains("bar")) + +// asserts a confirmation appears with the title 'Are you sure?' and the content 'Are you REALLY sure' and then confirms +input.AcceptConfirmation(Equals("Are you sure?"), Equals("Are you REALLY sure?")) +``` + ## Running tests There are three ways to invoke a test: diff --git a/pkg/integration/components/assert.go b/pkg/integration/components/assert.go index ab7d079c8..95998b759 100644 --- a/pkg/integration/components/assert.go +++ b/pkg/integration/components/assert.go @@ -21,57 +21,44 @@ func NewAssert(gui integrationTypes.GuiDriver) *Assert { return &Assert{gui: gui} } -// for making assertions on string values -type matcher struct { - testFn func(string) (bool, string) - prefix string -} - -func (self *matcher) test(value string) (bool, string) { - ok, message := self.testFn(value) - if ok { - return true, "" - } - - if self.prefix != "" { - return false, self.prefix + " " + message - } - - return false, message -} - -func (self *matcher) context(prefix string) *matcher { - self.prefix = prefix - - return self -} - func Contains(target string) *matcher { - return &matcher{testFn: func(value string) (bool, string) { - return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value) - }} + return NewMatcher( + fmt.Sprintf("contains '%s'", target), + func(value string) (bool, string) { + return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value) + }, + ) } func NotContains(target string) *matcher { - return &matcher{testFn: func(value string) (bool, string) { - return !strings.Contains(value, target), fmt.Sprintf("Expected '%s' to NOT be found in '%s'", target, value) - }} + return NewMatcher( + fmt.Sprintf("does not contain '%s'", target), + func(value string) (bool, string) { + return !strings.Contains(value, target), fmt.Sprintf("Expected '%s' to NOT be found in '%s'", target, value) + }, + ) } -func MatchesRegexp(regexStr string) *matcher { - return &matcher{testFn: func(value string) (bool, string) { - matched, err := regexp.MatchString(regexStr, value) - if err != nil { - return false, fmt.Sprintf("Unexpected error parsing regular expression '%s': %s", regexStr, err.Error()) - } - return matched, fmt.Sprintf("Expected '%s' to match regular expression '%s'", value, regexStr) - }} +func MatchesRegexp(target string) *matcher { + return NewMatcher( + fmt.Sprintf("matches regular expression '%s'", target), + func(value string) (bool, string) { + matched, err := regexp.MatchString(target, value) + if err != nil { + return false, fmt.Sprintf("Unexpected error parsing regular expression '%s': %s", target, err.Error()) + } + return matched, fmt.Sprintf("Expected '%s' to match regular expression '%s'", value, target) + }, + ) } func Equals(target string) *matcher { - return &matcher{testFn: func(value string) (bool, string) { - return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target) - }} + return NewMatcher( + fmt.Sprintf("equals '%s'", target), + func(value string) (bool, string) { + return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target) + }, + ) } func (self *Assert) WorkingTreeFileCount(expectedCount int) { @@ -186,6 +173,13 @@ func (self *Assert) InAlert() { }) } +func (self *Assert) InCommitMessagePanel() { + self.assertWithRetries(func() (bool, string) { + currentView := self.gui.CurrentContext().GetView() + return currentView.Name() == "commitMessage", "Expected commit message panel to be focused" + }) +} + func (self *Assert) InMenu() { self.assertWithRetries(func() (bool, string) { return self.gui.CurrentContext().GetView().Name() == "menu", "Expected popup menu to be focused" diff --git a/pkg/integration/components/input.go b/pkg/integration/components/input.go index adc4df261..b6f2022c6 100644 --- a/pkg/integration/components/input.go +++ b/pkg/integration/components/input.go @@ -28,87 +28,81 @@ func NewInput(gui integrationTypes.GuiDriver, keys config.KeybindingConfig, asse // 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 *Input) PressKeys(keyStrs ...string) { +func (self *Input) Press(keyStrs ...string) { for _, keyStr := range keyStrs { - self.pressKey(keyStr) + self.press(keyStr) } } -func (self *Input) pressKey(keyStr string) { +func (self *Input) press(keyStr string) { self.Wait(self.pushKeyDelay) self.gui.PressKey(keyStr) } func (self *Input) SwitchToStatusWindow() { - self.pressKey(self.keys.Universal.JumpToBlock[0]) + self.press(self.keys.Universal.JumpToBlock[0]) self.assert.CurrentWindowName("status") } func (self *Input) SwitchToFilesWindow() { - self.pressKey(self.keys.Universal.JumpToBlock[1]) + self.press(self.keys.Universal.JumpToBlock[1]) self.assert.CurrentWindowName("files") } func (self *Input) SwitchToBranchesWindow() { - self.pressKey(self.keys.Universal.JumpToBlock[2]) + self.press(self.keys.Universal.JumpToBlock[2]) self.assert.CurrentWindowName("localBranches") } func (self *Input) SwitchToCommitsWindow() { - self.pressKey(self.keys.Universal.JumpToBlock[3]) + self.press(self.keys.Universal.JumpToBlock[3]) self.assert.CurrentWindowName("commits") } func (self *Input) SwitchToStashWindow() { - self.pressKey(self.keys.Universal.JumpToBlock[4]) + self.press(self.keys.Universal.JumpToBlock[4]) self.assert.CurrentWindowName("stash") } func (self *Input) Type(content string) { for _, char := range content { - self.pressKey(string(char)) + self.press(string(char)) } } // i.e. pressing enter func (self *Input) Confirm() { - self.pressKey(self.keys.Universal.Confirm) -} - -func (self *Input) ProceedWhenAsked(matcher *matcher) { - self.assert.InConfirm() - self.assert.CurrentViewContent(matcher) - self.Confirm() + self.press(self.keys.Universal.Confirm) } // i.e. same as Confirm func (self *Input) Enter() { - self.pressKey(self.keys.Universal.Confirm) + self.press(self.keys.Universal.Confirm) } // i.e. pressing escape func (self *Input) Cancel() { - self.pressKey(self.keys.Universal.Return) + self.press(self.keys.Universal.Return) } // i.e. pressing space func (self *Input) PrimaryAction() { - self.pressKey(self.keys.Universal.Select) + self.press(self.keys.Universal.Select) } // i.e. pressing down arrow func (self *Input) NextItem() { - self.pressKey(self.keys.Universal.NextItem) + self.press(self.keys.Universal.NextItem) } // i.e. pressing up arrow func (self *Input) PreviousItem() { - self.pressKey(self.keys.Universal.PrevItem) + self.press(self.keys.Universal.PrevItem) } func (self *Input) ContinueMerge() { - self.PressKeys(self.keys.Universal.CreateRebaseOptionsMenu) + self.Press(self.keys.Universal.CreateRebaseOptionsMenu) self.assert.SelectedLine(Contains("continue")) self.Confirm() } @@ -141,7 +135,7 @@ func (self *Input) Log(message string) { // 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 *Input) NavigateToListItemContainingText(text string) { +func (self *Input) NavigateToListItem(matcher *matcher) { self.assert.InListContext() currentContext := self.gui.CurrentContext().(types.IListContext) @@ -151,19 +145,20 @@ func (self *Input) NavigateToListItemContainingText(text string) { var matchIndex int self.assert.assertWithRetries(func() (bool, string) { - matchCount := 0 matchIndex = -1 + var matches []string // first we look for a duplicate on the current screen. We won't bother looking beyond that though. for i, line := range view.ViewBufferLines() { - if strings.Contains(line, text) { - matchCount++ + ok, _ := matcher.test(line) + if ok { + matches = append(matches, line) matchIndex = i } } - if matchCount > 1 { - return false, fmt.Sprintf("Found %d matches for %s, expected only a single match", matchCount, text) - } else if matchCount == 0 { - return false, fmt.Sprintf("Could not find item containing text: %s", text) + if len(matches) > 1 { + return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. 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", matcher.name) } else { return true, "" } @@ -171,20 +166,67 @@ func (self *Input) NavigateToListItemContainingText(text string) { selectedLineIdx := view.SelectedLineIdx() if selectedLineIdx == matchIndex { - self.assert.SelectedLine(Contains(text)) + self.assert.SelectedLine(matcher) return } if selectedLineIdx < matchIndex { for i := selectedLineIdx; i < matchIndex; i++ { self.NextItem() } - self.assert.SelectedLine(Contains(text)) + self.assert.SelectedLine(matcher) return } else { for i := selectedLineIdx; i > matchIndex; i-- { self.PreviousItem() } - self.assert.SelectedLine(Contains(text)) + self.assert.SelectedLine(matcher) return } } + +func (self *Input) AcceptConfirmation(title *matcher, content *matcher) { + self.assert.InConfirm() + self.assert.CurrentViewTitle(title) + self.assert.CurrentViewContent(content) + self.Confirm() +} + +func (self *Input) DenyConfirmation(title *matcher, content *matcher) { + self.assert.InConfirm() + self.assert.CurrentViewTitle(title) + self.assert.CurrentViewContent(content) + self.Cancel() +} + +func (self *Input) Prompt(title *matcher, textToType string) { + self.assert.InPrompt() + self.assert.CurrentViewTitle(title) + self.Type(textToType) + self.Confirm() +} + +// type some text into a prompt, then switch to the suggestions panel and expect the first +// item to match the given matcher, then confirm that item. +func (self *Input) Typeahead(title *matcher, textToType string, expectedFirstOption *matcher) { + self.assert.InPrompt() + self.assert.CurrentViewTitle(title) + self.Type(textToType) + self.Press(self.keys.Universal.TogglePanel) + self.assert.CurrentViewName("suggestions") + self.assert.SelectedLine(expectedFirstOption) + self.Confirm() +} + +func (self *Input) Menu(title *matcher, optionToSelect *matcher) { + self.assert.InMenu() + self.assert.CurrentViewTitle(title) + self.NavigateToListItem(optionToSelect) + self.Confirm() +} + +func (self *Input) Alert(title *matcher, content *matcher) { + self.assert.InListContext() + self.assert.CurrentViewTitle(title) + self.assert.CurrentViewContent(content) + self.Confirm() +} diff --git a/pkg/integration/components/matcher.go b/pkg/integration/components/matcher.go new file mode 100644 index 000000000..02b10b714 --- /dev/null +++ b/pkg/integration/components/matcher.go @@ -0,0 +1,35 @@ +package components + +// for making assertions on string values +type matcher struct { + // e.g. "contains 'foo'" + name string + // returns a bool that says whether the test passed and if it returns false, it + // also returns a string of the error message + testFn func(string) (bool, string) + // this is printed when there's an error so that it's clear what the context of the assertion is + prefix string +} + +func NewMatcher(name string, testFn func(string) (bool, string)) *matcher { + return &matcher{name: name, testFn: testFn} +} + +func (self *matcher) test(value string) (bool, string) { + ok, message := self.testFn(value) + if ok { + return true, "" + } + + if self.prefix != "" { + return false, self.prefix + " " + message + } + + return false, message +} + +func (self *matcher) context(prefix string) *matcher { + self.prefix = prefix + + return self +} diff --git a/pkg/integration/components/runner.go b/pkg/integration/components/runner.go index 47677b3b9..85ea3f330 100644 --- a/pkg/integration/components/runner.go +++ b/pkg/integration/components/runner.go @@ -115,6 +115,10 @@ func prepareTestDir( } func buildLazygit() error { + // // TODO: remove this line! + // // skipping this because I'm not making changes to the app code atm. + // return nil + osCommand := oscommands.NewDummyOSCommand() return osCommand.Cmd.New(fmt.Sprintf( "go build -o %s pkg/integration/clients/injector/main.go", tempLazygitPath(), diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index 61466e2b4..dbab22883 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -64,8 +64,8 @@ func TestAssertionFailure(t *testing.T) { test := NewIntegrationTest(NewIntegrationTestArgs{ Description: unitTestDescription, Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.PressKeys("a") - input.PressKeys("b") + input.Press("a") + input.Press("b") assert.CommitCount(2) }, }) @@ -91,8 +91,8 @@ func TestSuccess(t *testing.T) { test := NewIntegrationTest(NewIntegrationTestArgs{ Description: unitTestDescription, Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) { - input.PressKeys("a") - input.PressKeys("b") + input.Press("a") + input.Press("b") assert.CommitCount(0) }, }) diff --git a/pkg/integration/tests/bisect/basic.go b/pkg/integration/tests/bisect/basic.go index 0b81bbcb3..f40b4fe39 100644 --- a/pkg/integration/tests/bisect/basic.go +++ b/pkg/integration/tests/bisect/basic.go @@ -20,24 +20,14 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ assert *Assert, keys config.KeybindingConfig, ) { - viewBisectOptions := func() { - input.PressKeys(keys.Commits.ViewBisectOptions) - assert.InMenu() - } markCommitAsBad := func() { - viewBisectOptions() - assert.SelectedLine(Contains("bad")) - - input.Confirm() + input.Press(keys.Commits.ViewBisectOptions) + input.Menu(Equals("Bisect"), MatchesRegexp(`mark .* as bad`)) } markCommitAsGood := func() { - viewBisectOptions() - assert.SelectedLine(Contains("bad")) - input.NextItem() - assert.SelectedLine(Contains("good")) - - input.Confirm() + input.Press(keys.Commits.ViewBisectOptions) + input.Menu(Equals("Bisect"), MatchesRegexp(`mark .* as good`)) } assert.AtLeastOneCommit() @@ -46,7 +36,7 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ assert.SelectedLine(Contains("commit 10")) - input.NavigateToListItemContainingText("commit 09") + input.NavigateToListItem(Contains("commit 09")) markCommitAsBad() @@ -55,11 +45,11 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ assert.CurrentViewName("commits") assert.SelectedLine(Contains("<-- bad")) - input.NavigateToListItemContainingText("commit 02") + input.NavigateToListItem(Contains("commit 02")) markCommitAsGood() - // lazygit will land us in the comit between our good and bad commits. + // lazygit will land us in the commit between our good and bad commits. assert.CurrentViewName("commits") assert.SelectedLine(Contains("commit 05")) assert.SelectedLine(Contains("<-- current")) @@ -72,12 +62,8 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{ markCommitAsGood() - assert.InAlert() - assert.CurrentViewContent(Contains("Bisect complete!")) // commit 5 is the culprit because we marked 4 as good and 5 as bad. - assert.CurrentViewContent(Contains("commit 05")) - assert.CurrentViewContent(Contains("Do you want to reset")) - input.Confirm() + input.Alert(Equals("Bisect complete"), MatchesRegexp("(?s)commit 05.*Do you want to reset")) assert.CurrentViewName("commits") assert.CurrentViewContent(Contains("commit 04")) diff --git a/pkg/integration/tests/bisect/from_other_branch.go b/pkg/integration/tests/bisect/from_other_branch.go index 52314518f..85829b741 100644 --- a/pkg/integration/tests/bisect/from_other_branch.go +++ b/pkg/integration/tests/bisect/from_other_branch.go @@ -24,20 +24,6 @@ var FromOtherBranch = NewIntegrationTest(NewIntegrationTestArgs{ assert *Assert, keys config.KeybindingConfig, ) { - viewBisectOptions := func() { - input.PressKeys(keys.Commits.ViewBisectOptions) - assert.InMenu() - } - - markCommitAsGood := func() { - viewBisectOptions() - assert.SelectedLine(Contains("bad")) - input.NextItem() - assert.SelectedLine(Contains("good")) - - input.Confirm() - } - assert.ViewContent("information", Contains("bisecting")) assert.AtLeastOneCommit() @@ -51,13 +37,10 @@ var FromOtherBranch = NewIntegrationTest(NewIntegrationTestArgs{ assert.SelectedLine(Contains("<-- current")) assert.SelectedLine(Contains("commit 07")) - markCommitAsGood() + input.Press(keys.Commits.ViewBisectOptions) + input.Menu(Equals("Bisect"), MatchesRegexp(`mark .* as good`)) - assert.InAlert() - assert.CurrentViewContent(Contains("Bisect complete!")) - assert.CurrentViewContent(Contains("commit 08")) - assert.CurrentViewContent(Contains("Do you want to reset")) - input.Confirm() + input.Alert(Equals("Bisect complete"), MatchesRegexp(`(?s)commit 08.*Do you want to reset`)) assert.ViewContent("information", NotContains("bisecting")) diff --git a/pkg/integration/tests/branch/checkout_by_name.go b/pkg/integration/tests/branch/checkout_by_name.go index 956a2c36b..de5c0131c 100644 --- a/pkg/integration/tests/branch/checkout_by_name.go +++ b/pkg/integration/tests/branch/checkout_by_name.go @@ -24,14 +24,11 @@ var CheckoutByName = NewIntegrationTest(NewIntegrationTestArgs{ assert.SelectedLine(Contains("master")) input.NextItem() assert.SelectedLine(Contains("@")) - input.PressKeys(keys.Branches.CheckoutBranchByName) - assert.InPrompt() - assert.CurrentViewTitle(Equals("Branch name:")) - input.Type("new-branch") - input.Confirm() - assert.InAlert() - assert.CurrentViewContent(Equals("Branch not found. Create a new branch named new-branch?")) - input.Confirm() + input.Press(keys.Branches.CheckoutBranchByName) + + input.Prompt(Equals("Branch name:"), "new-branch") + + input.Alert(Equals("Branch not found"), Equals("Branch not found. Create a new branch named new-branch?")) |