diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2023-02-25 13:08:45 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2023-02-25 21:37:16 +1100 |
commit | dd1bf629b87db297a6336511a4e1ba39ba7afbc8 (patch) | |
tree | c12fb83f36821eb2faf2cbe357a2876997605f74 /pkg/integration | |
parent | 6c0b805137dbed6d8d89d2e10cb7018776b55321 (diff) |
migrate patch building tests
Diffstat (limited to 'pkg/integration')
27 files changed, 1167 insertions, 218 deletions
diff --git a/pkg/integration/components/actions.go b/pkg/integration/components/actions.go index 50564233a..5e37fc966 100644 --- a/pkg/integration/components/actions.go +++ b/pkg/integration/components/actions.go @@ -38,3 +38,9 @@ func (self *Actions) ConfirmDiscardLines() { Content(Contains("Are you sure you want to delete the selected lines")). Confirm() } + +func (self *Actions) SelectPatchOption(matcher *Matcher) { + self.t.GlobalPress(self.t.keys.Universal.CreatePatchOptionsMenu) + + self.t.ExpectPopup().Menu().Title(Equals("Patch Options")).Select(matcher).Confirm() +} diff --git a/pkg/integration/components/alert_driver.go b/pkg/integration/components/alert_driver.go index 78247760a..7e1623041 100644 --- a/pkg/integration/components/alert_driver.go +++ b/pkg/integration/components/alert_driver.go @@ -11,7 +11,7 @@ func (self *AlertDriver) getViewDriver() *ViewDriver { } // asserts that the alert view has the expected title -func (self *AlertDriver) Title(expected *matcher) *AlertDriver { +func (self *AlertDriver) Title(expected *Matcher) *AlertDriver { self.getViewDriver().Title(expected) self.hasCheckedTitle = true @@ -20,7 +20,7 @@ func (self *AlertDriver) Title(expected *matcher) *AlertDriver { } // asserts that the alert view has the expected content -func (self *AlertDriver) Content(expected *matcher) *AlertDriver { +func (self *AlertDriver) Content(expected *Matcher) *AlertDriver { self.getViewDriver().Content(expected) self.hasCheckedContent = true diff --git a/pkg/integration/components/assertion_helper.go b/pkg/integration/components/assertion_helper.go index 70f2ff182..23539a8d3 100644 --- a/pkg/integration/components/assertion_helper.go +++ b/pkg/integration/components/assertion_helper.go @@ -13,7 +13,7 @@ type assertionHelper struct { // milliseconds we'll wait when an assertion fails. var retryWaitTimes = []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000} -func (self *assertionHelper) matchString(matcher *matcher, context string, getValue func() string) { +func (self *assertionHelper) matchString(matcher *Matcher, context string, getValue func() string) { self.assertWithRetries(func() (bool, string) { value := getValue() return matcher.context(context).test(value) diff --git a/pkg/integration/components/commit_message_panel_driver.go b/pkg/integration/components/commit_message_panel_driver.go index b44a6c1dc..e420334bf 100644 --- a/pkg/integration/components/commit_message_panel_driver.go +++ b/pkg/integration/components/commit_message_panel_driver.go @@ -9,7 +9,7 @@ func (self *CommitMessagePanelDriver) getViewDriver() *ViewDriver { } // asserts on the text initially present in the prompt -func (self *CommitMessagePanelDriver) InitialText(expected *matcher) *CommitMessagePanelDriver { +func (self *CommitMessagePanelDriver) InitialText(expected *Matcher) *CommitMessagePanelDriver { self.getViewDriver().Content(expected) return self diff --git a/pkg/integration/components/confirmation_driver.go b/pkg/integration/components/confirmation_driver.go index 5f9500c56..687523833 100644 --- a/pkg/integration/components/confirmation_driver.go +++ b/pkg/integration/components/confirmation_driver.go @@ -11,7 +11,7 @@ func (self *ConfirmationDriver) getViewDriver() *ViewDriver { } // asserts that the confirmation view has the expected title -func (self *ConfirmationDriver) Title(expected *matcher) *ConfirmationDriver { +func (self *ConfirmationDriver) Title(expected *Matcher) *ConfirmationDriver { self.getViewDriver().Title(expected) self.hasCheckedTitle = true @@ -20,7 +20,7 @@ func (self *ConfirmationDriver) Title(expected *matcher) *ConfirmationDriver { } // asserts that the confirmation view has the expected content -func (self *ConfirmationDriver) Content(expected *matcher) *ConfirmationDriver { +func (self *ConfirmationDriver) Content(expected *Matcher) *ConfirmationDriver { self.getViewDriver().Content(expected) self.hasCheckedContent = true diff --git a/pkg/integration/components/file_system.go b/pkg/integration/components/file_system.go index 4ac064838..cdc7413fb 100644 --- a/pkg/integration/components/file_system.go +++ b/pkg/integration/components/file_system.go @@ -26,7 +26,7 @@ func (self *FileSystem) PathNotPresent(path string) { } // Asserts that the file at the given path has the given content -func (self *FileSystem) FileContent(path string, matcher *matcher) { +func (self *FileSystem) FileContent(path string, matcher *Matcher) { self.assertWithRetries(func() (bool, string) { _, err := os.Stat(path) if os.IsNotExist(err) { diff --git a/pkg/integration/components/matcher.go b/pkg/integration/components/matcher.go index ebec084a8..a87234654 100644 --- a/pkg/integration/components/matcher.go +++ b/pkg/integration/components/matcher.go @@ -9,7 +9,7 @@ import ( ) // for making assertions on string values -type matcher struct { +type Matcher struct { rules []matcherRule // this is printed when there's an error so that it's clear what the context of the assertion is @@ -24,12 +24,12 @@ type matcherRule struct { testFn func(string) (bool, string) } -func NewMatcher(name string, testFn func(string) (bool, string)) *matcher { +func NewMatcher(name string, testFn func(string) (bool, string)) *Matcher { rules := []matcherRule{{name: name, testFn: testFn}} - return &matcher{rules: rules} + return &Matcher{rules: rules} } -func (self *matcher) name() string { +func (self *Matcher) name() string { if len(self.rules) == 0 { return "anything" } @@ -40,7 +40,7 @@ func (self *matcher) name() string { ) } -func (self *matcher) test(value string) (bool, string) { +func (self *Matcher) test(value string) (bool, string) { for _, rule := range self.rules { ok, message := rule.testFn(value) if ok { @@ -57,7 +57,7 @@ func (self *matcher) test(value string) (bool, string) { return true, "" } -func (self *matcher) Contains(target string) *matcher { +func (self *Matcher) Contains(target string) *Matcher { return self.appendRule(matcherRule{ name: fmt.Sprintf("contains '%s'", target), testFn: func(value string) (bool, string) { @@ -71,7 +71,7 @@ func (self *matcher) Contains(target string) *matcher { }) } -func (self *matcher) DoesNotContain(target string) *matcher { +func (self *Matcher) DoesNotContain(target string) *Matcher { return self.appendRule(matcherRule{ name: fmt.Sprintf("does not contain '%s'", target), testFn: func(value string) (bool, string) { @@ -80,7 +80,7 @@ func (self *matcher) DoesNotContain(target string) *matcher { }) } -func (self *matcher) MatchesRegexp(target string) *matcher { +func (self *Matcher) MatchesRegexp(target string) *Matcher { return self.appendRule(matcherRule{ name: fmt.Sprintf("matches regular expression '%s'", target), testFn: func(value string) (bool, string) { @@ -93,7 +93,7 @@ func (self *matcher) MatchesRegexp(target string) *matcher { }) } -func (self *matcher) Equals(target string) *matcher { +func (self *Matcher) Equals(target string) *Matcher { return self.appendRule(matcherRule{ name: fmt.Sprintf("equals '%s'", target), testFn: func(value string) (bool, string) { @@ -106,7 +106,7 @@ const IS_SELECTED_RULE_NAME = "is selected" // special rule that is only to be used in the TopLines and Lines methods, as a way of // asserting that a given line is selected. -func (self *matcher) IsSelected() *matcher { +func (self *Matcher) IsSelected() *Matcher { return self.appendRule(matcherRule{ name: IS_SELECTED_RULE_NAME, testFn: func(value string) (bool, string) { @@ -115,7 +115,7 @@ func (self *matcher) IsSelected() *matcher { }) } -func (self *matcher) appendRule(rule matcherRule) *matcher { +func (self *Matcher) appendRule(rule matcherRule) *Matcher { self.rules = append(self.rules, rule) return self @@ -123,39 +123,43 @@ func (self *matcher) appendRule(rule matcherRule) *matcher { // adds context so that if the matcher test(s) fails, we understand what we were trying to test. // E.g. prefix: "Unexpected content in view 'files'." -func (self *matcher) context(prefix string) *matcher { +func (self *Matcher) context(prefix string) *Matcher { self.prefix = prefix return self } // if the matcher has an `IsSelected` rule, it returns true, along with the matcher after that rule has been removed -func (self *matcher) checkIsSelected() (bool, *matcher) { - check := lo.ContainsBy(self.rules, func(rule matcherRule) bool { return rule.name == IS_SELECTED_RULE_NAME }) +func (self *Matcher) checkIsSelected() (bool, *Matcher) { + // copying into a new matcher in case we want to re-use the original later + newMatcher := &Matcher{} + *newMatcher = *self - self.rules = lo.Filter(self.rules, func(rule matcherRule, _ int) bool { return rule.name != IS_SELECTED_RULE_NAME }) + check := lo.ContainsBy(newMatcher.rules, func(rule matcherRule) bool { return rule.name == IS_SELECTED_RULE_NAME }) - return check, self + newMatcher.rules = lo.Filter(newMatcher.rules, func(rule matcherRule, _ int) bool { return rule.name != IS_SELECTED_RULE_NAME }) + + return check, newMatcher } // this matcher has no rules meaning it always passes the test. Use this // when you don't care what value you're dealing with. -func Anything() *matcher { - return &matcher{} +func Anything() *Matcher { + return &Matcher{} } -func Contains(target string) *matcher { +func Contains(target string) *Matcher { return Anything().Contains(target) } -func DoesNotContain(target string) *matcher { +func DoesNotContain(target string) *Matcher { return Anything().DoesNotContain(target) } -func MatchesRegexp(target string) *matcher { +func MatchesRegexp(target string) *Matcher { return Anything().MatchesRegexp(target) } -func Equals(target string) *matcher { +func Equals(target string) *Matcher { return Anything().Equals(target) } diff --git a/pkg/integration/components/menu_driver.go b/pkg/integration/components/menu_driver.go index b8155aaf7..326435f52 100644 --- a/pkg/integration/components/menu_driver.go +++ b/pkg/integration/components/menu_driver.go @@ -10,7 +10,7 @@ func (self *MenuDriver) getViewDriver() *ViewDriver { } // asserts that the popup has the expected title -func (self *MenuDriver) Title(expected *matcher) *MenuDriver { +func (self *MenuDriver) Title(expected *Matcher) *MenuDriver { self.getViewDriver().Title(expected) self.hasCheckedTitle = true @@ -30,19 +30,19 @@ func (self *MenuDriver) Cancel() { self.getViewDriver().PressEscape() } -func (self *MenuDriver) Select(option *matcher) *MenuDriver { +func (self *MenuDriver) Select(option *Matcher) *MenuDriver { self.getViewDriver().NavigateToListItem(option) return self } -func (self *MenuDriver) Lines(matchers ...*matcher) *MenuDriver { +func (self *MenuDriver) Lines(matchers ...*Matcher) *MenuDriver { self.getViewDriver().Lines(matchers...) return self } -func (self *MenuDriver) TopLines(matchers ...*matcher) *MenuDriver { +func (self *MenuDriver) TopLines(matchers ...*Matcher) *MenuDriver { self.getViewDriver().TopLines(matchers...) return self diff --git a/pkg/integration/components/prompt_driver.go b/pkg/integration/components/prompt_driver.go index 59e3587ef..58051bc70 100644 --- a/pkg/integration/components/prompt_driver.go +++ b/pkg/integration/components/prompt_driver.go @@ -10,7 +10,7 @@ func (self *PromptDriver) getViewDriver() *ViewDriver { } // asserts that the popup has the expected title -func (self *PromptDriver) Title(expected *matcher) *PromptDriver { +func (self *PromptDriver) Title(expected *Matcher) *PromptDriver { self.getViewDriver().Title(expected) self.hasCheckedTitle = true @@ -19,7 +19,7 @@ func (self *PromptDriver) Title(expected *matcher) *PromptDriver { } // asserts on the text initially present in the prompt -func (self *PromptDriver) InitialText(expected *matcher) *PromptDriver { +func (self *PromptDriver) InitialText(expected *Matcher) *PromptDriver { self.getViewDriver().Content(expected) return self @@ -55,13 +55,13 @@ func (self *PromptDriver) checkNecessaryChecksCompleted() { } } -func (self *PromptDriver) SuggestionLines(matchers ...*matcher) *PromptDriver { +func (self *PromptDriver) SuggestionLines(matchers ...*Matcher) *PromptDriver { self.t.Views().Suggestions().Lines(matchers...) return self } -func (self *PromptDriver) SuggestionTopLines(matchers ...*matcher) *PromptDriver { +func (self *PromptDriver) SuggestionTopLines(matchers ...*Matcher) *PromptDriver { self.t.Views().Suggestions().TopLines(matchers...) return self @@ -75,7 +75,7 @@ func (self *PromptDriver) ConfirmFirstSuggestion() { PressEnter() } -func (self *PromptDriver) ConfirmSuggestion(matcher *matcher) { +func (self *PromptDriver) ConfirmSuggestion(matcher *Matcher) { self.t.press(self.t.keys.Universal.TogglePanel) self.t.Views().Suggestions(). IsFocused(). diff --git a/pkg/integration/components/search_driver.go b/pkg/integration/components/search_driver.go index 4ab4a4103..66a2fae41 100644 --- a/pkg/integration/components/search_driver.go +++ b/pkg/integration/components/search_driver.go @@ -12,7 +12,7 @@ func (self *SearchDriver) getViewDriver() *ViewDriver { } // asserts on the text initially present in the prompt -func (self *SearchDriver) InitialText(expected *matcher) *SearchDriver { +func (self *SearchDriver) InitialText(expected *Matcher) *SearchDriver { self.getViewDriver().Content(expected) return self diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index 8d47083ad..2f2d4b719 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -136,6 +136,10 @@ func (self *Shell) EmptyCommit(message string) *Shell { return self.RunCommand(fmt.Sprintf("git commit --allow-empty -m \"%s\"", message)) } +func (self *Shell) Revert(ref string) *Shell { + return self.RunCommand(fmt.Sprintf("git revert %s", ref)) +} + func (self *Shell) CreateLightweightTag(name string, ref string) *Shell { return self.RunCommand(fmt.Sprintf("git tag %s %s", name, ref)) } diff --git a/pkg/integration/components/test_driver.go b/pkg/integration/components/test_driver.go index 694a59dbf..d645b7f0a 100644 --- a/pkg/integration/components/test_driver.go +++ b/pkg/integration/components/test_driver.go @@ -2,7 +2,6 @@ package components import ( "fmt" - "strings" "time" "github.com/atotto/clipboard" @@ -71,65 +70,6 @@ func (self *TestDriver) Shell() *Shell { return self.shell } -// 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) { - currentContext := self.gui.CurrentContext() - - 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 - } -} - // for making assertions on lazygit views func (self *TestDriver) Views() *Views { return &Views{t: self} @@ -140,11 +80,11 @@ func (self *TestDriver) ExpectPopup() *Popup { return &Popup{t: self} } -func (self *TestDriver) ExpectToast(matcher *matcher) { +func (self *TestDriver) ExpectToast(matcher *Matcher) { self.Views().AppStatus().Content(matcher) } -func (self *TestDriver) ExpectClipboard(matcher *matcher) { +func (self *TestDriver) ExpectClipboard(matcher *Matcher) { self.assertWithRetries(func() (bool, string) { text, err := clipboard.ReadAll() if err != nil { diff --git a/pkg/integration/components/viewDriver.go b/pkg/integration/components/viewDriver.go index 3f0b9726b..5060a4d1d 100644 --- a/pkg/integration/components/viewDriver.go +++ b/pkg/integration/components/viewDriver.go @@ -10,14 +10,49 @@ import ( type ViewDriver struct { // context is prepended to any error messages e.g. 'context: "current view"' - context string - getView func() *gocui.View - t *TestDriver - getSelectedLinesFn func() ([]string, error) + context string + getView func() *gocui.View + t *TestDriver + getSelectedLinesFn func() ([]string, error) + getSelectedRangeFn func() (int, int, error) + getSelectedLineIdxFn func() (int, error) +} + +func (self *ViewDriver) getSelectedLines() ([]string, error) { + if self.getSelectedLinesFn == nil { + view := self.t.gui.View(self.getView().Name()) + + return []string{view.SelectedLine()}, nil + } + + return self.getSelectedLinesFn() +} + +func (self *ViewDriver) getSelectedRange() (int, int, error) { + if self.getSelectedRangeFn == nil { + view := self.t.gui.View(self.getView().Name()) + idx := view.SelectedLineIdx() + + return idx, idx, nil + } + + return self.getSelectedRangeFn() +} + +// even if you have a selected range, there may still be a line within that range +// which the cursor points at. This function returns that line index. +func (self *ViewDriver) getSelectedLineIdx() (int, error) { + if self.getSelectedLineIdxFn == nil { + view := self.t.gui.View(self.getView().Name()) + + return view.SelectedLineIdx(), nil + } + + return self.getSelectedLineIdxFn() } // asserts that the view has the expected title -func (self *ViewDriver) Title(expected *matcher) *ViewDriver { +func (self *ViewDriver) Title(expected *Matcher) *ViewDriver { self.t.assertWithRetries(func() (bool, string) { actual := self.getView().Title return expected.context(fmt.Sprintf("%s title", self.context)).test(actual) @@ -26,42 +61,81 @@ func (self *ViewDriver) Title(expected *matcher) *ViewDriver { return self } +// asserts that the view has lines matching the given matchers. One matcher must be passed for each line. +// If you only care about the top n lines, use the TopLines method instead. +func (self *ViewDriver) Lines(matchers ...*Matcher) *ViewDriver { + self.validateMatchersPassed(matchers) + self.LineCount(len(matchers)) + + return self.assertLines(0, matchers...) +} + // asserts that the view has lines matching the given matchers. So if three matchers // are passed, we only check the first three lines of the view. // This method is convenient when you have a list of commits but you only want to // assert on the first couple of commits. -func (self *ViewDriver) TopLines(matchers ...*matcher) *ViewDriver { - if len(matchers) < 1 { - self.t.fail("TopLines method requires at least one matcher. If you are trying to assert that there are no lines, use .IsEmpty()") - } - - self.t.assertWithRetries(func() (bool, string) { - lines := self.getView().BufferLines() - return len(lines) >= len(matchers), fmt.Sprintf("unexpected number of lines in view. Expected at least %d, got %d", len(matchers), len(lines)) - }) +func (self *ViewDriver) TopLines(matchers ...*Matcher) *ViewDriver { + self.validateMatchersPassed(matchers) + self.validateEnoughLines(matchers) - return self.assertLines(matchers...) + return self.assertLines(0, matchers...) } -// asserts that the view has lines matching the given matchers. One matcher must be passed for each line. -// If you only care about the top n lines, use the TopLines method instead. -func (self *ViewDriver) Lines(matchers ...*matcher) *ViewDriver { - self.LineCount(len(matchers)) +func (self *ViewDriver) ContainsLines(matchers ...*Matcher) *ViewDriver { + self.validateMatchersPassed(matchers) + self.validateEnoughLines(matchers) - return self.assertLines(matchers...) -} + self.t.assertWithRetries(func() (bool, string) { + content := self.getView().Buffer() + lines := strings.Split(content, "\n") -func (self *ViewDriver) getSelectedLines() ([]string, error) { - if self.getSelectedLinesFn == nil { - view := self.t.gui.View(self.getView().Name()) + startIdx, endIdx, err := self.getSelectedRange() - return []string{view.SelectedLine()}, nil - } + for i := 0; i < len(lines)-len(matchers)+1; i++ { + matches := true + for j, matcher := range matchers { + checkIsSelected, matcher := matcher.checkIsSelected() // strip the IsSelected matcher out + lineIdx := i + j + ok, _ := matcher.test(lines[lineIdx]) + if !ok { + matches = false + break + } + if checkIsSelected { + if err != nil { + matches = false + break + } + if lineIdx < startIdx || lineIdx > endIdx { + matches = false + break + } + } + } + if matches { + return true, "" + } + } - return self.getSelectedLinesFn() + expectedContent := expectedContentFromMatchers(matchers) + + return false, fmt.Sprintf( + "Expected the following to be contained in the staging panel:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----\nSelected range: %d-%d", + expectedContent, + content, + startIdx, + endIdx, + ) + }) + + return self } -func (self *ViewDriver) SelectedLines(matchers ...*matcher) *ViewDriver { +// asserts on the lines that are selected in the view. +func (self *ViewDriver) SelectedLines(matchers ...*Matcher) *ViewDriver { + self.validateMatchersPassed(matchers) + self.validateEnoughLines(matchers) + self.t.assertWithRetries(func() (bool, string) { selectedLines, err := self.getSelectedLines() if err != nil { @@ -76,7 +150,12 @@ func (self *ViewDriver) SelectedLines(matchers ...*matcher) *ViewDriver { } for i, line := range selectedLines { - ok, message := matchers[i].test(line) + checkIsSelected, matcher := matchers[i].checkIsSelected() + if checkIsSelected { + self.t.fail("You cannot use the IsSelected matcher with the SelectedLines method") + } + + ok, message := matcher.test(line) if !ok { return false, fmt.Sprintf("Error: %s. Expected the following to be selected:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----", message, expectedContent, selectedContent) } @@ -88,53 +167,51 @@ func (self *ViewDriver) SelectedLines(matchers ...*matcher) *ViewDriver { return self } -func (self *ViewDriver) ContainsLines(matchers ...*matcher) *ViewDriver { - self.t.assertWithRetries(func() (bool, string) { - content := self.getView().Buffer() - lines := strings.Split(content, "\n") - - for i := 0; i < len(lines)-len(matchers)+1; i++ { - matches := true - for j, matcher := range matchers { - ok, _ := matcher.test(lines[i+j]) - if !ok { - matches = false - |