mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-15 01:34:26 +02:00
allow chaining matchers
This commit is contained in:
@ -177,8 +177,9 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
|
|||||||
self.assert.assertWithRetries(func() (bool, string) {
|
self.assert.assertWithRetries(func() (bool, string) {
|
||||||
matchIndex = -1
|
matchIndex = -1
|
||||||
var matches []string
|
var matches []string
|
||||||
|
lines := view.ViewBufferLines()
|
||||||
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
|
// 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)
|
ok, _ := matcher.test(line)
|
||||||
if ok {
|
if ok {
|
||||||
matches = append(matches, line)
|
matches = append(matches, line)
|
||||||
@ -186,9 +187,9 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(matches) > 1 {
|
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 {
|
} 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 {
|
} else {
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
@ -4,34 +4,118 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// for making assertions on string values
|
// 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
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
type matcherRule struct {
|
||||||
// e.g. "contains 'foo'"
|
// e.g. "contains 'foo'"
|
||||||
name string
|
name string
|
||||||
// returns a bool that says whether the test passed and if it returns false, it
|
// returns a bool that says whether the test passed and if it returns false, it
|
||||||
// also returns a string of the error message
|
// also returns a string of the error message
|
||||||
testFn func(string) (bool, string)
|
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 {
|
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) {
|
func (self *matcher) test(value string) (bool, string) {
|
||||||
ok, message := self.testFn(value)
|
// if there are no rules, then we pass the test by default
|
||||||
if ok {
|
if len(self.rules) == 0 {
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.prefix != "" {
|
for _, rule := range self.rules {
|
||||||
return false, self.prefix + " " + message
|
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 {
|
func (self *matcher) context(prefix string) *matcher {
|
||||||
@ -40,42 +124,24 @@ func (self *matcher) context(prefix string) *matcher {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
func Contains(target string) *matcher {
|
// this matcher has no rules meaning it always passes the test. Use this
|
||||||
return NewMatcher(
|
// when you don't care what value you're dealing with.
|
||||||
fmt.Sprintf("contains '%s'", target),
|
func Anything() *matcher {
|
||||||
func(value string) (bool, string) {
|
return &matcher{}
|
||||||
return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NotContains(target string) *matcher {
|
func Contains(target string) *matcher {
|
||||||
return NewMatcher(
|
return Anything().Contains(target)
|
||||||
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 DoesNotContain(target string) *matcher {
|
||||||
},
|
return Anything().DoesNotContain(target)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MatchesRegexp(target string) *matcher {
|
func MatchesRegexp(target string) *matcher {
|
||||||
return NewMatcher(
|
return Anything().MatchesRegexp(target)
|
||||||
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 {
|
func Equals(target string) *matcher {
|
||||||
return NewMatcher(
|
return Anything().Equals(target)
|
||||||
fmt.Sprintf("equals '%s'", target),
|
|
||||||
func(value string) (bool, string) {
|
|
||||||
return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -52,15 +52,13 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
// lazygit will land us in the commit between our good and bad commits.
|
// lazygit will land us in the commit between our good and bad commits.
|
||||||
assert.CurrentView().
|
assert.CurrentView().
|
||||||
Name("commits").
|
Name("commits").
|
||||||
SelectedLine(Contains("commit 05")).
|
SelectedLine(Contains("commit 05").Contains("<-- current"))
|
||||||
SelectedLine(Contains("<-- current"))
|
|
||||||
|
|
||||||
markCommitAsBad()
|
markCommitAsBad()
|
||||||
|
|
||||||
assert.CurrentView().
|
assert.CurrentView().
|
||||||
Name("commits").
|
Name("commits").
|
||||||
SelectedLine(Contains("commit 04")).
|
SelectedLine(Contains("commit 04").Contains("<-- current"))
|
||||||
SelectedLine(Contains("<-- current"))
|
|
||||||
|
|
||||||
markCommitAsGood()
|
markCommitAsGood()
|
||||||
|
|
||||||
@ -68,6 +66,6 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
input.Alert(Equals("Bisect complete"), MatchesRegexp("(?s)commit 05.*Do you want to reset"))
|
input.Alert(Equals("Bisect complete"), MatchesRegexp("(?s)commit 05.*Do you want to reset"))
|
||||||
|
|
||||||
assert.CurrentView().Name("commits").Content(Contains("commit 04"))
|
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`))
|
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
|
// back in master branch which just had the one commit
|
||||||
assert.CurrentView().Name("commits").Lines(
|
assert.CurrentView().Name("commits").Lines(
|
||||||
|
@ -47,7 +47,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
|
|
||||||
input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?"))
|
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(
|
assert.View("commits").TopLines(
|
||||||
Contains("second-change-branch unrelated change"),
|
Contains("second-change-branch unrelated change"),
|
||||||
|
@ -72,7 +72,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
|
|
||||||
input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?"))
|
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(
|
assert.View("commits").TopLines(
|
||||||
Contains("to keep"),
|
Contains("to keep"),
|
||||||
|
@ -70,6 +70,6 @@ var CherryPick = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
|
|
||||||
assert.View("information").Content(Contains("2 commits copied"))
|
assert.View("information").Content(Contains("2 commits copied"))
|
||||||
input.Press(keys.Universal.Return)
|
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"))
|
assert.View("information").Content(Contains("2 commits copied"))
|
||||||
input.Press(keys.Universal.Return)
|
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
|
// we start with both lines having been staged
|
||||||
assert.View("stagingSecondary").Content(Contains("+myfile content"))
|
assert.View("stagingSecondary").Content(Contains("+myfile content"))
|
||||||
assert.View("stagingSecondary").Content(Contains("+with a second line"))
|
assert.View("stagingSecondary").Content(Contains("+with a second line"))
|
||||||
assert.View("staging").Content(NotContains("+myfile content"))
|
assert.View("staging").Content(DoesNotContain("+myfile content"))
|
||||||
assert.View("staging").Content(NotContains("+with a second line"))
|
assert.View("staging").Content(DoesNotContain("+with a second line"))
|
||||||
|
|
||||||
// unstage the selected line
|
// unstage the selected line
|
||||||
input.PrimaryAction()
|
input.PrimaryAction()
|
||||||
|
|
||||||
// the line should have been moved to the main view
|
// 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("stagingSecondary").Content(Contains("+with a second line"))
|
||||||
assert.View("staging").Content(Contains("+myfile content"))
|
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)
|
input.Press(keys.Files.CommitChanges)
|
||||||
commitMessage := "my commit message"
|
commitMessage := "my commit message"
|
||||||
|
@ -25,19 +25,19 @@ var StagedWithoutHooks = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
input.Enter()
|
input.Enter()
|
||||||
assert.CurrentView().Name("stagingSecondary")
|
assert.CurrentView().Name("stagingSecondary")
|
||||||
// we start with both lines having been staged
|
// we start with both lines having been staged
|
||||||
assert.View("stagingSecondary").Content(Contains("+myfile content"))
|
assert.View("stagingSecondary").Content(
|
||||||
assert.View("stagingSecondary").Content(Contains("+with a second line"))
|
Contains("+myfile 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").DoesNotContain("+with a second line"),
|
||||||
|
)
|
||||||
|
|
||||||
// unstage the selected line
|
// unstage the selected line
|
||||||
input.PrimaryAction()
|
input.PrimaryAction()
|
||||||
|
|
||||||
// the line should have been moved to the main view
|
// the line should have been moved to the main view
|
||||||
assert.View("stagingSecondary").Content(NotContains("+myfile content"))
|
assert.View("stagingSecondary").Content(DoesNotContain("+myfile content").Contains("+with a second line"))
|
||||||
assert.View("stagingSecondary").Content(Contains("+with a second line"))
|
assert.View("staging").Content(Contains("+myfile content").DoesNotContain("+with a second line"))
|
||||||
assert.View("staging").Content(Contains("+myfile content"))
|
|
||||||
assert.View("staging").Content(NotContains("+with a second line"))
|
|
||||||
|
|
||||||
input.Press(keys.Files.CommitChangesWithoutHook)
|
input.Press(keys.Files.CommitChangesWithoutHook)
|
||||||
assert.InCommitMessagePanel()
|
assert.InCommitMessagePanel()
|
||||||
|
@ -23,10 +23,10 @@ var Unstaged = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
assert.CurrentView().Name("files").SelectedLine(Contains("myfile"))
|
assert.CurrentView().Name("files").SelectedLine(Contains("myfile"))
|
||||||
input.Enter()
|
input.Enter()
|
||||||
assert.CurrentView().Name("staging")
|
assert.CurrentView().Name("staging")
|
||||||
assert.View("stagingSecondary").Content(NotContains("+myfile content"))
|
assert.View("stagingSecondary").Content(DoesNotContain("+myfile content"))
|
||||||
// stage the first line
|
// stage the first line
|
||||||
input.PrimaryAction()
|
input.PrimaryAction()
|
||||||
assert.View("staging").Content(NotContains("+myfile content"))
|
assert.View("staging").Content(DoesNotContain("+myfile content"))
|
||||||
assert.View("stagingSecondary").Content(Contains("+myfile content"))
|
assert.View("stagingSecondary").Content(Contains("+myfile content"))
|
||||||
|
|
||||||
input.Press(keys.Files.CommitChanges)
|
input.Press(keys.Files.CommitChanges)
|
||||||
|
@ -54,7 +54,7 @@ var DiffAndApplyPatch = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
input.Press(keys.Universal.DiffingMenu)
|
input.Press(keys.Universal.DiffingMenu)
|
||||||
input.Menu(Equals("Diffing"), Contains("exit diff mode"))
|
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)
|
input.Press(keys.Universal.CreatePatchOptionsMenu)
|
||||||
// adding the regex '$' here to distinguish the menu item from the 'apply patch in reverse' item
|
// 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.CommitCount(1)
|
||||||
|
|
||||||
assert.MainView().
|
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
|
// 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)
|
// (though it would be cool if we could show that too)
|
||||||
Content(Contains("baz"))
|
Content(Contains("baz"))
|
||||||
|
Reference in New Issue
Block a user