1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-15 11:56:37 +02:00

Merge pull request #2704 from jesseduffield/int-matchers

This commit is contained in:
Jesse Duffield 2023-06-03 15:56:34 +10:00 committed by GitHub
commit 042ab2f99a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 240 additions and 160 deletions

View File

@ -39,13 +39,13 @@ import (
func CopyFile(src, dst string) (err error) { func CopyFile(src, dst string) (err error) {
in, err := os.Open(src) in, err := os.Open(src)
if err != nil { if err != nil {
return return //nolint: nakedret
} }
defer in.Close() defer in.Close()
out, err := os.Create(dst) out, err := os.Create(dst)
if err != nil { if err != nil {
return return //nolint: nakedret
} }
defer func() { defer func() {
if e := out.Close(); e != nil { if e := out.Close(); e != nil {
@ -55,21 +55,21 @@ func CopyFile(src, dst string) (err error) {
_, err = io.Copy(out, in) _, err = io.Copy(out, in)
if err != nil { if err != nil {
return return //nolint: nakedret
} }
err = out.Sync() err = out.Sync()
if err != nil { if err != nil {
return return //nolint: nakedret
} }
si, err := os.Stat(src) si, err := os.Stat(src)
if err != nil { if err != nil {
return return //nolint: nakedret
} }
err = os.Chmod(dst, si.Mode()) err = os.Chmod(dst, si.Mode())
if err != nil { if err != nil {
return return //nolint: nakedret
} }
return //nolint: nakedret return //nolint: nakedret
@ -92,7 +92,7 @@ func CopyDir(src string, dst string) (err error) {
_, err = os.Stat(dst) _, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return return //nolint: nakedret
} }
if err == nil { if err == nil {
// it exists so let's remove it // it exists so let's remove it
@ -103,12 +103,12 @@ func CopyDir(src string, dst string) (err error) {
err = os.MkdirAll(dst, si.Mode()) err = os.MkdirAll(dst, si.Mode())
if err != nil { if err != nil {
return return //nolint: nakedret
} }
entries, err := ioutil.ReadDir(src) entries, err := ioutil.ReadDir(src)
if err != nil { if err != nil {
return return //nolint: nakedret
} }
for _, entry := range entries { for _, entry := range entries {
@ -118,7 +118,7 @@ func CopyDir(src string, dst string) (err error) {
if entry.IsDir() { if entry.IsDir() {
err = CopyDir(srcPath, dstPath) err = CopyDir(srcPath, dstPath)
if err != nil { if err != nil {
return return //nolint: nakedret
} }
} else { } else {
// Skip symlinks. // Skip symlinks.
@ -128,7 +128,7 @@ func CopyDir(src string, dst string) (err error) {
err = CopyFile(srcPath, dstPath) err = CopyFile(srcPath, dstPath)
if err != nil { if err != nil {
return return //nolint: nakedret
} }
} }
} }

View File

@ -72,13 +72,13 @@ type prefixWriter struct {
writer io.Writer writer io.Writer
} }
func (self *prefixWriter) Write(p []byte) (n int, err error) { func (self *prefixWriter) Write(p []byte) (int, error) {
if !self.prefixWritten { if !self.prefixWritten {
self.prefixWritten = true self.prefixWritten = true
// assuming we can write this prefix in one go // assuming we can write this prefix in one go
_, err = self.writer.Write([]byte(self.prefix)) n, err := self.writer.Write([]byte(self.prefix))
if err != nil { if err != nil {
return return n, err
} }
} }
return self.writer.Write(p) return self.writer.Write(p)

View File

@ -11,7 +11,7 @@ func (self *AlertDriver) getViewDriver() *ViewDriver {
} }
// asserts that the alert view has the expected title // asserts that the alert view has the expected title
func (self *AlertDriver) Title(expected *Matcher) *AlertDriver { func (self *AlertDriver) Title(expected *TextMatcher) *AlertDriver {
self.getViewDriver().Title(expected) self.getViewDriver().Title(expected)
self.hasCheckedTitle = true self.hasCheckedTitle = true
@ -20,7 +20,7 @@ func (self *AlertDriver) Title(expected *Matcher) *AlertDriver {
} }
// asserts that the alert view has the expected content // asserts that the alert view has the expected content
func (self *AlertDriver) Content(expected *Matcher) *AlertDriver { func (self *AlertDriver) Content(expected *TextMatcher) *AlertDriver {
self.getViewDriver().Content(expected) self.getViewDriver().Content(expected)
self.hasCheckedContent = true self.hasCheckedContent = true

View File

@ -22,7 +22,7 @@ func retryWaitTimes() []int {
} }
} }
func (self *assertionHelper) matchString(matcher *Matcher, context string, getValue func() string) { func (self *assertionHelper) matchString(matcher *TextMatcher, context string, getValue func() string) {
self.assertWithRetries(func() (bool, string) { self.assertWithRetries(func() (bool, string) {
value := getValue() value := getValue()
return matcher.context(context).test(value) return matcher.context(context).test(value)

View File

@ -9,19 +9,19 @@ func (self *CommitMessagePanelDriver) getViewDriver() *ViewDriver {
} }
// asserts on the text initially present in the prompt // asserts on the text initially present in the prompt
func (self *CommitMessagePanelDriver) InitialText(expected *Matcher) *CommitMessagePanelDriver { func (self *CommitMessagePanelDriver) InitialText(expected *TextMatcher) *CommitMessagePanelDriver {
return self.Content(expected) return self.Content(expected)
} }
// asserts on the current context in the prompt // asserts on the current context in the prompt
func (self *CommitMessagePanelDriver) Content(expected *Matcher) *CommitMessagePanelDriver { func (self *CommitMessagePanelDriver) Content(expected *TextMatcher) *CommitMessagePanelDriver {
self.getViewDriver().Content(expected) self.getViewDriver().Content(expected)
return self return self
} }
// asserts that the confirmation view has the expected title // asserts that the confirmation view has the expected title
func (self *CommitMessagePanelDriver) Title(expected *Matcher) *CommitMessagePanelDriver { func (self *CommitMessagePanelDriver) Title(expected *TextMatcher) *CommitMessagePanelDriver {
self.getViewDriver().Title(expected) self.getViewDriver().Title(expected)
return self return self

View File

@ -39,7 +39,7 @@ func (self *Common) ConfirmDiscardLines() {
Confirm() Confirm()
} }
func (self *Common) SelectPatchOption(matcher *Matcher) { func (self *Common) SelectPatchOption(matcher *TextMatcher) {
self.t.GlobalPress(self.t.keys.Universal.CreatePatchOptionsMenu) self.t.GlobalPress(self.t.keys.Universal.CreatePatchOptionsMenu)
self.t.ExpectPopup().Menu().Title(Equals("Patch options")).Select(matcher).Confirm() self.t.ExpectPopup().Menu().Title(Equals("Patch options")).Select(matcher).Confirm()

View File

@ -11,7 +11,7 @@ func (self *ConfirmationDriver) getViewDriver() *ViewDriver {
} }
// asserts that the confirmation view has the expected title // asserts that the confirmation view has the expected title
func (self *ConfirmationDriver) Title(expected *Matcher) *ConfirmationDriver { func (self *ConfirmationDriver) Title(expected *TextMatcher) *ConfirmationDriver {
self.getViewDriver().Title(expected) self.getViewDriver().Title(expected)
self.hasCheckedTitle = true self.hasCheckedTitle = true
@ -20,7 +20,7 @@ func (self *ConfirmationDriver) Title(expected *Matcher) *ConfirmationDriver {
} }
// asserts that the confirmation view has the expected content // asserts that the confirmation view has the expected content
func (self *ConfirmationDriver) Content(expected *Matcher) *ConfirmationDriver { func (self *ConfirmationDriver) Content(expected *TextMatcher) *ConfirmationDriver {
self.getViewDriver().Content(expected) self.getViewDriver().Content(expected)
self.hasCheckedContent = true 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 // 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 *TextMatcher) {
self.assertWithRetries(func() (bool, string) { self.assertWithRetries(func() (bool, string) {
_, err := os.Stat(path) _, err := os.Stat(path)
if os.IsNotExist(err) { if os.IsNotExist(err) {

View File

@ -0,0 +1,58 @@
package components
import (
"fmt"
)
type IntMatcher struct {
*Matcher[int]
}
func (self *IntMatcher) EqualsInt(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
name: fmt.Sprintf("equals '%d'", target),
testFn: func(value int) (bool, string) {
return value == target, fmt.Sprintf("Expected '%d' to equal '%d'", value, target)
},
})
return self
}
func (self *IntMatcher) GreaterThan(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
name: fmt.Sprintf("greater than '%d'", target),
testFn: func(value int) (bool, string) {
return value > target, fmt.Sprintf("Expected '%d' to greater than '%d'", value, target)
},
})
return self
}
func (self *IntMatcher) LessThan(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
name: fmt.Sprintf("less than '%d'", target),
testFn: func(value int) (bool, string) {
return value < target, fmt.Sprintf("Expected '%d' to less than '%d'", value, target)
},
})
return self
}
func AnyInt() *IntMatcher {
return &IntMatcher{Matcher: &Matcher[int]{}}
}
func EqualsInt(target int) *IntMatcher {
return AnyInt().EqualsInt(target)
}
func GreaterThan(target int) *IntMatcher {
return AnyInt().GreaterThan(target)
}
func LessThan(target int) *IntMatcher {
return AnyInt().LessThan(target)
}

View File

@ -1,46 +1,39 @@
package components package components
import ( import (
"fmt"
"regexp"
"strings" "strings"
"github.com/samber/lo" "github.com/samber/lo"
) )
// for making assertions on string values // for making assertions on string values
type Matcher struct { type Matcher[T any] struct {
rules []matcherRule rules []matcherRule[T]
// this is printed when there's an error so that it's clear what the context of the assertion is // this is printed when there's an error so that it's clear what the context of the assertion is
prefix string prefix string
} }
type matcherRule struct { type matcherRule[T any] 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(T) (bool, string)
} }
func NewMatcher(name string, testFn func(string) (bool, string)) *Matcher { func (self *Matcher[T]) name() string {
rules := []matcherRule{{name: name, testFn: testFn}}
return &Matcher{rules: rules}
}
func (self *Matcher) name() string {
if len(self.rules) == 0 { if len(self.rules) == 0 {
return "anything" return "anything"
} }
return strings.Join( return strings.Join(
lo.Map(self.rules, func(rule matcherRule, _ int) string { return rule.name }), lo.Map(self.rules, func(rule matcherRule[T], _ int) string { return rule.name }),
", ", ", ",
) )
} }
func (self *Matcher) test(value string) (bool, string) { func (self *Matcher[T]) test(value T) (bool, string) {
for _, rule := range self.rules { for _, rule := range self.rules {
ok, message := rule.testFn(value) ok, message := rule.testFn(value)
if ok { if ok {
@ -57,65 +50,7 @@ func (self *Matcher) test(value string) (bool, string) {
return true, "" return true, ""
} }
func (self *Matcher) Contains(target string) *Matcher { func (self *Matcher[T]) appendRule(rule matcherRule[T]) *Matcher[T] {
return self.appendRule(matcherRule{
name: fmt.Sprintf("contains '%s'", target),
testFn: func(value string) (bool, string) {
// everything contains the empty string so we unconditionally return true here
if target == "" {
return true, ""
}
return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value)
},
})
}
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) {
return !strings.Contains(value, target), fmt.Sprintf("Expected '%s' to NOT be found in '%s'", target, value)
},
})
}
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) {
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 (self *Matcher) Equals(target string) *Matcher {
return self.appendRule(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)
},
})
}
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 {
return self.appendRule(matcherRule{
name: IS_SELECTED_RULE_NAME,
testFn: func(value string) (bool, string) {
panic("Special IsSelected matcher is not supposed to have its testFn method called. This rule should only be used within the .Lines() and .TopLines() method on a ViewAsserter.")
},
})
}
func (self *Matcher) appendRule(rule matcherRule) *Matcher {
self.rules = append(self.rules, rule) self.rules = append(self.rules, rule)
return self return self
@ -123,43 +58,8 @@ 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. // 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'." // E.g. prefix: "Unexpected content in view 'files'."
func (self *Matcher) context(prefix string) *Matcher { func (self *Matcher[T]) context(prefix string) *Matcher[T] {
self.prefix = prefix self.prefix = prefix
return self 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) {
// copying into a new matcher in case we want to re-use the original later
newMatcher := &Matcher{}
*newMatcher = *self
check := lo.ContainsBy(newMatcher.rules, func(rule matcherRule) bool { return rule.name == IS_SELECTED_RULE_NAME })
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 Contains(target string) *Matcher {
return Anything().Contains(target)
}
func DoesNotContain(target string) *Matcher {
return Anything().DoesNotContain(target)
}
func MatchesRegexp(target string) *Matcher {
return Anything().MatchesRegexp(target)
}
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 // asserts that the popup has the expected title
func (self *MenuDriver) Title(expected *Matcher) *MenuDriver { func (self *MenuDriver) Title(expected *TextMatcher) *MenuDriver {
self.getViewDriver().Title(expected) self.getViewDriver().Title(expected)
self.hasCheckedTitle = true self.hasCheckedTitle = true
@ -30,19 +30,19 @@ func (self *MenuDriver) Cancel() {
self.getViewDriver().PressEscape() self.getViewDriver().PressEscape()
} }
func (self *MenuDriver) Select(option *Matcher) *MenuDriver { func (self *MenuDriver) Select(option *TextMatcher) *MenuDriver {
self.getViewDriver().NavigateToLine(option) self.getViewDriver().NavigateToLine(option)
return self return self
} }
func (self *MenuDriver) Lines(matchers ...*Matcher) *MenuDriver { func (self *MenuDriver) Lines(matchers ...*TextMatcher) *MenuDriver {
self.getViewDriver().Lines(matchers...) self.getViewDriver().Lines(matchers...)
return self return self
} }
func (self *MenuDriver) TopLines(matchers ...*Matcher) *MenuDriver { func (self *MenuDriver) TopLines(matchers ...*TextMatcher) *MenuDriver {
self.getViewDriver().TopLines(matchers...) self.getViewDriver().TopLines(matchers...)
return self return self

View File

@ -10,7 +10,7 @@ func (self *PromptDriver) getViewDriver() *ViewDriver {
} }
// asserts that the popup has the expected title // asserts that the popup has the expected title
func (self *PromptDriver) Title(expected *Matcher) *PromptDriver { func (self *PromptDriver) Title(expected *TextMatcher) *PromptDriver {
self.getViewDriver().Title(expected) self.getViewDriver().Title(expected)
self.hasCheckedTitle = true self.hasCheckedTitle = true
@ -19,7 +19,7 @@ func (self *PromptDriver) Title(expected *Matcher) *PromptDriver {
} }
// asserts on the text initially present in the prompt // asserts on the text initially present in the prompt
func (self *PromptDriver) InitialText(expected *Matcher) *PromptDriver { func (self *PromptDriver) InitialText(expected *TextMatcher) *PromptDriver {
self.getViewDriver().Content(expected) self.getViewDriver().Content(expected)
return self return self
@ -55,13 +55,13 @@ func (self *PromptDriver) checkNecessaryChecksCompleted() {
} }
} }
func (self *PromptDriver) SuggestionLines(matchers ...*Matcher) *PromptDriver { func (self *PromptDriver) SuggestionLines(matchers ...*TextMatcher) *PromptDriver {
self.t.Views().Suggestions().Lines(matchers...) self.t.Views().Suggestions().Lines(matchers...)
return self return self
} }
func (self *PromptDriver) SuggestionTopLines(matchers ...*Matcher) *PromptDriver { func (self *PromptDriver) SuggestionTopLines(matchers ...*TextMatcher) *PromptDriver {
self.t.Views().Suggestions().TopLines(matchers...) self.t.Views().Suggestions().TopLines(matchers...)
return self return self
@ -75,7 +75,7 @@ func (self *PromptDriver) ConfirmFirstSuggestion() {
PressEnter() PressEnter()
} }
func (self *PromptDriver) ConfirmSuggestion(matcher *Matcher) { func (self *PromptDriver) ConfirmSuggestion(matcher *TextMatcher) {
self.t.press(self.t.keys.Universal.TogglePanel) self.t.press(self.t.keys.Universal.TogglePanel)
self.t.Views().Suggestions(). self.t.Views().Suggestions().
IsFocused(). IsFocused().

View File

@ -12,7 +12,7 @@ func (self *SearchDriver) getViewDriver() *ViewDriver {
} }
// asserts on the text initially present in the prompt // asserts on the text initially present in the prompt
func (self *SearchDriver) InitialText(expected *Matcher) *SearchDriver { func (self *SearchDriver) InitialText(expected *TextMatcher) *SearchDriver {
self.getViewDriver().Content(expected) self.getViewDriver().Content(expected)
return self return self

View File

@ -80,11 +80,11 @@ func (self *TestDriver) ExpectPopup() *Popup {
return &Popup{t: self} return &Popup{t: self}
} }
func (self *TestDriver) ExpectToast(matcher *Matcher) { func (self *TestDriver) ExpectToast(matcher *TextMatcher) {
self.Views().AppStatus().Content(matcher) self.Views().AppStatus().Content(matcher)
} }
func (self *TestDriver) ExpectClipboard(matcher *Matcher) { func (self *TestDriver) ExpectClipboard(matcher *TextMatcher) {
self.assertWithRetries(func() (bool, string) { self.assertWithRetries(func() (bool, string) {
text, err := clipboard.ReadAll() text, err := clipboard.ReadAll()
if err != nil { if err != nil {

View File

@ -0,0 +1,116 @@
package components
import (
"fmt"
"regexp"
"strings"
"github.com/samber/lo"
)
type TextMatcher struct {
*Matcher[string]
}
func (self *TextMatcher) Contains(target string) *TextMatcher {
self.appendRule(matcherRule[string]{
name: fmt.Sprintf("contains '%s'", target),
testFn: func(value string) (bool, string) {
// everything contains the empty string so we unconditionally return true here
if target == "" {
return true, ""
}
return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value)
},
})
return self
}
func (self *TextMatcher) DoesNotContain(target string) *TextMatcher {
self.appendRule(matcherRule[string]{
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)
},
})
return self
}
func (self *TextMatcher) MatchesRegexp(target string) *TextMatcher {
self.appendRule(matcherRule[string]{
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)
},
})
return self
}
func (self *TextMatcher) Equals(target string) *TextMatcher {
self.appendRule(matcherRule[string]{
name: fmt.Sprintf("equals '%s'", target),
testFn: func(value string) (bool, string) {
return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target)
},
})
return self
}
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 *TextMatcher) IsSelected() *TextMatcher {
self.appendRule(matcherRule[string]{
name: IS_SELECTED_RULE_NAME,
testFn: func(value string) (bool, string) {
panic("Special IsSelected matcher is not supposed to have its testFn method called. This rule should only be used within the .Lines() and .TopLines() method on a ViewAsserter.")
},
})
return self
}
// if the matcher has an `IsSelected` rule, it returns true, along with the matcher after that rule has been removed
func (self *TextMatcher) checkIsSelected() (bool, *TextMatcher) {
// copying into a new matcher in case we want to re-use the original later
newMatcher := &TextMatcher{}
*newMatcher = *self
check := lo.ContainsBy(newMatcher.rules, func(rule matcherRule[string]) bool { return rule.name == IS_SELECTED_RULE_NAME })
newMatcher.rules = lo.Filter(newMatcher.rules, func(rule matcherRule[string], _ 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 AnyString() *TextMatcher {
return &TextMatcher{Matcher: &Matcher[string]{}}
}
func Contains(target string) *TextMatcher {
return AnyString().Contains(target)
}
func DoesNotContain(target string) *TextMatcher {
return AnyString().DoesNotContain(target)
}
func MatchesRegexp(target string) *TextMatcher {
return AnyString().MatchesRegexp(target)
}
func Equals(target string) *TextMatcher {
return AnyString().Equals(target)
}

View File

@ -52,7 +52,7 @@ func (self *ViewDriver) getSelectedLineIdx() (int, error) {
} }
// asserts that the view has the expected title // asserts that the view has the expected title
func (self *ViewDriver) Title(expected *Matcher) *ViewDriver { func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) { self.t.assertWithRetries(func() (bool, string) {
actual := self.getView().Title actual := self.getView().Title
return expected.context(fmt.Sprintf("%s title", self.context)).test(actual) return expected.context(fmt.Sprintf("%s title", self.context)).test(actual)
@ -64,7 +64,7 @@ func (self *ViewDriver) Title(expected *Matcher) *ViewDriver {
// asserts that the view has lines matching the given matchers. One matcher must be passed for each line. // 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. // If you only care about the top n lines, use the TopLines method instead.
// If you only care about a subset of lines, use the ContainsLines method instead. // If you only care about a subset of lines, use the ContainsLines method instead.
func (self *ViewDriver) Lines(matchers ...*Matcher) *ViewDriver { func (self *ViewDriver) Lines(matchers ...*TextMatcher) *ViewDriver {
self.validateMatchersPassed(matchers) self.validateMatchersPassed(matchers)
self.LineCount(len(matchers)) self.LineCount(len(matchers))
@ -75,7 +75,7 @@ func (self *ViewDriver) Lines(matchers ...*Matcher) *ViewDriver {
// are passed, we only check the first three lines of the view. // 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 // This method is convenient when you have a list of commits but you only want to
// assert on the first couple of commits. // assert on the first couple of commits.
func (self *ViewDriver) TopLines(matchers ...*Matcher) *ViewDriver { func (self *ViewDriver) TopLines(matchers ...*TextMatcher) *ViewDriver {
self.validateMatchersPassed(matchers) self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers) self.validateEnoughLines(matchers)
@ -83,7 +83,7 @@ func (self *ViewDriver) TopLines(matchers ...*Matcher) *ViewDriver {
} }
// asserts that somewhere in the view there are consequetive lines matching the given matchers. // asserts that somewhere in the view there are consequetive lines matching the given matchers.
func (self *ViewDriver) ContainsLines(matchers ...*Matcher) *ViewDriver { func (self *ViewDriver) ContainsLines(matchers ...*TextMatcher) *ViewDriver {
self.validateMatchersPassed(matchers) self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers) self.validateEnoughLines(matchers)
@ -162,7 +162,7 @@ func (self *ViewDriver) DoesNotContainColoredText(fgColorStr string, text string
} }
// asserts on the lines that are selected in the view. Don't use the `IsSelected` matcher with this because it's redundant. // asserts on the lines that are selected in the view. Don't use the `IsSelected` matcher with this because it's redundant.
func (self *ViewDriver) SelectedLines(matchers ...*Matcher) *ViewDriver { func (self *ViewDriver) SelectedLines(matchers ...*TextMatcher) *ViewDriver {
self.validateMatchersPassed(matchers) self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers) self.validateEnoughLines(matchers)
@ -197,13 +197,13 @@ func (self *ViewDriver) SelectedLines(matchers ...*Matcher) *ViewDriver {
return self return self
} }
func (self *ViewDriver) validateMatchersPassed(matchers []*Matcher) { func (self *ViewDriver) validateMatchersPassed(matchers []*TextMatcher) {
if len(matchers) < 1 { 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()") 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) validateEnoughLines(matchers []*Matcher) { func (self *ViewDriver) validateEnoughLines(matchers []*TextMatcher) {
view := self.getView() view := self.getView()
self.t.assertWithRetries(func() (bool, string) { self.t.assertWithRetries(func() (bool, string) {
@ -212,7 +212,7 @@ func (self *ViewDriver) validateEnoughLines(matchers []*Matcher) {
}) })
} }
func (self *ViewDriver) assertLines(offset int, matchers ...*Matcher) *ViewDriver { func (self *ViewDriver) assertLines(offset int, matchers ...*TextMatcher) *ViewDriver {
view := self.getView() view := self.getView()
for matcherIndex, matcher := range matchers { for matcherIndex, matcher := range matchers {
@ -252,7 +252,7 @@ func (self *ViewDriver) assertLines(offset int, matchers ...*Matcher) *ViewDrive
} }
// asserts on the content of the view i.e. the stuff within the view's frame. // 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 *TextMatcher) *ViewDriver {
self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context), self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context),
func() string { func() string {
return self.getView().Buffer() return self.getView().Buffer()
@ -265,7 +265,7 @@ func (self *ViewDriver) Content(matcher *Matcher) *ViewDriver {
// asserts on the selected line of the view. If your view has multiple lines selected, // 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 // 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. // the cursor is on. Otherwise it will assert on the first line of the selection.
func (self *ViewDriver) SelectedLine(matcher *Matcher) *ViewDriver { func (self *ViewDriver) SelectedLine(matcher *TextMatcher) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) { self.t.assertWithRetries(func() (bool, string) {
selectedLineIdx, err := self.getSelectedLineIdx() selectedLineIdx, err := self.getSelectedLineIdx()
if err != nil { if err != nil {
@ -410,7 +410,7 @@ func (self *ViewDriver) PressEscape() *ViewDriver {
// If this changes in future, we'll need to update this code to first attempt to find the item // 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, // in the current page and failing that, jump to the top of the view and iterate through all of it,
// looking for the item. // looking for the item.
func (self *ViewDriver) NavigateToLine(matcher *Matcher) *ViewDriver { func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
self.IsFocused() self.IsFocused()
view := self.getView() view := self.getView()
@ -510,8 +510,8 @@ func (self *ViewDriver) Self() *ViewDriver {
return self return self
} }
func expectedContentFromMatchers(matchers []*Matcher) string { func expectedContentFromMatchers(matchers []*TextMatcher) string {
return strings.Join(lo.Map(matchers, func(matcher *Matcher, _ int) string { return strings.Join(lo.Map(matchers, func(matcher *TextMatcher, _ int) string {
return matcher.name() return matcher.name()
}), "\n") }), "\n")
} }

View File

@ -19,8 +19,8 @@ var RemoteNamedStar = NewIntegrationTest(NewIntegrationTestArgs{
// here we're just asserting that we haven't panicked upon starting lazygit // here we're just asserting that we haven't panicked upon starting lazygit
t.Views().Commits(). t.Views().Commits().
Lines( Lines(
Anything(), AnyString(),
Anything(), AnyString(),
) )
}, },
}) })

View File

@ -28,6 +28,12 @@ var Patch = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit (initial): one"), Contains("commit (initial): one"),
). ).
SelectNextItem(). SelectNextItem().
Lines(
Contains("reset: moving to HEAD^^"),
Contains("commit: three").IsSelected(),
Contains("commit: two"),
Contains("commit (initial): one"),
).
PressEnter() PressEnter()
t.Views().SubCommits(). t.Views().SubCommits().