mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-20 05:19:24 +02:00
allow chaining matchers
This commit is contained in:
parent
c841ba8237
commit
96310288ee
@ -177,8 +177,9 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
|
||||
self.assert.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 view.ViewBufferLines() {
|
||||
for i, line := range lines {
|
||||
ok, _ := matcher.test(line)
|
||||
if ok {
|
||||
matches = append(matches, line)
|
||||
@ -186,9 +187,9 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
|
||||
}
|
||||
}
|
||||
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"))
|
||||
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", matcher.name)
|
||||
return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n"))
|
||||
} else {
|
||||
return true, ""
|
||||
}
|
||||
|
@ -4,34 +4,118 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// for making assertions on string values
|
||||
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
|
||||
prefix string
|
||||
}
|
||||
|
||||
type matcherRule 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}
|
||||
rules := []matcherRule{{name: name, testFn: testFn}}
|
||||
return &matcher{rules: rules}
|
||||
}
|
||||
|
||||
func (self *matcher) name() string {
|
||||
if len(self.rules) == 0 {
|
||||
return "anything"
|
||||
}
|
||||
|
||||
return strings.Join(
|
||||
lo.Map(self.rules, func(rule matcherRule, _ int) string { return rule.name }),
|
||||
", ",
|
||||
)
|
||||
}
|
||||
|
||||
func (self *matcher) test(value string) (bool, string) {
|
||||
ok, message := self.testFn(value)
|
||||
if ok {
|
||||
// if there are no rules, then we pass the test by default
|
||||
if len(self.rules) == 0 {
|
||||
return true, ""
|
||||
}
|
||||
|
||||
if self.prefix != "" {
|
||||
return false, self.prefix + " " + message
|
||||
for _, rule := range self.rules {
|
||||
ok, message := rule.testFn(value)
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if self.prefix != "" {
|
||||
return false, self.prefix + " " + message
|
||||
}
|
||||
|
||||
return false, message
|
||||
}
|
||||
|
||||
return false, message
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (self *matcher) Contains(target string) *matcher {
|
||||
rule := matcherRule{
|
||||
name: fmt.Sprintf("contains '%s'", target),
|
||||
testFn: func(value string) (bool, string) {
|
||||
return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value)
|
||||
},
|
||||
}
|
||||
|
||||
self.rules = append(self.rules, rule)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *matcher) DoesNotContain(target string) *matcher {
|
||||
rule := matcherRule{
|
||||
name: fmt.Sprintf("does not contain '%s'", target),
|
||||
testFn: func(value string) (bool, string) {
|
||||
return !strings.Contains(value, target), fmt.Sprintf("Expected '%s' to NOT be found in '%s'", target, value)
|
||||
},
|
||||
}
|
||||
|
||||
self.rules = append(self.rules, rule)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *matcher) MatchesRegexp(target string) *matcher {
|
||||
rule := matcherRule{
|
||||
name: fmt.Sprintf("matches regular expression '%s'", target),
|
||||
testFn: 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)
|
||||
},
|
||||
}
|
||||
|
||||
self.rules = append(self.rules, rule)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *matcher) Equals(target string) *matcher {
|
||||
rule := matcherRule{
|
||||
name: fmt.Sprintf("equals '%s'", target),
|
||||
testFn: func(value string) (bool, string) {
|
||||
return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target)
|
||||
},
|
||||
}
|
||||
|
||||
self.rules = append(self.rules, rule)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *matcher) context(prefix string) *matcher {
|
||||
@ -40,42 +124,24 @@ func (self *matcher) context(prefix string) *matcher {
|
||||
return self
|
||||
}
|
||||
|
||||
func Contains(target string) *matcher {
|
||||
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)
|
||||
},
|
||||
)
|
||||
// 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 NotContains(target string) *matcher {
|
||||
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 Contains(target string) *matcher {
|
||||
return Anything().Contains(target)
|
||||
}
|
||||
|
||||
func DoesNotContain(target string) *matcher {
|
||||
return Anything().DoesNotContain(target)
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
)
|
||||
return Anything().MatchesRegexp(target)
|
||||
}
|
||||
|
||||
func Equals(target string) *matcher {
|
||||
return NewMatcher(
|
||||
fmt.Sprintf("equals '%s'", target),
|
||||
func(value string) (bool, string) {
|
||||
return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target)
|
||||
},
|
||||
)
|
||||
return Anything().Equals(target)
|
||||
}
|
||||
|
@ -52,15 +52,13 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
// lazygit will land us in the commit between our good and bad commits.
|
||||
assert.CurrentView().
|
||||
Name("commits").
|
||||
SelectedLine(Contains("commit 05")).
|
||||
SelectedLine(Contains("<-- current"))
|
||||
SelectedLine(Contains("commit 05").Contains("<-- current"))
|
||||
|
||||
markCommitAsBad()
|
||||
|
||||
assert.CurrentView().
|
||||
Name("commits").
|
||||
SelectedLine(Contains("commit 04")).
|
||||
SelectedLine(Contains("<-- current"))
|
||||
SelectedLine(Contains("commit 04").Contains("<-- current"))
|
||||
|
||||
markCommitAsGood()
|
||||
|
||||
@ -68,6 +66,6 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
input.Alert(Equals("Bisect complete"), MatchesRegexp("(?s)commit 05.*Do you want to reset"))
|
||||
|
||||
assert.CurrentView().Name("commits").Content(Contains("commit 04"))
|
||||
assert.View("information").Content(NotContains("bisecting"))
|
||||
assert.View("information").Content(DoesNotContain("bisecting"))
|
||||
},
|
||||
})
|
||||
|
@ -44,7 +44,7 @@ var FromOtherBranch = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
|
||||
input.Alert(Equals("Bisect complete"), MatchesRegexp(`(?s)commit 08.*Do you want to reset`))
|
||||
|
||||
assert.View("information").Content(NotContains("bisecting"))
|
||||
assert.View("information").Content(DoesNotContain("bisecting"))
|
||||
|
||||
// back in master branch which just had the one commit
|
||||
assert.CurrentView().Name("commits").Lines(
|
||||
|
@ -47,7 +47,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
|
||||
input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?"))
|
||||
|
||||
assert.View("information").Content(NotContains("rebasing"))
|
||||
assert.View("information").Content(DoesNotContain("rebasing"))
|
||||
|
||||
assert.View("commits").TopLines(
|
||||
Contains("second-change-branch unrelated change"),
|
||||
|
@ -72,7 +72,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
|
||||
input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?"))
|
||||
|
||||
assert.View("information").Content(NotContains("rebasing"))
|
||||
assert.View("information").Content(DoesNotContain("rebasing"))
|
||||
|
||||
assert.View("commits").TopLines(
|
||||
Contains("to keep"),
|
||||
|
@ -70,6 +70,6 @@ var CherryPick = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
|
||||
assert.View("information").Content(Contains("2 commits copied"))
|
||||
input.Press(keys.Universal.Return)
|
||||
assert.View("information").Content(NotContains("commits copied"))
|
||||
assert.View("information").Content(DoesNotContain("commits copied"))
|
||||
},
|
||||
})
|
||||
|
@ -83,6 +83,6 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
|
||||
assert.View("information").Content(Contains("2 commits copied"))
|
||||
input.Press(keys.Universal.Return)
|
||||
assert.View("information").Content(NotContains("commits copied"))
|
||||
assert.View("information").Content(DoesNotContain("commits copied"))
|
||||
},
|
||||
})
|
||||
|
@ -27,17 +27,17 @@ var Staged = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
// we start with both lines having been staged
|
||||
assert.View("stagingSecondary").Content(Contains("+myfile content"))
|
||||
assert.View("stagingSecondary").Content(Contains("+with a second line"))
|
||||
assert.View("staging").Content(NotContains("+myfile content"))
|
||||
assert.View("staging").Content(NotContains("+with a second line"))
|
||||
assert.View("staging").Content(DoesNotContain("+myfile content"))
|
||||
assert.View("staging").Content(DoesNotContain("+with a second line"))
|
||||
|
||||
// unstage the selected line
|
||||
input.PrimaryAction()
|
||||
|
||||
// the line should have been moved to the main view
|
||||
assert.View("stagingSecondary").Content(NotContains("+myfile content"))
|
||||
assert.View("stagingSecondary").Content(DoesNotContain("+myfile content"))
|
||||
assert.View("stagingSecondary").Content(Contains("+with a second line"))
|
||||
assert.View("staging").Content(Contains("+myfile content"))
|
||||
assert.View("staging").Content(NotContains("+with a second line"))
|
||||
assert.View("staging").Content(DoesNotContain("+with a second line"))
|
||||
|
||||
input.Press(keys.Files.CommitChanges)
|
||||
commitMessage := "my commit message"
|
||||
|
@ -25,19 +25,19 @@ var StagedWithoutHooks = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
input.Enter()
|
||||
assert.CurrentView().Name("stagingSecondary")
|
||||
// we start with both lines having been staged
|
||||
assert.View("stagingSecondary").Content(Contains("+myfile content"))
|
||||
assert.View("stagingSecondary").Content(Contains("+with a second line"))
|
||||
assert.View("staging").Content(NotContains("+myfile content"))
|
||||
assert.View("staging").Content(NotContains("+with a second line"))
|
||||
assert.View("stagingSecondary").Content(
|
||||
Contains("+myfile content").Contains("+with a second line"),
|
||||
)
|
||||
assert.View("staging").Content(
|
||||
DoesNotContain("+myfile content").DoesNotContain("+with a second line"),
|
||||
)
|
||||
|
||||
// unstage the selected line
|
||||
input.PrimaryAction()
|
||||
|
||||
// the line should have been moved to the main view
|
||||
assert.View("stagingSecondary").Content(NotContains("+myfile content"))
|
||||
assert.View("stagingSecondary").Content(Contains("+with a second line"))
|
||||
assert.View("staging").Content(Contains("+myfile content"))
|
||||
assert.View("staging").Content(NotContains("+with a second line"))
|
||||
assert.View("stagingSecondary").Content(DoesNotContain("+myfile content").Contains("+with a second line"))
|
||||
assert.View("staging").Content(Contains("+myfile content").DoesNotContain("+with a second line"))
|
||||
|
||||
input.Press(keys.Files.CommitChangesWithoutHook)
|
||||
assert.InCommitMessagePanel()
|
||||
|
@ -23,10 +23,10 @@ var Unstaged = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
assert.CurrentView().Name("files").SelectedLine(Contains("myfile"))
|
||||
input.Enter()
|
||||
assert.CurrentView().Name("staging")
|
||||
assert.View("stagingSecondary").Content(NotContains("+myfile content"))
|
||||
assert.View("stagingSecondary").Content(DoesNotContain("+myfile content"))
|
||||
// stage the first line
|
||||
input.PrimaryAction()
|
||||
assert.View("staging").Content(NotContains("+myfile content"))
|
||||
assert.View("staging").Content(DoesNotContain("+myfile content"))
|
||||
assert.View("stagingSecondary").Content(Contains("+myfile content"))
|
||||
|
||||
input.Press(keys.Files.CommitChanges)
|
||||
|
@ -54,7 +54,7 @@ var DiffAndApplyPatch = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
input.Press(keys.Universal.DiffingMenu)
|
||||
input.Menu(Equals("Diffing"), Contains("exit diff mode"))
|
||||
|
||||
assert.View("information").Content(NotContains("building patch"))
|
||||
assert.View("information").Content(DoesNotContain("building patch"))
|
||||
|
||||
input.Press(keys.Universal.CreatePatchOptionsMenu)
|
||||
// adding the regex '$' here to distinguish the menu item from the 'apply patch in reverse' item
|
||||
|
@ -25,7 +25,7 @@ var DirWithUntrackedFile = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
assert.CommitCount(1)
|
||||
|
||||
assert.MainView().
|
||||
Content(NotContains("error: Could not access")).
|
||||
Content(DoesNotContain("error: Could not access")).
|
||||
// we show baz because it's a modified file but we don't show bar because it's untracked
|
||||
// (though it would be cool if we could show that too)
|
||||
Content(Contains("baz"))
|
||||
|
Loading…
x
Reference in New Issue
Block a user