1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-10-08 22:52:12 +02:00

migrate patch building tests

This commit is contained in:
Jesse Duffield
2023-02-25 13:08:45 +11:00
parent 6c0b805137
commit dd1bf629b8
161 changed files with 1204 additions and 489 deletions

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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().

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -10,45 +10,12 @@ 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)
}
// asserts that the view has the expected title
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)
})
return self
}
// 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))
})
return self.assertLines(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))
return self.assertLines(matchers...)
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) {
@@ -61,7 +28,114 @@ func (self *ViewDriver) getSelectedLines() ([]string, error) {
return self.getSelectedLinesFn()
}
func (self *ViewDriver) SelectedLines(matchers ...*matcher) *ViewDriver {
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 {
self.t.assertWithRetries(func() (bool, string) {
actual := self.getView().Title
return expected.context(fmt.Sprintf("%s title", self.context)).test(actual)
})
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 {
self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers)
return self.assertLines(0, matchers...)
}
func (self *ViewDriver) ContainsLines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers)
self.t.assertWithRetries(func() (bool, string) {
content := self.getView().Buffer()
lines := strings.Split(content, "\n")
startIdx, endIdx, err := self.getSelectedRange()
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, ""
}
}
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
}
// 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
break
}
}
if matches {
return true, ""
}
}
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-----",
expectedContent,
content,
)
})
return self
func (self *ViewDriver) validateMatchersPassed(matchers []*Matcher) {
if len(matchers) < 1 {
self.t.fail("'Lines' methods require at least one matcher to be passed as an argument. If you are trying to assert that there are no lines, use .IsEmpty()")
}
}
func (self *ViewDriver) assertLines(matchers ...*matcher) *ViewDriver {
func (self *ViewDriver) validateEnoughLines(matchers []*Matcher) {
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) assertLines(offset int, matchers ...*Matcher) *ViewDriver {
view := self.getView()
for i, matcher := range matchers {
for matcherIndex, matcher := range matchers {
lineIdx := matcherIndex + offset
checkIsSelected, matcher := matcher.checkIsSelected()
self.t.matchString(matcher, fmt.Sprintf("Unexpected content in view '%s'.", view.Name()),
func() string {
return view.BufferLines()[i]
return view.BufferLines()[lineIdx]
},
)
if checkIsSelected {
self.t.assertWithRetries(func() (bool, string) {
lineIdx := view.SelectedLineIdx()
return lineIdx == i, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected %d, got %d", view.Name(), i, lineIdx)
startIdx, endIdx, err := self.getSelectedRange()
if err != nil {
return false, err.Error()
}
if lineIdx < startIdx || lineIdx > endIdx {
if startIdx == endIdx {
return false, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected %d, got %d", view.Name(), lineIdx, startIdx)
} else {
lines, err := self.getSelectedLines()
if err != nil {
return false, err.Error()
}
return false, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected line %d to be in range %d to %d. Selected lines:\n---\n%s\n---\n\nExpected line: '%s'", view.Name(), lineIdx, startIdx, endIdx, strings.Join(lines, "\n"), matcher.name())
}
}
return true, ""
})
}
}
@@ -143,7 +220,7 @@ func (self *ViewDriver) assertLines(matchers ...*matcher) *ViewDriver {
}
// asserts on the content of the view i.e. the stuff within the view's frame.
func (self *ViewDriver) Content(matcher *matcher) *ViewDriver {
func (self *ViewDriver) Content(matcher *Matcher) *ViewDriver {
self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context),
func() string {
return self.getView().Buffer()
@@ -153,40 +230,27 @@ func (self *ViewDriver) Content(matcher *matcher) *ViewDriver {
return self
}
// asserts on the selected line of the view
func (self *ViewDriver) SelectedLine(matcher *matcher) *ViewDriver {
// asserts on the selected line of the view. If your view has multiple lines selected,
// but also has a concept of a cursor position, this will assert on the line that
// the cursor is on. Otherwise it will assert on the first line of the selection.
func (self *ViewDriver) SelectedLine(matcher *Matcher) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
selectedLines, err := self.getSelectedLines()
selectedLineIdx, err := self.getSelectedLineIdx()
if err != nil {
return false, err.Error()
}
if len(selectedLines) == 0 {
return false, "No line selected. Expected exactly one line to be selected"
} else if len(selectedLines) > 1 {
return false, fmt.Sprintf(
"Multiple lines selected. Expected only a single line to be selected. Selected lines:\n---\n%s\n---\n\nExpected line: %s",
strings.Join(selectedLines, "\n"),
matcher.name(),
)
viewLines := self.getView().BufferLines()
if selectedLineIdx >= len(viewLines) {
return false, fmt.Sprintf("%s: Expected view to have at least %d lines, but it only has %d", self.context, selectedLineIdx+1, len(viewLines))
}
value := selectedLines[0]
value := viewLines[selectedLineIdx]
return matcher.context(fmt.Sprintf("%s: Unexpected selected line.", self.context)).test(value)
})
self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected selected line.", self.context),
func() string {
selectedLines, err := self.getSelectedLines()
if err != nil {
self.t.gui.Fail(err.Error())
return "<failed to obtain selected line>"
}
return selectedLines[0]
},
)
return self
}
@@ -298,10 +362,63 @@ func (self *ViewDriver) PressEscape() *ViewDriver {
return self.Press(self.t.keys.Universal.Return)
}
func (self *ViewDriver) NavigateToListItem(matcher *matcher) *ViewDriver {
// 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 BufferLines 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 *ViewDriver) NavigateToListItem(matcher *Matcher) *ViewDriver {
self.IsFocused()
self.t.navigateToListItem(matcher)
view := self.getView()
var matchIndex int
self.t.assertWithRetries(func() (bool, string) {
matchIndex = -1
var matches []string
lines := view.BufferLines()
// 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, err := self.getSelectedLineIdx()
if err != nil {
self.t.fail(err.Error())
return self
}
if selectedLineIdx == matchIndex {
self.SelectedLine(matcher)
} else if selectedLineIdx < matchIndex {
for i := selectedLineIdx; i < matchIndex; i++ {
self.SelectNextItem()
}
self.SelectedLine(matcher)
} else {
for i := selectedLineIdx; i > matchIndex; i-- {
self.SelectPreviousItem()
}
self.SelectedLine(matcher)
}
return self
}
@@ -349,8 +466,13 @@ func (self *ViewDriver) Tap(f func()) *ViewDriver {
return self
}
func expectedContentFromMatchers(matchers []*matcher) string {
return strings.Join(lo.Map(matchers, func(matcher *matcher, _ int) string {
// This purely exists as a convenience method for those who hate the trailing periods in multi-line method chains
func (self *ViewDriver) Self() *ViewDriver {
return self
}
func expectedContentFromMatchers(matchers []*Matcher) string {
return strings.Join(lo.Map(matchers, func(matcher *Matcher, _ int) string {
return matcher.name()
}), "\n")
}

View File

@@ -40,34 +40,97 @@ func (self *Views) Secondary() *ViewDriver {
}
func (self *Views) regularView(viewName string) *ViewDriver {
return self.newStaticViewDriver(viewName, nil)
return self.newStaticViewDriver(viewName, nil, nil, nil)
}
func (self *Views) patchExplorerViewByName(viewName string) *ViewDriver {
return self.newStaticViewDriver(viewName, func() ([]string, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return nil, errors.New("Expected patch explorer to be activated")
}
selectedContent := state.PlainRenderSelected()
// the above method returns a string with a trailing newline so we need to remove that before splitting
selectedLines := strings.Split(strings.TrimSuffix(selectedContent, "\n"), "\n")
return selectedLines, nil
})
return self.newStaticViewDriver(
viewName,
func() ([]string, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return nil, errors.New("Expected patch explorer to be activated")
}
selectedContent := state.PlainRenderSelected()
// the above method returns a string with a trailing newline so we need to remove that before splitting
selectedLines := strings.Split(strings.TrimSuffix(selectedContent, "\n"), "\n")
return selectedLines, nil
},
func() (int, int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return 0, 0, errors.New("Expected patch explorer to be activated")
}
startIdx, endIdx := state.SelectedRange()
return startIdx, endIdx, nil
},
func() (int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return 0, errors.New("Expected patch explorer to be activated")
}
return state.GetSelectedLineIdx(), nil
},
)
}
// 'static' because it'll always refer to the same view, as opposed to the 'main' view which could actually be
// one of several views, or the 'current' view which depends on focus.
func (self *Views) newStaticViewDriver(viewName string, getSelectedLinesFn func() ([]string, error)) *ViewDriver {
func (self *Views) newStaticViewDriver(
viewName string,
getSelectedLinesFn func() ([]string, error),
getSelectedLineRangeFn func() (int, int, error),
getSelectedLineIdxFn func() (int, error),
) *ViewDriver {
return &ViewDriver{
context: fmt.Sprintf("%s view", viewName),
getView: func() *gocui.View { return self.t.gui.View(viewName) },
getSelectedLinesFn: getSelectedLinesFn,
t: self.t,
context: fmt.Sprintf("%s view", viewName),
getView: func() *gocui.View { return self.t.gui.View(viewName) },
getSelectedLinesFn: getSelectedLinesFn,
getSelectedRangeFn: getSelectedLineRangeFn,
getSelectedLineIdxFn: getSelectedLineIdxFn,
t: self.t,
}
}
func (self *Views) MergeConflicts() *ViewDriver {
viewName := "mergeConflicts"
return self.newStaticViewDriver(
viewName,
func() ([]string, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return nil, errors.New("Expected patch explorer to be activated")
}
selectedContent := strings.Split(state.PlainRenderSelected(), "\n")
return selectedContent, nil
},
func() (int, int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return 0, 0, errors.New("Expected patch explorer to be activated")
}
startIdx, endIdx := state.GetSelectedRange()
return startIdx, endIdx, nil
},
// there is no concept of a cursor in the merge conflicts panel so we just return the start of the selection
func() (int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return 0, errors.New("Expected patch explorer to be activated")
}
startIdx, _ := state.GetSelectedRange()
return startIdx, nil
},
)
}
func (self *Views) Commits() *ViewDriver {
return self.regularView("commits")
}
@@ -158,10 +221,6 @@ func (self *Views) Suggestions() *ViewDriver {
return self.regularView("suggestions")
}
func (self *Views) MergeConflicts() *ViewDriver {
return self.regularView("mergeConflicts")
}
func (self *Views) Search() *ViewDriver {
return self.regularView("search")
}